El nuevo iOS Mobile Enterprise. Parte # 1: Generación de código para recursos

Hola a todos!


Me llamo Dmitry Dio la casualidad de que soy un líder de equipo en un equipo de 13 desarrolladores de iOS durante los últimos dos años. Y juntos estamos trabajando en la aplicación Tinkoff Business .


Quiero compartir con ustedes nuestra experiencia sobre cómo lanzar la aplicación en un momento inesperado con el conjunto máximo de características o correcciones de errores y aún no ponerse gris.


Le contaré sobre las prácticas y los enfoques que ayudaron al equipo a acelerar significativamente el desarrollo y las pruebas y reducir significativamente la cantidad de estrés, errores, problemas con una liberación no programada o urgente. #MakeReleaseWithoutStress .


Vamos!


Descripción del problema


Imagina la siguiente situación.


Hay otro lanzamiento. Fue precedido por pruebas de regresión, los evaluadores encontraron nuevamente un lugar donde, en lugar de texto en la aplicación, se muestra la ID de línea.


Error de localización

Este fue uno de nuestros problemas más frecuentes que encontramos.


Es posible que no encuentre este problema si no tiene la aplicación localizada en otro idioma, o si toda la localización se escribe en líneas directamente en el código sin usar el archivo Localizable.strings.


Pero puede encontrar otros problemas que le ayudaremos a resolver:


  • La aplicación se bloquea porque especificó incorrectamente el nombre de la imagen y forzó el desenvolvimiento forzado
    UIImage(named: "NotExist")! 
  • La aplicación se bloquea si el guión gráfico no se agrega al destino
  • La aplicación se bloquea si crea un controlador desde un guión gráfico con una ID inexistente
  • La aplicación se bloquea si crea un controlador desde el guión gráfico con una ID existente, pero se envía a la clase incorrecta
  • Comportamiento impredecible si usa una fuente en el código que no se agrega a info.plist, o si el archivo de fuente no está marcado con target: es posible un bloqueo, o es posible obtener simplemente una fuente estándar en lugar de la que necesita. Desarrollador Apple: Fuentes personalizadas , Stackoverflow: crash
  • La aplicación se bloquea si los guiones gráficos indican al controlador una clase que no existe
  • Un montón de código monótono que crea íconos, fuentes, controladores, vistas
  • No hay imágenes, iconos en tiempo de ejecución, aunque el nombre de la imagen está en el guión gráfico, pero no en los activos
  • El guión gráfico utiliza una fuente que no está en info.plist
  • Las ID de fila aparecen en la aplicación, en lugar de localizar en lugares inesperados, debido a la eliminación de líneas en Localizable.strings (se pensó que no se utilizaron)
  • Algo más que olvidé mencionar, o que aún no hemos encontrado.

Razón → Efecto


¿Por qué está pasando todo esto?


Hay un código de programa que compila. Si escribió algo incorrecto (sintácticamente, o el nombre de la función es incorrecto cuando se lo llama), entonces su proyecto simplemente no se ensamblará. Esto es comprensible, obvio y lógico.


¿Pero qué pasa con cosas como los recursos?


No se compilan, simplemente se agregan al paquete después de compilar el código. A este respecto, pueden ocurrir una gran cantidad de problemas en tiempo de ejecución, por ejemplo, el caso descrito anteriormente, con cadenas en la localización.


Busca una solución


Pensamos en cómo se resuelven tales problemas en general, y cómo podemos solucionarlo. Recordé una de las conferencias de Cocoaheads en mail.ru. Se habló sobre la comparación de herramientas de generación de código.


Después de ver una vez más de qué se tratan estas herramientas (bibliotecas / marcos), finalmente encontramos lo que se necesitaba.


Al mismo tiempo, los desarrolladores de Android han utilizado un enfoque similar durante años. Google pensó en ellos y los convirtió en una herramienta fuera de la caja. Pero Apple, incluso Xcode estable, no puede hacernos ...


Todo lo que quedaba era averiguar qué instrumento elegir: ¿ Natalie , SwiftGen o R.swift ?


Natalie no tenía soporte de localización, se decidió abandonarlo inmediatamente. SwiftGen y R.swift tenían capacidades muy similares. Optamos por R.swift, simplemente en función del número de estrellas, sabiendo que en cualquier momento podemos cambiar a SwiftGen.


Cómo funciona R.swift


Se inicia un script de fase de compilación previa a la compilación, se ejecuta a través de la estructura del proyecto y genera un archivo llamado R.generated.swift , que deberá agregarse al proyecto (le diremos más sobre cómo hacerlo al final).


El archivo tiene la siguiente estructura:


 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: "") } } } } 

Uso:


 let str = R.string.localizable.card_actions_activate_apple_pay() print(str) >  Apple Pay 

“¿Por qué necesitamos Rswift.StringResource ?”, Preguntas. Yo mismo no entiendo por qué generarlo, pero, como explican los autores, es necesario para lo siguiente: enlace .


