Les applications mobiles ne sont pas toujours simples et concises, comme nous les développeurs l'adorons. D'autres applications sont créées pour résoudre des problèmes complexes d'utilisateurs et contiennent de nombreux écrans et scripts. Par exemple, des applications pour effectuer des tests, des questionnaires et des enquêtes - partout où vous devez remplir de nombreux formulaires au cours du processus. Cette application sera discutée dans cet article.

Nous avons commencé à développer une application mobile pour les agents qui effectuent l'enregistrement sur place des polices d'assurance. Ils remplissent de grands formulaires dans l'application avec des données clients: informations sur la voiture, les propriétaires, les conducteurs, etc. Bien que chaque formulaire ait ses propres sections, cellules et structure et que chaque élément du questionnaire nécessite un type de données unique (chaîne, date, document joint), les formulaires d'écran étaient assez similaires. Mais l'essentiel est leur nombre ... Personne ne veut s'engager à plusieurs reprises dans la visualisation et le traitement des mêmes éléments.
Pour éviter les nombreuses heures de travail manuel sur la création de formulaires, vous devez appliquer un peu d'ingéniosité et beaucoup de construction dynamique de l'interface utilisateur. Dans cet article, nous voulons partager comment nous avons résolu ce problème.
Pour une solution élégante au problème, nous avons utilisé le mécanisme de génération d'objets - ViewModels, qui sont utilisés pour créer des formulaires personnalisés à l'aide de tableaux.

Dans le travail normal, pour chaque table individuelle que le développeur souhaite voir à l'écran, une classe ViewModel distincte doit être créée. Il définit la composante visuelle de la table. Nous avons décidé d'aller un niveau plus haut et de générer dynamiquement des ViewModels et des modèles, en utilisant une simple description de la structure à travers les champs Enum.
Comment ça marche
Tout a commencé avec enum. Pour chaque profil, nous créons une énumération unique - ce sont nos sections du profil. L'une de ses méthodes consiste à renvoyer le tableau de cellules dans cette section.
Les cellules du tableau seront également énumérées avec des fonctions supplémentaires qui décriront les propriétés des cellules. Dans de telles fonctions, nous définissons le nom de la cellule, la valeur initiale. Paramètres ajoutés ultérieurement tels que
- vérification de l'affichage: certaines cellules doivent être masquées,
- liste des cellules «parents»: cellules dont dépend la valeur, la validation ou l'affichage de cette cellule,
- type de cellule: cellules simples avec valeurs, cellules en switch, cellules avec fonction d'ajout d'éléments, etc.
Nous souscrivons toutes les sections au protocole général QuestionnaireSectionCellType afin d'exclure la liaison à une section spécifique, nous ferons de même avec toutes les cellules du tableau (QuestionnaireCellType).
protocol QuestionnaireSectionCellType { var title: String { get } var sectionCellTypes: [QuestionnaireCellType] { get } } protocol QuestionnaireCellType { var title: String { get } var initialValue: Any? { get } var isHidden: Bool { get } var parentFields: [QuestionnaireCellType] { get } … }
Un tel modèle sera très facile à remplir. Nous parcourons simplement toutes les sections, dans chaque section, nous parcourons un tableau de cellules et les ajoutons au tableau de modèles.
Sur l'exemple de l'écran du preneur d'assurance (énumération des sections - InsurantSectionType):
final class InsurantModel: BaseModel<QuestionnaireCellType> { override init() { super.init() initParameters() } private func initParameters() { InsurantSectionType.allCases.forEach { type in type.sectionCellTypes.forEach { if let valueModel = ValueModel(type: $0, parentFields: $0.parentFields, value: $0.initialValue) { valueModels.append(valueModel) } } } } }
C'est fait! Nous avons maintenant un tableau avec les valeurs initiales. Ajoutez des méthodes pour lire la valeur avec la clé QuestionnaireCellType et l'enregistrer dans l'élément de tableau souhaité.
Certains modèles peuvent avoir des champs facultatifs, nous avons donc ajouté un tableau avec des clés facultatives. Lors de la validation du modèle, ces clés peuvent ne pas contenir de valeurs, mais le modèle sera considéré comme rempli.
De plus, pour plus de commodité, toutes les valeurs du ValueModel nous souscrivent au protocole commun StringRepresentable pour limiter la liste des valeurs possibles et ajouter une méthode pour afficher la valeur dans la cellule.
protocol StringRepresentable { var stringValue: String? { get } }
La fonctionnalité s'est développée et de nombreuses autres propriétés et méthodes sont apparues dans les modèles: nettoyage du modèle (les valeurs initiales doivent être définies dans certains modèles), prise en charge d'un tableau dynamique de valeurs (valeur: tableau), etc.
Cette approche s'est avérée très pratique pour le stockage dans la base de données à l'aide de Realm. Pour remplir le questionnaire, il est possible de sélectionner un modèle complété précédemment enregistré. Pour étendre la politique CTP, l'agent n'aura plus besoin de remplir les documents de l'utilisateur, les pilotes attachés par lui et les données TCP pour le nouveau. Au lieu de cela, vous pouvez simplement le réutiliser pour remplir celui existant.
Pour modifier ou compléter des tableaux, il vous suffit de trouver le ViewModel lié à un écran particulier, de trouver l'énumération nécessaire responsable de l'affichage du bloc souhaité et d'ajouter ou de corriger plusieurs cas. Tout, la table prendra la forme nécessaire!
Remplir le formulaire avec des valeurs de test était également très pratique et rapide. De cette façon, vous pouvez générer rapidement des données de test. Et si vous ajoutez un fichier séparé avec les données initiales, d'où le programme prendra la valeur dans chaque champ spécifique du questionnaire, alors même un débutant peut générer des questionnaires prêts à l'emploi sans entrer et démonter le reste du code, à l'exception d'un fichier spécifique.
Dépendances
Une tâche distincte que nous avons résolue au cours du processus de développement est la gestion des dépendances. Certains éléments du questionnaire étaient interconnectés. Ainsi, le numéro de document ne peut pas être rempli sans choisir le type de ce document lui-même, le numéro de maison ne peut pas être indiqué sans indiquer la ville et la rue, etc.

