Cómo crear tu primer Swift Package Plugin

Los plugins son scripts de Swift que pueden realizar acciones en un paquete Swift o en un proyecto de Xcode.

Estos pueden ser muy útiles cuando queremos compartir herramientas con nuestro equipo, para mejorar el proceso de desarrollo.

En esta oportunidad vamos a ver paso a paso como crear un plugin siguiendo este requerimiento de ejemplo:

El plugin debe tomar los archivos de localizaciones (xxx.strings) de cada paquete y crear en dentro de la carpeta Excludes una versión del archivo en un formato JSON como este:

{
"<Nombre del paquete>" : {
"key-1": "value1",
"key-2": "value2",
...
}
}

¡Manos a la obra!

Conociendo el proyecto de inicio

Primero descarga el proyecto de inicio que se encuentra en este enlace: Paquete Inicial.

Vista de la estructura del paquete

Veamos que contiene el proyecto:

  • El proyecto contiene dos sub-paquetes: MyPackage y SecondPackage.
  • Ambos tienen varios directorios, pero para nuestro ejemplo nos interesan los siguientes:
  • Excludes donde alojaremos nuestro JSON a generar.
  • Resources este directorio aloja los recursos como imágenes y localizaciones.
  • Los demás directorios son solo para ilustrar un poco, no son relevantes para el ejemplo.

Ahora que tenemos el proyecto inicial, creemos el plugin.

Creando un command plugin

Hay dos clases de plugin, Command (comando) y Build Tool (herramienta compilación). En este caso, crearemos uno de tipo comando. Esto nos permitirá ejecutar nuestro plugin usando el menú de contexto de Xcode, haciendo click derecho sobre la carpeta del paquete.

Creando la estructura del plugin

  1. Lo primero que tenemos que hacer es crear una carpeta para alojar el plugin, por convención debemos usar el nombre Plugins para la misma. Esta carpeta debe crearse al mismo nivel que la carpeta Sources que se encuentran en el paquete.

  2. Dentro de la carpeta Plugins creamos la carpeta para el plugin que estaremos trabajando, el nombre será LocalizationsGenerator. Esta carpeta contendrá el código de nuestro plugin.

  3. Dentro de la carpeta LocalizationsGenerator creamos el archivo Plugin.swift.

Vista de la estructura del proyecto luego de agregar las carpetas para el Plugin

Con esos primeros pasos hemos creado todos los archivos y directorios que necesitaremos, es importante que conservemos la jerarquía indicada, porque si no tendremos problemas en los siguientes pasos.

Modificando el Manifiesto

Ahora procedemos a modificar nuestro manifiesto. Abrimos el archivo Package.swift, colocamos una coma luego de la declaración del target, SecondPackage, y escribimos:

.plugin(
name: "LocalizationsGenerator",
capability: .command(
intent: .custom(
verb: "gen-locales",
description: "Genera archivos de localizaciones en formato json"
),
permissions: [
.writeToPackageDirectory(reason: "Necesito permiso para crear los archivos.")
]
)
)

Con esto le indicamos al manifiesto que el proyecto contiene un target de tipo plugin. Para esto indicamos el nombre y en la propiedad capability establecemos que es de tipo command.

Command acepta dos parámetros intent donde indicamos verb, que es la cadena de caracteres con la cual podremos ejecutar este plugin desde la terminal, en este caso, gen-locales, así mismo pasamos la descripción. Por último, indicamos los permisos que necesitaremos en el parámetro permissions, en nuestro caso, necesitaremos permisos de escritura para poder crear los archivos JSON.

Escribiendo el código de nuestro plugin

Ya tenemos todo el proyecto configurado y ahora necesitamos escribir el código que ejecutará nuestro plugin.

Abrimos Plugin.swift y escribimos el siguiente código:

import Foundation
import PackagePlugin
@main
struct LocalizationsGenerator: CommandPlugin {
func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws {
}
}

Creamos un struct llamado LocalizationsGenerator precedido por la notación @main. Esto quiere decir que este código será lo primero a ejecutarse.

Dentro de LocalizationsGenerator conformamos a CommandPlugin, el cual solo tiene un requisito performCommand(context:arguments:), lo que pongamos dentro de este método será la lógica de nuestro plugin.

Copia el siguiente código dentro de performCommand(context:arguments:):

for target in context.package.targets {
guard let target = target as? SourceModuleTarget, target.kind != .test else {
continue
}
for file in target.sourceFiles(withSuffix: ".strings") {
guard
let content = try? String(contentsOfFile: file.path.string)
else {
continue
}
let json = encode(
from: [target.moduleName : convertToDictionary(content)]
)
try? save(content: json, for: target.moduleName)
}
}