Aplicación del mundo real


Una pequeña explicación del contenido a continuación:


* Lo fue: usaron el enfoque por un tiempo, al final, lo dejaron
* Se ha convertido en el enfoque que usamos al escribir código nuevo
* No lo era, pero puede tenerlo: un enfoque que nunca existió en nuestra aplicación, pero lo conocí en varios proyectos, en aquellos tiempos lejanos, cuando aún no había trabajado en Tinkoff.ru.


Localización


Comenzamos a usar R.swift para la localización, nos salvó de los problemas sobre los que escribimos al principio. Ahora, si la identificación en la localización ha cambiado, el proyecto no se ensamblará.


* Esto solo funciona si cambia la identificación en todas las localizaciones a otra. Si una cadena permanece en cualquiera de las localizaciones, en la compilación habrá una advertencia de que esta identificación no está localizada en todos los idiomas.


Advertencia

No está allí, pero es posible que tenga:
 final class NewsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() titleLabel.text = NSLocalizedString("news_title", comment: "News title") } } 

Fue:
 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 } } 

Se convirtió en:
 titleLabel.text = R.string.localizable.newsTitle() 

Imágenes


Ahora, si cambiamos el nombre de algo en * .xcassets y no cambiamos el código, entonces el proyecto simplemente no se ensamblará.


Fue:
 imageView.image = UIImage(named: "NotExist") //     imageView.image = UIImage(named: "NotExist")! // crash imageView.image = #imageLiteral(resourceName: "NotExist") // crash 

Se convirtió en:
 imageView.image = R.image.tinkoffLogo() //     

Storyboards


Fue:
 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() ¯\_(ツ)_/¯} 

Se convirtió en:
 guard let vc = R.storyboard.someStoryboard.someViewController() else { //    -  ,  Fabric  Firebase //    fatalError() ¯\_(ツ)_/¯ } 

Y así sucesivamente.


Validación Storyboard


R.validate () es una herramienta maravillosa que se da la mano (o más bien, simplemente arroja un error en un bloque catch) si hiciste algo mal en el guión gráfico o en los archivos xib.
Por ejemplo:


  • Indica el nombre de la imagen, que no está en el proyecto.
  • Indicaron la fuente y luego dejaron de usarla y la eliminaron del proyecto (de info.plist)

Uso:


 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 } } 

¡Y ahora estás listo para comprar dos!


¡Cállate y toma mi dinero!

¿Cómo implementar?


* Sistema basado en componentes: un wiki , un concepto de desarrollo de código en el que los componentes (un conjunto de pantallas / módulos interconectados) se desarrollan en un entorno cerrado (en nuestro caso, en módulos locales) para reducir la coherencia de la base del código. Muchas personas conocen el enfoque en el backend, que se basa en este concepto: microservicios.


* Monolith - wiki , el concepto de desarrollo de código, en el que toda la base de código se encuentra en un repositorio, y el código está estrechamente relacionado. Este concepto es adecuado para pequeños proyectos con un conjunto finito de funciones.


Si está desarrollando una aplicación monolítica o utilizando dependencias de terceros, entonces tiene suerte (pero esto no es exacto). Tome el tutorial y haga todo estrictamente al respecto.


Este no fue nuestro caso. Nos involucramos Como utilizamos un sistema basado en componentes, incorporar R.swift en la aplicación principal, decidimos incorporarlo en módulos locales (que son componentes).


Debido a la actualización constante de localizaciones, imágenes y todos los elementos que afectan el archivo R.generated.swift, existen muchos conflictos en el archivo generado cuando se fusionan con la rama común. Y para evitar esto, debe eliminar R.generated.swift del repositorio de git. El autor también recomienda hacer esto .


Agregue las siguientes líneas a .gitignore .


 # R.Swift generated files *.generated.swift 

Además, si no desea generar código para algunos recursos, siempre puede usar ignorando archivos individuales o carpetas completas:


 "${PODS_ROOT}/R.swift/rswift" generate "${SRCROOT}/Example" "--rswiftignore" "Example/.rswiftignore" 

descripción de .rswiftignore


Al igual que en el proyecto principal, era importante para nosotros no agregar archivos R.generated.swift de pods locales al repositorio git. Comenzamos a considerar opciones sobre cómo podría hacerse esto:


  • alias en R.generated.swift para que el archivo (alias, por ejemplo: R.swift) se agregue al proyecto y luego, al compilar por referencia, el archivo real esté disponible. Pero los cocoapodos son inteligentes, y no se les permite hacerlo
  • en podspec en la fase de precompilación, agregue el archivo R.generated.swift al proyecto mismo usando scripts, pero luego se agregará simplemente como un archivo en el sistema de archivos, y el archivo no aparecerá en el proyecto
  • otras opciones más o menos ordenadas
  • magia en podfile


    Magia
    Magia

     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 



  • y otra opción ... todavía agregue R.generated.swift a git