Nous avons mis à jour les valeurs du questionnaire en effaçant tous les champs dépendants (Par exemple, en supprimant ou en changeant le type d'un document, nous effaçons le champ «numéro de document»):
func updateValueModel(value: StringRepresentable?, for type: QuestionnaireCellType) { guard let model = valueModels.first(where: { $0.type.equal(to: type) }) else { return } model.value = value clearRelativeValues(type: type) } func clearRelativeValues(type: QuestionnaireCellType) { _ = valueModels.filter { $0.parentFields.contains(where: { $0.equal(to: type) }) } .compactMap { $0.type } .compactMap { updateValueModel(value: nil, for: $0) } }
Pièges que nous avons dû résoudre pendant le développement et comment nous avons réussi
Il est clair que cette méthode est pratique pour les écrans ayant la même fonctionnalité (en remplissant les champs), mais pas si pratique si des éléments ou des fonctions uniques apparaissent sur un écran séparé qui ne sont pas sur d'autres écrans. Dans notre candidature, ce sont:
- Un écran avec la puissance du moteur, qui devait être généré séparément, c'est pourquoi il différait en fonctionnalités. Sur cet écran, la demande devrait disparaître et la valeur du serveur est automatiquement remplacée. J'ai dû lui créer séparément une classe qui serait chargée d'afficher, de charger, de valider, de charger depuis le serveur et de substituer une valeur dans un champ vide, sans déranger l'utilisateur si ce dernier décide d'entrer sa propre valeur.
- L'écran du numéro d'enregistrement, dans lequel le seul est le commutateur, ce qui affecte l'affichage ou le masquage du champ de texte. Pour ce cas, une condition supplémentaire devait être effectuée, qui déterminerait par programme les cas avec la position du commutateur activée comme valeur vide.
- Des listes dynamiques, comme une liste de pilotes qui devaient être stockés et liés à un formulaire, qui sont également sortis du concept.
- Types uniques de validation des données. Cela pourrait être beaucoup de masques mélangés à des regex'ami. Et la validation de la date pour divers domaines, où la validation différait considérablement (restrictions sur les valeurs minimum / maximum), etc.
- Les écrans de saisie des données sont constitués de cellules collectionView. (Cela était requis par la conception!) Pour cette raison, l'affichage des fenêtres modales nécessitait un contrôle précis sur l'index sélectionné. J'ai dû vérifier les champs disponibles pour le remplissage et exclure de la liste ceux que l'utilisateur ne devrait pas voir.
- Pour afficher correctement les données dans le tableau, il a fallu apporter des modifications aux méthodes de modélisation de certains écrans. Les cellules telles que le nom et l'adresse sont affichées dans le tableau comme un seul élément, mais nécessitent plusieurs écrans contextuels pour être entièrement remplis.
Conclusion
Cette expérience nous a permis chez True Engineering de mettre en œuvre rapidement une application mobile facile à entretenir. La polyvalence vous permet de générer rapidement des tableaux avec différents types de données d'entrée: nous avons réalisé 20 fenêtres en seulement une semaine. Cette approche accélère également le processus de test des applications. Dans un avenir proche, nous réutiliserons l'usine finie pour générer rapidement de nouvelles tables et de nouvelles fonctionnalités.