En este código estamos iterando sobre todos los targets del proyecto. Luego nos aseguramos de que sean de tipo SourceModuleTarget y que no sean tests. Si estas dos condiciones son correctas, procedemos a leer cada uno de los archivos que tengan la extensión .strings.

Cuando ya tenemos el contenido del archivo, procedemos a llamar el método encode() el cual convierte un diccionario en JSON.

Por último, llamamos el método save(content:for:), el cual creará el archivo Localizations.json dentro de la carpeta Excludes.

Copiamos el siguiente código dentro de LocalizationsGenerator:

func convertToDictionary(_ content: String) -> [String: String] {
var result = [String: String]()
guard let regex = try? NSRegularExpression(pattern: "\"(.*?)\"\\s*=\\s*\"(.*?)\";", options: []) else {
return result
}
regex.enumerateMatches(in: content, options: [], range: NSRange(content.startIndex..., in: content)) { match, _, _ in
guard let match = match else { return }
guard
let keyRange = Range(match.range(at: 1), in: content),
let valueRange = Range(match.range(at: 2), in: content) else {
return
}
result[String(content[keyRange])] = String(content[valueRange])
}
return result
}
func save(content: String, for packageName: String) throws {
try content.write(toFile: "./Sources/\(packageName)/Excludes/Localizations.json", atomically: true, encoding: .utf8)
}
func encode(from dictionary: [String: [String: String]]) -> String {
guard let jsonData = try? JSONSerialization.data(withJSONObject: dictionary, options: .prettyPrinted) else {
return ""
}
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
return ""
}
return jsonString
}

Para evitar extender el artículo no explicaremos estos bloques de código, pero son necesarios para convertir el contenido de los archivos .strings a JSON y guardarlo en la carpeta Excludes.

Usando nuestro plugin

Compila el paquete, luego haz click derecho sobre grupo que contiene el paquete. En la parte inferior podrás dar click sobre el comando que hemos creado LocalizationsGenerator:

Vista de la estructura del proyecto luego de agregar las carpetas para el Plugin

Algunas veces no funciona a la primera y necesitamos cerrar Xcode y abrirlo nuevamente.

Cuando seleccionemos el comando aparecerá el siguiente modal:

Modal para seleccionar targets

Aquí podemos elegir sobre cuales targets queremos ejecutar nuestro plugin. Por ahora solo presionamos Run, para ejecutarlo en todos.

Luego, aparecerá el siguiente mensaje para confirmar que otorgamos al plugin los permisos de escritura.

Mensaje de confirmación

Aquí podemos ver el mensaje que indicamos cuando declaramos el plugin en el manifiesto, "Necesito permiso para crear los archivos.".

Comprobando los resultados

Nuestra tarea consistía en generar archivos json con los datos de los archivos de localización. Si verificamos las carpetas Excludes en cada paquete, veremos que sean generado satisfactoriamente:

Resultados

Ejecutando el plugin desde la terminal

Recordemos que cuando declaramos el plugin indicamos un parámetro llamado verb, al cual le asignamos el valor gen-locales. Esto nos permite ejecutar el plugin desde la terminal.

En la terminal nos movemos hasta el directorio donde está nuestro paquete, y ejecutamos el siguiente comando:

swift package gen-locales
Plugin ‘LocalizationsGenerator’ wants permission to write to the package directory.
Stated reason: “Necesito permiso para crear los archivos.”.
Allow this plugin to write to the package directory? (yes/no) yes

En la terminal también debemos autorizar el permiso de escritura. Una alternativa para no tener que responder yes en la terminal. Es usar el siguiente comando:

swift package --allow-writing-to-package-directory gen-locales

!Haz creado tu primer plugin, felicidades! Si quieres verificar contra el proyecto final, puedes descargarlo usando este enlace: Paquete Final.

Comparte este artículo

Subscríbete a nuestro Newsletter

Mantente al día en el mundo de las aplicaciones móviles con nuestro blog especializado.

Artículos semanales

Todas las semanas artículos nuevos sobre el mundo de las aplicaciones móviles.

No spam

No te enviaremos spam, solo contenido de calidad. Puedes darte de baja cuando quieras.

Contenido de calidad

Nada de contenido generado de manera automática usando ChatGPT.

Recomendaciones

Tips indispensables sobre mejores prácticas y metodologías.

© 2024 AsyncLearn