La nouvelle entreprise mobile iOS. Partie # 1: Génération de code pour les ressources

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.


Bug de localisation

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:


  • L'application se bloque car vous avez incorrectement spécifié le nom de l'image et forcé le déballage
    UIImage(named: "NotExist")! 
  • L'application se bloque si le storyboard n'est pas ajouté à la cible
  • L'application se bloque si vous avez créé un contrôleur à partir d'un storyboard avec un ID inexistant
  • L'application se bloque si vous avez créé un contrôleur à partir du storyboard avec un ID existant, mais casté dans la mauvaise classe
  • Comportement imprévisible si vous utilisez une police dans du code qui n'est pas ajouté à info.plist, ou si le fichier de police n'est pas marqué avec target: le crash est possible, ou il est possible d'obtenir simplement une police standard au lieu de celle dont vous avez besoin. Développeur Apple: Polices personnalisées , Stackoverflow: plantage
  • L'application se bloque si les storyboards indiquent au contrôleur une classe qui n'existe pas
  • Un tas de code monotone qui crée des icônes, des polices, des contrôleurs, des vues
  • Il n'y a pas d'images, d'icônes dans l'exécution, bien que le nom de l'image soit dans le storyboard, mais pas dans les éléments
  • Le storyboard utilise une police qui n'est pas dans info.plist
  • Les ID de ligne apparaissent dans l'application, au lieu d'être localisés dans des endroits inattendus, en raison de la suppression de lignes dans Localizable.strings (pensant qu'elles n'étaient pas utilisées)
  • Quelque chose d'autre que j'ai oublié de mentionner, ou que nous n'avons pas encore rencontré.

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.


Avertissement

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!


Tais-toi et prends mon argent!

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
    La magie

     pre_install do |installer| installer.pod_targets.flat_map do |pod_target| if pod_target.pod_target_srcroot.include? 'LocalPods' #           LocalPods,     ,   pod_target_srcroot = pod_target.pod_target_srcroot #   pod_target_path = pod_target_srcroot.sub('${PODS_ROOT}/..', '.') #       pod_target_sources_path = pod_target_path + '/' + pod_target.name + '/Sources' #     Sources generated_file_path = pod_target_sources_path + '/R.generated.swift' #     R.generated.swift File.new(generated_file_path, 'w') #    R.generated.swift      end end end 



  • 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| # ... generated_file_path = "News/Sources/R.generated.swift" s.prepare_command = <<-CMD touch "#{generated_file_path}" CMD # ... end 

Vient ensuite une autre "feinte avec des oreilles" - nous devons appeler le script R.swift pour les foyers locaux.


 Pod::Spec.new do |s| # ... s.dependency 'R.swift' r_swift_script = '"${PODS_ROOT}/R.swift/rswift" generate "${PODS_TARGET_SRCROOT}/News/Sources"' s.script_phases = [ { :name => 'R.swift', :script => r_swift_script, :execution_position => :before_compile } ] end 

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| # map development pods installer.development_pod_targets.each do |target| # get only main spec and exclude subspecs spec = target.non_test_specs.first # get full podspec file path podspec_file_path = spec.defined_in_file # get podspec dir path pod_directory = podspec_file_path.parent # check if path contains local pods directory # exclude development but non local pods local_pods_directory_name = "LocalPods" if pod_directory.to_s.include? local_pods_directory_name # go to pod root directorty and run prepare command in sub-shell system("cd \"#{pod_directory}\"; #{spec.prepare_command}") end end end 

Et le même script qui ne fonctionnait pas pour les foyers locaux


 Pod::Spec.new do |s| # ... s.dependency 'R.swift' generated_file_path = "News/Sources/R.generated.swift" s.prepare_command = <<-CMD touch "#{generated_file_path}" CMD r_swift_script = '"${PODS_ROOT}/R.swift/rswift" generate "${PODS_TARGET_SRCROOT}/News/Sources"' s.script_phases = [ { :name => 'R.swift', :script => r_swift_script, :execution_position => :before_compile } ] end 

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.

Source: https://habr.com/ru/post/fr431148/


All Articles