Probablement, dans chaque grand projet iOS, celui qui dure longtemps peut tomber sur des icônes qui ne sont utilisées nulle part, ou accéder à des clés de localisation qui ont disparu depuis longtemps. Le plus souvent, de telles situations résultent de l'inattention, et l'automatisation est le meilleur remède contre l'inattention.
Au sein de l'équipe iOS de HeadHunter, nous accordons une grande attention à l'automatisation des tâches de routine qu'un développeur peut rencontrer. Avec cet article, nous voulons commencer un cycle d'histoires sur ces outils et approches qui simplifient notre travail quotidien.
Il y a quelque temps, nous avons réussi à prendre le contrôle des ressources des applications à l'aide de l'utilitaire SwiftGen. Comment le configurer, comment vivre avec et comment cet utilitaire aide à déplacer la vérification de la pertinence des ressources vers le compilateur, et nous parlerons de cat.

SwiftGen est un utilitaire qui vous permet de générer du code Swift pour accéder à diverses ressources d'un projet Xcode, parmi lesquelles:
- polices
- les couleurs
- storyboards;
- chaînes de localisation;
- actifs.
Tout le monde peut écrire un code similaire pour initialiser des images ou des chaînes de localisation:
logoImageView.image = UIImage(named: "Swift") nameLabel.text = String( format: NSLocalizedString("languages.swift.name", comment: ""), locale: Locale.current )
Pour indiquer le nom de l'image ou de la clé de localisation, nous utilisons des littéraux de chaîne. Ce qui est écrit entre guillemets doubles n'est pas validé par le compilateur ou l'environnement de développement (Xcode). C'est là que réside l'ensemble des problèmes suivants:
- Vous pouvez faire une faute de frappe;
- vous pouvez oublier de mettre à jour l'utilisation du code après avoir modifié ou supprimé la clé / l'image.
Voyons comment nous pouvons améliorer ce code avec SwiftGen.
Pour notre équipe, la génération n'était pertinente que pour les chaînes et les actifs, et ils seront abordés dans l'article. La génération pour d'autres types de ressources est similaire et, si vous le souhaitez, est facilement maîtrisée indépendamment.
Mise en œuvre du projet
Vous devez d'abord installer SwiftGen. Nous avons choisi de l'installer via CocoaPods comme un moyen pratique de distribuer l'utilitaire entre tous les membres de l'équipe. Mais cela peut être fait d'autres manières, qui sont décrites en détail dans la documentation . Dans notre cas, il pod 'SwiftGen'
ajouter le pod 'SwiftGen'
, puis d'ajouter une nouvelle phase de construction ( Build Phase
) qui lancera SwiftGen avant de construire le projet.
"$PODS_ROOT"/SwiftGen/bin/swiftgen
Il est important d'exécuter SwiftGen avant de démarrer la phase de Compile Sources
pour éviter les erreurs lors de la compilation du projet.

