Swift Package Manager es la herramienta oficial de gestión de dependencias y build de proyectos Swift. En su versión básica, se usa para añadir paquetes de terceros y organizar el código en targets. Pero SPM tiene un sistema de plugins muy potente que permite automatizar tareas de build como generación de código, linting o compresión de recursos directamente desde Package.swift, sin herramientas externas.
Build plugins: código generado en compilación
Un build plugin es un plugin que SPM ejecuta como parte del proceso de compilación. El caso más habitual es la generación de código a partir de otros archivos (protobuf, OpenAPI, etc.):
// Package.swift
let package = Package(
name: "MiApp",
targets: [
.target(
name: "MiApp",
plugins: [
.plugin(name: "GeneradorOpenAPI", package: "swift-openapi-generator")
]
),
// Definir un plugin propio
.plugin(
name: "GeneradorLocalizable",
capability: .buildTool()
)
]
)
La implementación del plugin reside en el target del plugin y conforma al protocolo BuildToolPlugin:
// Plugins/GeneradorLocalizable/plugin.swift
import PackagePlugin
@main
struct GeneradorLocalizable: BuildToolPlugin {
func createBuildCommands(
context: PluginContext,
target: Target
) async throws -> [Command] {
// Encontrar archivos .strings en el target
guard let sourceTarget = target as? SourceModuleTarget else { return [] }
let archivosStrings = sourceTarget.sourceFiles
.filter { $0.path.extension == "strings" }
.map { $0.path }
guard !archivosStrings.isEmpty else { return [] }
let salida = context.pluginWorkDirectory.appending("GeneratedStrings.swift")
return [
.buildCommand(
displayName: "Generar enums de localización",
executable: try context.tool(named: "strings-gen").path,
arguments: archivosStrings.map { $0.string } + ["--output", salida.string],
inputFiles: archivosStrings,
outputFiles: [salida]
)
]
}
}
Command plugins: comandos invocables desde terminal
Los command plugins no forman parte del build, sino que se invocan manualmente con swift package:
// Package.swift
.plugin(
name: "FormatearCodigo",
capability: .command(
intent: .sourceCodeFormatting(),
permissions: [.writeToPackageDirectory(reason: "Formatear archivos Swift")]
)
)
// Plugins/FormatearCodigo/plugin.swift
@main
struct FormatearCodigo: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
let swift_format = try context.tool(named: "swift-format")
let archivos = context.package.targets
.compactMap { $0 as? SourceModuleTarget }
.flatMap { $0.sourceFiles.filter { $0.path.extension == "swift" } }
.map { $0.path.string }
try Process.run(
URL(fileURLWithPath: swift_format.path.string),
arguments: ["--in-place"] + archivos
).waitUntilExit()
}
}
Se invoca con: swift package plugin FormatearCodigo
Binary targets: distribuir frameworks precompilados
Cuando distribuyes un SDK o un framework privado sin querer exponer el código fuente, uses binary targets con XCFramework:
// Package.swift del SDK
let package = Package(
name: "MiSDK",
products: [
.library(name: "MiSDK", targets: ["MiSDKBinary"])
],
targets: [
.binaryTarget(
name: "MiSDKBinary",
url: "https://cdn.example.com/MiSDK-2.1.0.zip",
checksum: "abc123def456..." // sha256 del zip
)
]
)
Para crear el XCFramework:
# Compilar para cada arquitectura
xcodebuild archive -scheme MiSDK -destination "generic/platform=iOS"
-archivePath build/iOS SKIP_INSTALL=NO
xcodebuild archive -scheme MiSDK -destination "generic/platform=iOS Simulator"
-archivePath build/iOS-Sim SKIP_INSTALL=NO
# Crear el XCFramework
xcodebuild -create-xcframework
-framework build/iOS.xcarchive/Products/Library/Frameworks/MiSDK.framework
-framework build/iOS-Sim.xcarchive/Products/Library/Frameworks/MiSDK.framework
-output MiSDK.xcframework
# Comprimir y calcular checksum
zip -r MiSDK-2.1.0.zip MiSDK.xcframework
swift package compute-checksum MiSDK-2.1.0.zip
Recursos: process() y copy()
Los targets que necesitan archivos no-Swift (imágenes, JSON, modelos de ML) los declaran en la sección resources:
.target(
name: "MiApp",
resources: [
// Procesar: aplica optimizaciones según el tipo (imágenes ? asset catalog)
.process("Resources/Imagenes"),
// Copy: copia sin modificaciones (preserva estructura de directorios)
.copy("Resources/Modelos/modelo.mlmodel"),
.copy("Resources/Datos/config.json"),
// Excluir archivos que no son recursos
.process("Resources/Localizacion", localization: .default)
]
)
Acceder a los recursos en código:
// Imagen en asset catalog (tras .process)
let imagen = UIImage(named: "mi-imagen", in: .module, compatibleWith: nil)
// Fichero copiado
if let url = Bundle.module.url(forResource: "config", withExtension: "json") {
let datos = try Data(contentsOf: url)
let config = try JSONDecoder().decode(Configuracion.self, from: datos)
}
Dependencias por plataforma
Puedes condicionar dependencias y targets según la plataforma:
let package = Package(
name: "MiApp",
platforms: [.iOS(.v17), .macOS(.v14)],
targets: [
.target(
name: "MiApp",
dependencies: [
.target(name: "UIComponents"),
// Solo en iOS
.target(name: "PushNotifications",
condition: .when(platforms: [.iOS])),
// Solo en macOS
.target(name: "MenuBar",
condition: .when(platforms: [.macOS]))
]
)
]
)
Swift Package Registry: publicar paquetes
Además de GitHub, SPM soporta registros privados y el registro oficial de Apple. Para publicar en un registro:
# Configurar el registro
swift package-registry set-default https://packages.swift.org
# Autenticarse
swift package-registry login https://packages.swift.org --token TU_TOKEN
# Publicar
swift package-registry publish 1.0.0
--registry https://packages.swift.org
Para usar paquetes del registro en vez de URLs de GitHub:
dependencies: [
// Por URL (GitHub/GitLab)
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
// Por identidad de registro
.package(id: "swift.org.swift-algorithms", from: "1.2.0")
]
Resumen
SPM en 2024 es mucho más que un gestor de dependencias. Los build plugins automatizan la generación de código integrándola en el compilador, los command plugins añaden herramientas de desarrollo al proyecto, los binary targets permiten distribuir código privado sin exponerlo, y el sistema de recursos hace que los assets sean parte de primera clase del paquete. Dominar estas características permite construir toolchains de desarrollo completas sin salir del ecosistema Swift.
