Probablemente, en cada gran proyecto de iOS, el de larga duración puede tropezar con iconos que no se utilizan en ningún lugar, o acceder a teclas de localización que no han existido durante mucho tiempo. Muy a menudo, tales situaciones surgen de la falta de atención, y la automatización es la mejor cura para la falta de atención.
En el equipo iOS de HeadHunter, prestamos gran atención a la automatización de las tareas de rutina que un desarrollador puede encontrar. Con este artículo queremos comenzar un ciclo de historias sobre esas herramientas y enfoques que simplifican nuestro trabajo diario.
Hace algún tiempo, logramos tomar el control de los recursos de la aplicación utilizando la utilidad SwiftGen. Sobre cómo configurarlo, cómo vivir con él y cómo esta utilidad ayuda a cambiar la verificación de la relevancia de los recursos para el compilador, y hablaremos sobre cat.

SwiftGen es una utilidad que le permite generar código Swift para acceder a varios recursos de un proyecto Xcode, entre ellos:
- fuentes
- colores
- guiones gráficos;
- cadenas de localización;
- activos.
Todos podrían escribir un código similar para inicializar imágenes o cadenas de localización:
logoImageView.image = UIImage(named: "Swift") nameLabel.text = String( format: NSLocalizedString("languages.swift.name", comment: ""), locale: Locale.current )
Para indicar el nombre de la imagen o la clave de localización, usamos cadenas literales. Lo que está escrito entre comillas dobles no está validado por el compilador o el entorno de desarrollo (Xcode). Ahí radica el siguiente conjunto de problemas:
- Puedes hacer un error tipográfico;
- Es posible que olvide actualizar el uso del código después de editar o eliminar la clave / imagen.
Veamos cómo podemos mejorar este código con SwiftGen.
Para nuestro equipo, la generación solo era relevante para cadenas y activos, y serán discutidos en el artículo. La generación de otros tipos de recursos es similar y, si se desea, se domina fácilmente de forma independiente.
Implementación de proyecto
Primero necesitas instalar SwiftGen. Elegimos instalarlo a través de CocoaPods como una forma conveniente de distribuir la utilidad entre todos los miembros del equipo. Pero esto se puede hacer de otras maneras, que se describen en detalle en la documentación . En nuestro caso, todo lo que hay que hacer es agregar el pod 'SwiftGen'
, y luego agregar una nueva fase de Build Phase
( Build Phase
) que lanzará SwiftGen antes de construir el proyecto.
"$PODS_ROOT"/SwiftGen/bin/swiftgen
Es importante ejecutar SwiftGen antes de comenzar la fase de Compile Sources
para evitar errores al compilar el proyecto.