Nous sommes maintenant prêts à adapter SwiftGen à notre projet.
Configurer SwiftGen
Tout d'abord, vous devez configurer les modèles par lesquels le code d'accès aux ressources sera généré. L'utilitaire contient déjà un ensemble de modèles pour générer du code, ils peuvent tous être consultés sur le github et, en principe, ils sont prêts à l'emploi. Les modèles sont écrits en langage Stencil , vous le connaissez peut-être si vous avez utilisé Sourcery ou joué avec Kitura . Si vous le souhaitez, chacun des modèles peut être adapté à leurs propres guides.
Par exemple, prenez le modèle généré par enum
pour accéder aux chaînes de localisation. Il nous a semblé que dans la norme il y a trop de superflu et cela peut être simplifié. Un exemple simplifié avec des commentaires explicatifs se trouve sous le spoiler.
Exemple de modèle {# #} {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} {# #} {% macro parametersBlock types %}{% filter removeNewlines:"leading" %} {% for type in types %} _ p{{forloop.counter}}: {{type}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {% macro argumentsBlock types %}{% filter removeNewlines:"leading" %} {% for type in types %} p{{forloop.counter}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {# enum #} {% macro recursiveBlock table item sp %} {{sp}}{% for string in item.strings %} {{sp}}{% if not param.noComments %} {{sp}}/// {{string.translation}} {{sp}}{% endif %} {{sp}}{% if string.types %} {{sp}}{{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { {{sp}} return localize("{{string.key}}", {% call argumentsBlock string.types %}) {{sp}}} {{sp}}{% else %} {{sp}}{{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = localize("{{string.key}}") {{sp}}{% endif %} {{sp}}{% endfor %} {{sp}}{% for child in item.children %} {{sp}}{{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { {{sp}}{% set sp2 %}{{sp}} {% endset %} {{sp}}{% call recursiveBlock table child sp2 %} {{sp}}} {{sp}}{% endfor %} {% endmacro %} import Foundation {# enum #} {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} {{accessModifier}} enum {{enumName}} { {% if tables.count > 1 %} {% for table in tables %} {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { {% call recursiveBlock table.name table.levels " " %} } {% endfor %} {% else %} {% call recursiveBlock tables.first.name tables.first.levels " " %} {% endif %} } {# enum Localization #} extension Localization { fileprivate static func localize(_ key: String, _ args: CVarArg...) -> String { return String( format: NSLocalizedString(key, comment: ""), locale: Locale.current, arguments: args ) } }
Il est pratique d'enregistrer le fichier de modèle lui-même à la racine du projet, par exemple, dans le dossier SwiftGen/Templates
afin que ce modèle soit disponible pour tous ceux qui travaillent sur le projet.
L'utilitaire prend en charge la configuration via le fichier YAML swiftgen.yml
, dans lequel vous pouvez spécifier les chemins d'accès aux fichiers source, aux modèles et aux paramètres supplémentaires. Créez-le à la racine du projet dans le dossier Swiftgen
, puis regroupez les autres fichiers liés au script dans le même dossier.
Pour notre projet, ce fichier peut ressembler à ceci:
xcassets: - paths: ../SwiftGenExample/Assets.xcassets templatePath: Templates/ImageAssets.stencil output: ../SwiftGenExample/Image.swift params: enumName: Image publicAccess: 1 noAllValues: 1 strings: - paths: ../SwiftGenExample/en.lproj/Localizable.strings templatePath: Templates/LocalizableStrings.stencil output: ../SwiftGenExample/Localization.swift params: enumName: Localization publicAccess: 1 noComments: 0
En fait, les chemins d'accès aux fichiers et aux modèles y sont indiqués, ainsi que les paramètres supplémentaires qui sont passés au contexte du modèle.
Étant donné que le fichier n'est pas à la racine du projet, nous devons spécifier le chemin d'accès à celui-ci lors du démarrage de Swiftgen. Modifiez notre script de démarrage:
"$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml
Maintenant, notre projet peut être assemblé. Après l'assemblage, deux fichiers Localization.swift
et Image.swift
doivent apparaître dans le dossier du projet le long des chemins spécifiés dans Image.swift
. Ils doivent être ajoutés au projet Xcode. Dans notre cas, les fichiers générés contiennent les éléments suivants:
Pour les cordes: public enum Localization { public enum Languages { public enum ObjectiveC {
Pour les images: public enum Image { public enum Logos { public static var objectiveC: UIImage { return image(named: "ObjectiveC") } public static var swift: UIImage { return image(named: "Swift") } } private static func image(named name: String) -> UIImage { let bundle = Bundle(for: BundleToken.self) guard let image = UIImage(named: name, in: bundle, compatibleWith: nil) else { fatalError("Unable to load image named \(name).") } return image } } private final class BundleToken {}
Vous pouvez maintenant remplacer toutes les utilisations des chaînes de localisation et d'initialisation d'images de la forme UIImage(named: "")
par ce que nous avons généré. Cela nous facilitera le suivi des modifications ou la suppression des clés de chaîne de localisation. Dans tous ces cas, le projet ne sera tout simplement pas assemblé tant que toutes les erreurs associées aux modifications n'auront pas été corrigées.
Après les modifications, notre code ressemble à ceci:
let logos = Image.Logos.self let localization = Localization.self private func setupWithLanguage(_ language: ProgrammingLanguage) { switch language { case .Swift: logoImageView.image = logos.swift nameLabel.text = localization.Languages.Swift.name descriptionLabel.text = localization.Languages.Swift.description wikiUrl = localization.Languages.Swift.link.toURL() case .ObjectiveC: logoImageView.image = logos.objectiveC nameLabel.text = localization.Languages.ObjectiveC.name descriptionLabel.text = localization.Languages.ObjectiveC.description wikiUrl = localization.Languages.ObjectiveC.link.toURL() } }
Configuration d'un projet dans Xcode
Il y a un problème avec les fichiers générés: ils peuvent être modifiés manuellement par erreur, et comme ils sont écrasés à partir de zéro à chaque compilation, ces modifications peuvent être perdues. Pour éviter cela, vous pouvez verrouiller les fichiers pour l'écriture après avoir SwiftGen
script SwiftGen
.
Ceci peut être réalisé en utilisant la chmod
. Nous réécrivons notre Build Phase
avec le lancement de SwiftGen comme suit:
PODSROOT="$1" OUTPUT_FILES=() COUNTER=0 while [ $COUNTER -lt ${SCRIPT_OUTPUT_FILE_COUNT} ]; do tmp="SCRIPT_OUTPUT_FILE_$COUNTER" OUTPUT_FILES+=(${!tmp}) COUNTER=$[$COUNTER+1] done for file in "${OUTPUT_FILES[@]}" do if [ -f $file ] then chmod a=rw "$file" fi done $PODSROOT/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml for file in "${OUTPUT_FILES[@]}" do chmod a=r "$file" done
Le script est assez simple. Avant de démarrer la génération, si les fichiers existent, nous leur accordons des autorisations d'écriture. Après avoir exécuté le script, nous bloquons la possibilité de modifier des fichiers.
Pour faciliter l'édition et la vérification du script pour révision, il est pratique de le placer dans un fichier runswiftgen.sh
distinct. La version finale du script avec des modifications mineures peut être trouvée ici . Maintenant, notre Build Phase
ressemblera à ceci: nous transmettons le chemin d'accès au dossier Pods à l'entrée de script:
"$SRCROOT"/SwiftGen/runswiftgen.sh "$PODS_ROOT"
Nous reconstruisons le projet, et maintenant lorsque vous essayez de modifier manuellement le fichier généré, un avertissement apparaît:

Ainsi, le dossier avec Swiftgen contient maintenant un fichier de configuration, un script pour verrouiller les fichiers et lancer Swiftgen
et un dossier avec des modèles personnalisés. Il est pratique de l'ajouter au projet pour une édition ultérieure si nécessaire.

Et comme les Image.swift
Localization.swift
et Image.swift
générés automatiquement, vous pouvez les ajouter à .gitignore afin de ne pas avoir à résoudre les conflits après la git merge
.
Je voudrais également attirer l'attention sur la nécessité de spécifier des fichiers d'entrée / sortie pour la construction de la phase SwiftGen si votre projet utilise le nouveau système de construction Xcode (c'est le système de construction par défaut avec Xcode 10). Dans la liste Fichiers d'entrée, vous devez spécifier tous les fichiers sur la base desquels le code est généré. Dans notre cas, il s'agit du script lui-même, de la configuration, des modèles, des fichiers .strings et .xcassets:
$(SRCROOT)/SwiftGen/runswiftgen.sh $(SRCROOT)/SwiftGen/swiftgen.yml $(SRCROOT)/SwiftGen/Templates/ImageAssets.stencil $(SRCROOT)/SwiftGen/Templates/LocalizableStrings.stencil $(SRCROOT)/SwiftGenExample/en.lproj/Localizable.strings $(SRCROOT)/SwiftGenExample/Assets.xcassets
Dans les fichiers de sortie, nous mettons les fichiers dans lesquels SwiftGen génère le code:
$(SRCROOT)/SwiftGenExample/Image.swift $(SRCROOT)/SwiftGenExample/Localization.swift
L'indication de ces fichiers est nécessaire pour que le système de génération puisse décider d'exécuter le script, selon la présence ou l'absence de modifications dans les fichiers, et à quel moment de l'assemblage du projet ce script doit être exécuté.
Résumé
SwiftGen est un bon outil pour se protéger contre notre négligence lorsque nous travaillons avec des ressources de projet. Avec lui, nous avons pu générer automatiquement du code pour accéder aux ressources de l'application et déplacer une partie du travail sur la vérification de la pertinence des ressources pour le compilateur, ce qui signifie simplifier un peu notre travail. De plus, nous avons mis en place le projet Xcode pour que le travail ultérieur avec l'outil soit plus pratique.
Avantages:
- Contrôle plus facile des ressources du projet.
- Les erreurs de frappe sont réduites, il devient possible d'utiliser l'auto-substitution.
- Les erreurs sont vérifiées au stade de la compilation.
Inconvénients:
- Aucun support pour Localizable.stringsdict.
- Les ressources non utilisées ne sont pas prises en compte.
Un exemple complet peut être vu sur github