Bonjour à tous!
Je m'appelle Dmitry. Il se trouve que je suis chef d'équipe dans une équipe de 13 développeurs iOS depuis deux ans. Et ensemble, nous travaillons sur l'application Tinkoff Business .
Je veux partager avec vous notre expérience sur la façon de publier l'application à un moment inattendu avec le maximum de fonctionnalités ou de corrections de bugs et toujours pas gris.
Je vais vous parler des pratiques et des approches qui ont aidé l'équipe à accélérer considérablement le développement et les tests et à réduire considérablement la quantité de stress, de bogues, de problèmes liés à une publication imprévue ou urgente. #MakeReleaseWithoutStress .
C'est parti!
Description du problème
Imaginez la situation suivante.
Il existe une autre version. Il a été précédé par des tests de régression, les testeurs ont à nouveau trouvé un endroit où, au lieu du texte dans l'application, affiche l'ID de ligne.

C'était l'un de nos problèmes les plus fréquents.
Vous ne pouvez pas rencontrer ce problème si vous n'avez pas l'application localisée dans une autre langue ou si toute la localisation est écrite en lignes directement dans le code sans utiliser le fichier Localizable.strings.
Mais vous pouvez rencontrer d'autres problèmes que nous vous aiderons à résoudre:
Raison → Effet
Pourquoi tout cela se produit-il?
Il y a du code de programme qui se compile. Si vous avez écrit quelque chose de mal (syntaxiquement ou un nom de fonction incorrect lors de l'appel), votre projet ne sera tout simplement pas assemblé. C'est compréhensible, évident et logique.
Mais qu'en est-il des choses comme les ressources?
Ils ne sont pas compilés, ils sont simplement ajoutés au bundle après la compilation du code. À cet égard, un grand nombre de problèmes peuvent survenir lors de l'exécution, par exemple, le cas décrit ci-dessus - avec des chaînes de localisation.
Rechercher une solution
Nous avons réfléchi à la manière dont ces problèmes sont résolus en général et à la manière de les résoudre. J'ai rappelé l' une des conférences Cocoaheads sur mail.ru. Il a été question de comparer les outils de génération de code.
Après avoir revu une fois de plus ce que sont ces outils (bibliothèques / frameworks), nous avons enfin trouvé ce qu'il fallait.
Dans le même temps, une approche similaire est utilisée par les développeurs pour Android depuis des années. Google y a pensé et en a fait un tel outil hors de la boîte. Mais Apple, même Xcode stable, ne peut pas nous faire ...
Il ne restait plus qu'à savoir quel instrument choisir: Natalie , SwiftGen ou R.swift ?
Natalie n'avait pas de support de localisation, il a été décidé de l'abandonner immédiatement. SwiftGen et R.swift avaient des capacités très similaires. Nous avons opté pour R.swift, simplement en fonction du nombre d'étoiles, sachant qu'à tout moment nous pouvons passer à SwiftGen.
Comment fonctionne R.swift
Un script de phase de compilation pré-compilé est lancé, il parcourt la structure du projet et génère un fichier appelé R.generated.swift
, qui devra être ajouté au projet (nous vous en dirons plus sur la façon de le faire à la toute fin).
Le fichier a la structure suivante:
import Foundation import Rswift import UIKit /// This `R` struct is generated and contains references to static resources. struct R: Rswift.Validatable { fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap(Locale.init) ?? Locale.current fileprivate static let hostingBundle = Bundle(for: R.Class.self) static func validate() throws { try intern.validate() } // ... /// This `R.string` struct is generated, and contains static references to 2 localization tables. struct string { /// This `R.string.localizable` struct is generated, and contains static references to 1196 localization keys. struct localizable { /// en translation: Apple Pay /// /// Locales: en, ru static let card_actions_activate_apple_pay = Rswift.StringResource(key: "card_actions_activate_apple_pay", tableName: "Localizable", bundle: R.hostingBundle, locales: ["en", "ru"], comment: nil) // ... /// en translation: Apple Pay /// /// Locales: en, ru static func card_actions_activate_apple_pay(_: Void = ()) -> String { return NSLocalizedString("card_actions_activate_apple_pay", bundle: R.hostingBundle, comment: "") } } } }
Utilisation:
let str = R.string.localizable.card_actions_activate_apple_pay() print(str) > Apple Pay
"Pourquoi Rswift.StringResource
- Rswift.StringResource
besoin de Rswift.StringResource
?", Demandez-vous. Je ne comprends pas moi-même pourquoi le générer, mais, comme l'expliquent les auteurs, il est nécessaire pour ce qui suit: lien .
Application réelle
Une petite explication du contenu ci-dessous:
* C'était - ils ont utilisé l'approche pendant un certain temps, à la fin, ils l'ont laissée
* Il est devenu - l'approche que nous utilisons lors de l'écriture de nouveau code
* Ce n'était pas le cas, mais vous pouvez l'avoir - une approche qui n'a jamais existé dans notre application, mais je l'ai rencontrée dans divers projets, à l'époque, quand je ne travaillais pas encore chez Tinkoff.ru.
Localisation
Nous avons commencé à utiliser R.swift
pour la localisation, cela nous a sauvé des problèmes que nous avons écrits au tout début. Maintenant, si l'ID de la localisation a changé, le projet ne sera pas assemblé.
* Cela ne fonctionne que si vous changez l'identifiant de toutes les localisations en un autre. Si une chaîne reste dans l'une des localisations, lors de la compilation, il y aura un avertissement que cet identifiant n'est pas localisé dans toutes les langues.

Pas là, mais vous pourriez avoir: final class NewsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() titleLabel.text = NSLocalizedString("news_title", comment: "News title") } }
C'était: extension String { public func localized(in bundle: Bundle = .main, value: String = "", comment: String = "") -> String { return NSLocalizedString(self, tableName: nil, bundle: bundle, value: value, comment: comment) } } final class NewsViewController: UIViewController { private enum Localized { static let newsTitle = "news_title".localized() } override func viewDidLoad() { super.viewDidLoad() titleLabel.text = Localized.newsTitle } }
C'est devenu: titleLabel.text = R.string.localizable.newsTitle()
Les images
Maintenant, si nous avons renommé quelque chose dans * .xcassets et que nous n'avons pas changé le code, le projet ne sera tout simplement pas assemblé.
C'était: imageView.image = UIImage(named: "NotExist") // imageView.image = UIImage(named: "NotExist")! // crash imageView.image = #imageLiteral(resourceName: "NotExist") // crash
C'est devenu: imageView.image = R.image.tinkoffLogo() //
Storyboards
C'était: let someStoryboardName = "SomeStoryboard" // Change to something else (eg: "somestoryboard") - get nil or crash in else let someVCIdentifier = "SomeViewController" // Change to something else (eg: "someviewcontroller") - get nil or crash in else let storyboard = UIStoryboard(name: someStoryboardName, bundle: .main) let _vc = storyboard.instantiateViewController(withIdentifier: someVCIdentifier) guard let vc = _vc as? SomeViewController else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯}
C'est devenu: guard let vc = R.storyboard.someStoryboard.someViewController() else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯ }
Et ainsi de suite.
Storyboard de validation
R.validate () est un merveilleux outil qui serre la main (ou plutôt, jette simplement une erreur dans un bloc catch) si vous avez fait quelque chose de mal dans le storyboard ou les fichiers xib.
Par exemple:
- Indiqué le nom de l'image, qui n'est pas dans le projet
- Ils ont indiqué la police, puis ont cessé de l'utiliser et l'ont supprimée du projet (de info.plist)
Utilisation:
final class AppDelegate: UIResponder { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { #if DEBUG do { try R.validate() } catch { // fatalError // debug // - , production fatalError(error.localizedDescription) } #endif return true } }
Et maintenant, vous êtes prêt à en acheter deux!

Comment l'implémenter?
* Système basé sur les composants - un wiki , un concept de développement de code dans lequel des composants (un ensemble d'écrans / modules interconnectés) sont développés dans un environnement fermé (dans notre cas, dans des pods locaux) afin de réduire la cohérence de la base de code. Beaucoup de gens connaissent l'approche du backend, qui est basée sur ce concept - les microservices.
* Monolith - wiki , le concept de développement de code, dans lequel la base de code entière se trouve dans un référentiel, et le code est étroitement lié. Ce concept convient aux petits projets avec un ensemble fini de fonctions.
Si vous développez une application monolithique ou n'utilisez que des dépendances tierces, alors vous avez de la chance (mais ce n'est pas exact). Suivez le tutoriel et faites tout strictement dessus.
Ce n'était pas notre cas. Nous nous sommes impliqués. Puisque nous utilisons un système basé sur des composants,
intégrer R.swift dans l'application principale, nous avons décidé de l'intégrer dans des pods locaux (qui sont des composants).
En raison de la mise à jour constante des localisations, des images et de tous les éléments qui affectent le fichier R.generated.swift, il existe de nombreux conflits dans le fichier généré lors de la fusion avec la branche commune. Et pour éviter cela, vous devez supprimer R.generated.swift du référentiel git. L'auteur recommande également de le faire .
Ajoutez les lignes suivantes à .gitignore
.
# R.Swift generated files *.generated.swift
De plus, si vous ne souhaitez pas générer de code pour certaines ressources, vous pouvez toujours utiliser en ignorant des fichiers individuels ou des dossiers entiers:
"${PODS_ROOT}/R.swift/rswift" generate "${SRCROOT}/Example" "--rswiftignore" "Example/.rswiftignore"
description de .rswiftignore
Comme dans le projet principal, il était important pour nous de ne pas ajouter de fichiers R.generated.swift de pods locaux au référentiel git. Nous avons commencé à envisager des options sur la façon dont cela pourrait être fait:
- alias sur R.generated.swift pour que le fichier (alias, par exemple: R.swift) soit ajouté au projet, puis, lors de la compilation par référence, le fichier réel est disponible. Mais les cocoapodes sont intelligents et ne sont pas autorisés à le faire
- dans podspec dans la phase de pré-compilation, ajoutez le fichier R.generated.swift au projet lui-même à l'aide de scripts, mais il sera ajouté simplement en tant que fichier dans le système de fichiers et le fichier n'apparaîtra pas dans le projet
- d'autres options plus ou moins soignées
magie dans podfile
La magie
pre_install do |installer| installer.pod_targets.flat_map do |pod_target| if pod_target.pod_target_srcroot.include? 'LocalPods'
- et une autre option ... ajoutez toujours R.generated.swift à git
Nous nous sommes temporairement installés sur l'option: "la magie dans le Podfile", malgré le fait qu'il avait un certain nombre de défauts:
- Il ne pouvait être lancé qu'à partir de la racine du projet (bien que les cocoapods puissent être lancés à partir de presque n'importe quel dossier du projet)
- Tous les pods doivent avoir un dossier appelé Sources (bien que cela ne soit pas critique si les pods sont en ordre)
- Il était étrange et incompréhensible, mais tôt ou tard il devrait supporter (c'est toujours une béquille)
- Si une bibliothèque tierce se trouve dans un dossier avec "LocalPods" sur son chemin, alors elle essaiera d'y ajouter le fichier R.generated.swift ou elle plantera
prepare_command
Vivant quelque temps avec un scénario et des souffrances, j'ai décidé d'étudier ce sujet plus largement et j'ai trouvé une autre option.
Dans Podspec, il y a prepare_command , qui est uniquement destiné à créer et à modifier les sources, qui seront ensuite ajoutées au projet.
* News - le nom du pod, qui doit être remplacé par le nom de votre pod local
* touch - commande pour créer un fichier. L'argument est le chemin d'accès relatif au fichier (y compris le nom du fichier avec l'extension)
Ensuite, nous ferons des fraudes avec News.podspec
Ce script est appelé la première fois que l' pod install
exécutée et ajoute le fichier dont nous avons besoin au dossier source dans l'âtre.
Pod::Spec.new do |s|
Vient ensuite une autre "feinte avec des oreilles" - nous devons appeler le script R.swift pour les foyers locaux.
Pod::Spec.new do |s|
Certes, il y a un «mais». prepare_command
ne fonctionne pas avec les pods locaux, ou plutôt cela fonctionne, mais dans certains cas spéciaux. Il y a une discussion sur ce sujet sur Github .
Fatalité
* Fatality - wiki , le dernier succès de Mortal Kombat.
Après un peu plus de recherches, j'ai trouvé une autre solution - un hybride d'approches c prepare_command
et pre_install
.
Une petite modification de la magie du Podfile:
pre_install do |installer|
Et le même script qui ne fonctionnait pas pour les foyers locaux
Pod::Spec.new do |s|
En fin de compte, cela fonctionne comme prévu.
Enfin!
PS:
J'ai essayé de faire une autre commande personnalisée au lieu de prepare_command
, mais pod lib lint
(une commande pour valider le contenu de podspec et le foyer lui-même) jure sur des variables supplémentaires et ne passe pas.
Foyers non locaux
Dans les pods distants (ceux qui sont chacun dans leur propre référentiel), vous n'avez pas besoin de toute cette magie de script, décrite ci-dessus, car la base de code est strictement liée à la version de dépendance.
Il suffit simplement d'incorporer l'exemple (un projet généré après la commande pod lib create <Name>) R.swift lui-même et d'ajouter R.generated.swift au package de la bibliothèque (en bas). Si le projet n'a pas d'exemple, vous devrez écrire des scripts qui seront similaires à ceux que j'ai cités.
PS:
Il y a une petite clarification:
R.swift + Xcode 10 + nouveau système de construction + construction incrémentielle! = <3
Plus d'informations sur le problème sur la page principale de la bibliothèque ou ici
R.swift v4.0.0 ne fonctionne pas avec les cocoapods 1.6.0 :(
Je pense que bientôt tous les problèmes seront corrigés.
Conclusion
Vous devez toujours maintenir la barre de qualité aussi élevée que possible. Ceci est particulièrement important pour les applications qui fonctionnent avec la finance.
Dans ce cas, vous n'avez pas besoin de surcharger les tests et de trouver les bogues le plus tôt possible. Dans notre cas, c'est soit au moment où le développeur a compilé le code, soit lors du test pour les requêtes de tirage. Ainsi, nous constatons le manque de localisation non pas par le regard attentif des testeurs ou par des tests automatisés, mais par le processus habituel de construction de l'application.
Vous devez également tenir compte du fait qu'il s'agit d'un outil tiers lié à la structure du projet et analysant son contenu. Si la structure du fichier de projet change, l'outil devra être changé.
Nous avons pris ce risque et, dans ce cas, nous sommes toujours prêts à remplacer cet outil par un autre ou à écrire le vôtre.
Et le gain de R.swift est un nombre énorme d'heures de travail que l'équipe peut consacrer à des choses beaucoup plus importantes: nouvelles fonctionnalités, nouvelles solutions techniques, amélioration de la qualité, etc. R.swift a entièrement restitué le temps consacré à son intégration, même en tenant compte du remplacement éventuel de celui-ci à l'avenir par une autre solution similaire.
R.swift
Bonus
Vous pouvez jouer avec un exemple pour voir immédiatement de vos propres yeux le profit de la génération de code pour les ressources. Le code source du projet "à jouer": GitHub .
Merci beaucoup d'avoir lu l'article ou simplement de feuilleter cet endroit, je suis content en tout cas)
C'est tout.