Nos decidimos temporalmente por la opción: "magia en el Podfile", a pesar de que tenía una serie de deficiencias:


  • Solo se pudo iniciar desde la raíz del proyecto (aunque los cocoapods se pueden iniciar desde casi cualquier carpeta del proyecto)
  • Todos los pods deben tener una carpeta llamada Fuentes (aunque esto no es crítico si los pods están en orden)
  • Era extraño e incomprensible, pero tarde o temprano tendría que apoyar (esto todavía es una muleta)
  • Si alguna biblioteca de terceros está en una carpeta con "LocalPods" en su ruta, entonces intentará agregar el archivo R.generated.swift allí o se bloqueará con un error

prepare_command


Viviendo un tiempo con un guión y sufriendo, decidí estudiar este tema más ampliamente y encontré otra opción.
En Podspec hay prepare_command , que está destinado solo a crear y modificar las fuentes, que luego se agregarán al proyecto.


* Noticias: el nombre del pod, que debe reemplazarse con el nombre de su pod local
* touch - comando para crear un archivo. El argumento es la ruta relativa al archivo (incluido el nombre del archivo con la extensión)


A continuación haremos fraudes con News.podspec


Este script se llama la primera vez que pod install ejecuta la pod install y agrega el archivo que necesitamos a la carpeta de origen en el hogar.


 Pod::Spec.new do |s| # ... generated_file_path = "News/Sources/R.generated.swift" s.prepare_command = <<-CMD touch "#{generated_file_path}" CMD # ... end 

El siguiente es otro "finta con orejas": tenemos que hacer una llamada al guión R.swift para hogares locales.


 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 

Es cierto, hay un "pero". prepare_command no funciona con pods locales, o más bien funciona, pero en algunos casos especiales. Hay una discusión sobre este tema en Github .


Fatalidad


* Fatality - wiki , el golpe final en Mortal Kombat.


Después de un poco más de investigación, encontré otra solución: un híbrido de enfoques c prepare_command y pre_install .


Una pequeña modificación de la magia del 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 

Y el mismo script que no se ejecutó para hogares locales


 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 

Al final, funciona como esperamos.


Por fin!


PD:


Traté de hacer otro comando personalizado en lugar de prepare_command , pero pod lib lint (un comando para validar el contenido de podspec y el propio hogar) jura sobre variables adicionales y no pasa.


Hogares no locales


En los pods remotos (aquellos que están en su propio repositorio) no necesita toda esta magia de scripting, que se describe anteriormente, porque allí la base del código está estrictamente vinculada a la versión de dependencia.


Es suficiente simplemente incrustar el Ejemplo (un proyecto generado después de que el pod lib cree el comando <Nombre>) el script R.swift y agregar R.generated.swift al paquete de la biblioteca (abajo). Si el proyecto no tiene Ejemplo, entonces tendrá que escribir scripts que serán similares a los que he citado.


PD:


Hay una pequeña aclaración:
R.swift + Xcode 10 + nuevo sistema de compilación + compilación incremental! = <3
Más información sobre el problema en la página principal de la biblioteca o aquí.
R.swift v4.0.0 no funciona con cocoapods 1.6.0 :(
Creo que pronto todos los problemas serán corregidos.


Conclusión


Siempre debe mantener la barra de calidad lo más alta posible. Esto es especialmente importante para aplicaciones que funcionan con finanzas.


En este caso, no necesita sobrecargar las pruebas y encontrar errores lo antes posible. En nuestro caso, esto ocurre en el momento en que el desarrollador compiló el código o en la ejecución de prueba para Solicitudes de extracción. Por lo tanto, encontramos la falta de localización no por la mirada atenta de los evaluadores o por pruebas automatizadas, sino por el proceso habitual de construcción de la aplicación.


También debe tener en cuenta el hecho de que esta es una herramienta de terceros que está vinculada a la estructura del proyecto y analiza su contenido. Si la estructura del archivo del proyecto cambia, entonces la herramienta deberá cambiarse.
Asumimos este riesgo y, en cuyo caso, siempre estamos listos para cambiar esta herramienta por otra o escribir la suya propia.


Y la ganancia de R.swift es una gran cantidad de horas hombre que el equipo puede dedicar a cosas mucho más importantes: nuevas características, nuevas soluciones técnicas, mejora de la calidad, etc. R.swift devolvió completamente la cantidad de tiempo que se dedicó a su integración, incluso teniendo en cuenta el posible reemplazo en el futuro con otra solución similar.


R.swift


Bono


Puede jugar con un ejemplo para ver de inmediato con sus propios ojos el beneficio de la generación de código para los recursos. El código fuente del proyecto "para jugar": GitHub .


Muchas gracias por leer el artículo o simplemente hojear este lugar, de todos modos estoy satisfecho)


Eso es todo

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


All Articles