Ahora estamos listos para adaptar SwiftGen a nuestro proyecto.
Configurar SwiftGen
En primer lugar, debe configurar las plantillas mediante las cuales se generará el código para acceder a los recursos. La utilidad ya contiene un conjunto de plantillas para generar código, todas se pueden ver en el github y, en principio, están listas para usar. Las plantillas están escritas en el lenguaje Stencil , puede estar familiarizado con ellas si usó Sourcery o jugó con Kitura . Si lo desea, cada una de las plantillas se puede adaptar a sus propias guías.
Por ejemplo, tome la plantilla que enum
genera para acceder a las cadenas de localización. Nos pareció que en el estándar hay demasiado superfluo y puede simplificarse. Un ejemplo simplificado con comentarios explicativos está debajo del spoiler.
Ejemplo de plantilla {# #} {% 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 ) } }
Es conveniente guardar el archivo de plantilla en la raíz del proyecto, por ejemplo, en la carpeta SwiftGen/Templates
para que esta plantilla esté disponible para todos los que trabajan en el proyecto.
La utilidad admite la configuración mediante el archivo YAML swiftgen.yml
, en el que puede especificar las rutas a los archivos de origen, plantillas y parámetros adicionales. Swiftgen
en la raíz del proyecto en la carpeta Swiftgen
y luego Swiftgen
otros archivos relacionados con el script en la misma carpeta.
Para nuestro proyecto, este archivo puede verse así:
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
De hecho, las rutas a los archivos y las plantillas se indican allí, así como los parámetros adicionales que se pasan al contexto de la plantilla.
Como el archivo no está en la raíz del proyecto, debemos especificar la ruta al iniciar Swiftgen. Cambia nuestro script de inicio:
"$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml
Ahora nuestro proyecto puede ser ensamblado. Después del ensamblaje, deben aparecer dos archivos Localization.swift
e Image.swift
en la carpeta del proyecto a lo largo de las rutas especificadas en Image.swift
. Deben agregarse al proyecto Xcode. En nuestro caso, los archivos generados contienen lo siguiente:
Para cuerdas: public enum Localization { public enum Languages { public enum ObjectiveC {
Para imágenes: 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 {}
Ahora puede reemplazar todos los usos de las cadenas de localización e inicialización de imágenes del formulario UIImage(named: "")
con lo que generamos. Esto nos facilitará el seguimiento de los cambios o la eliminación de las claves de cadena de localización. En cualquiera de estos casos, el proyecto simplemente no se ensamblará hasta que se hayan solucionado todos los errores asociados con los cambios.
Después de los cambios, nuestro código se ve así:
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() } }
Configurar un proyecto en Xcode
Hay un problema con los archivos generados: pueden cambiarse manualmente por error, y dado que se sobrescriben desde cero con cada compilación, estos cambios pueden perderse. Para evitar esto, puede bloquear archivos para escribir después de SwiftGen
script SwiftGen
.
Esto se puede lograr usando el chmod
. Reescribimos nuestra Build Phase
con el lanzamiento de SwiftGen de la siguiente manera:
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
El guión es bastante simple. Antes de comenzar la generación, si los archivos existen, les damos permisos de escritura. Después de ejecutar el script, bloqueamos la capacidad de modificar archivos.
Para facilitar la edición y verificar el script para su revisión, es conveniente colocarlo en un archivo runswiftgen.sh
separado. La versión final del script con modificaciones menores se puede encontrar aquí . Ahora nuestra Build Phase
se verá así: pasamos la ruta a la carpeta Pods a la entrada del script:
"$SRCROOT"/SwiftGen/runswiftgen.sh "$PODS_ROOT"
Reconstruimos el proyecto y ahora, cuando intenta modificar manualmente el archivo generado, aparece una advertencia:

Por lo tanto, la carpeta con Swiftgen ahora contiene un archivo de configuración, un script para bloquear archivos e iniciar Swiftgen
y una carpeta con plantillas personalizadas. Es conveniente agregarlo al proyecto para su posterior edición si es necesario.

Y dado que los Image.swift
Localization.swift
e Image.swift
generan automáticamente, puede agregarlos a .gitignore para que no tenga que resolver conflictos después de que git merge
nuevamente.
También me gustaría llamar la atención sobre la necesidad de especificar archivos de entrada / salida para la compilación de la fase SwiftGen si su proyecto utiliza el sistema Xcode New Build System (es el sistema de compilación predeterminado con Xcode 10). En la lista Archivos de entrada, debe especificar todos los archivos en función de los cuales se genera el código. En nuestro caso, este es el script en sí, la configuración, las plantillas, las cadenas y los archivos .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
En los archivos de salida colocamos los archivos en los que SwiftGen genera el código:
$(SRCROOT)/SwiftGenExample/Image.swift $(SRCROOT)/SwiftGenExample/Localization.swift
Es necesario indicar estos archivos para que el sistema de compilación pueda decidir si ejecutar el script, dependiendo de la presencia o ausencia de cambios en los archivos, y en qué punto del ensamblaje del proyecto se debe ejecutar este script.
Resumen
SwiftGen es una buena herramienta para protegernos de nuestro descuido cuando trabajamos con recursos del proyecto. Con él, pudimos generar automáticamente código para acceder a los recursos de la aplicación y transferir parte del trabajo para verificar la relevancia de los recursos para el compilador, lo que significa simplificar un poco nuestro trabajo. Además, configuramos el proyecto Xcode para que sea más conveniente seguir trabajando con la herramienta.
Pros:
- Más fácil de controlar los recursos del proyecto.
- Se reducen los errores tipográficos, se hace posible utilizar la sustitución automática.
- Los errores se verifican en la etapa de compilación.
Contras:
- No hay soporte para Localizable.stringsdict.
- Los recursos que no se utilizan no se tienen en cuenta.
El ejemplo completo se puede ver en github