SPM desde 0

¿Qué es SPM?

SPM (Swift Package Manager) es un proyecto open source desarrollado por Apple que permite desarrollar y distribuir código para un ecosistema Swift de forma sencilla. Son muchas las razones para empezar a integrar SPM en tus desarrollos. Personalmente una de las virtudes que más me llamó la atención era la posibilidad de desarrollar el paquete (o los paquetes) que necesitabas al mismo tiempo que tu aplicación.

Primeros pasos

¿Qué necesitamos? Dado que el desarrollo va a ser iOS, un Mac. La versión de Xcode ha de permitirnos trabajar Swift 3 o superior. 

Empecemos por el principio, ¡crear el paquete! A mí me gusta trabajar desde la consola así que, abre un terminal, sitúate en la ruta donde desees generar el paquete y ejecuta

  swift package init

Enhorabuena, acabas de crear tu primer paquete (ya que estas podrías iniciar un repositorio de git). Paremos un minuto a comentar los ficheros más importantes que se han generado (puedes abrir el paquete en XCode ejecutando open Package.swift): 

  1. Package.swift – Es el manifiesto de nuestro paquete, en el definiremos su nombre y su descripción, así como los targets, productos, plataformas y las dependencias que estos requieran.
  2. Directorio Sources – Como su propio nombre indica bajo este directorio escribiremos todo el código de nuestro paquete.
  3. Directorio Tests – En este directorio colocaremos los test Unitarios que necesitemos. Cuando construyas tu paquete se ejecutarán dichos test, aparte de que puedes ejecutarlos tu mismo.

Nosotros vamos a utilizar nuestro paquete únicamente para incluirlo en proyectos iOS, así que empecemos por definir las plataformas. En Package.swift, después del argumento name, puedes pasar un argumento Platforms, en nuestro caso iOS 13

platforms: [ .iOS(.v13), ]

Swift es, a día de hoy, un lenguaje multiplataforma aunque si queremos utilizar los frameworks específicos de una determinada plataforma, como en este caso cocoa-touch, hemos de indicar dicha plataforma. 

Tu descriptor debería asemejarse a esto:

// swift-tools-version:5.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
 
import PackageDescription
 
let package = Package(
    name: "helloSPM",
    platforms: [
        .iOS(.v13),
    ],
    products: [
        // Products define the executables and libraries produced by a package, and make them visible to other packages.
        .library(
            name: "helloSPM",
            targets: ["helloSPM"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "helloSPM",
            dependencies: []),
        .testTarget(
            name: "helloSPMTests",
            dependencies: ["helloSPM"]),
    ]
)

Manejando dependencias

El paquete que voy a desarrollar como ejemplo no depende de ningún otro. No obstante, te voy a enseñar cómo declarar dependencias. Dentro de Package.swift, en el constructor del paquete, tenemos dos argumentos que nos van a interesar, dependencies y target. Dentro de dependencies hemos de indicar todos los paquetes que queremos usar y a partir de que versión. Supongamos que queremos usar Alamofire para consumir un API rest y Hippolyte para realizar test de red, podemos declarar la dependencia de la siguiente manera:

dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.0.0")),
        .package(url: "https://github.com/JanGorman/Hippolyte.git", .upToNextMajor(from: "1.0.0")),
    
    ],

No obstante, por más que tengamos declarada la dependencia, no vamos a poder importar ninguno de estos paquetes hasta que no indiquemos en cuál de nuestros targets deseamos utilizarlos. En mi caso deseo utilizar Alamofire en mi paquete y Hippolyte en mis tests, por ello en el constructor de los correspondientes targets indicaré que dependen del paquete correspondiente:

targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "HelloSPM",
            dependencies: ["Alamofire"]),
        .testTarget(
            name: "HelloSPMTests",
            dependencies: ["HelloSPM","Hippolyte"]),
    ]

Desarrolla tu paquete junto con tu app

Una de las funcionalidades más curiosas que tiene SPM es que puedes desarrollar tu paquete mientras desarrollas tu app. Para ello crea tu proyecto iOS en Xcode y después arrastra el directorio donde se encuentra el paquete (desde finder) al root del proyecto.

how to drag and drop the package onto the app

¡Es la hora de picar!

Ahora que ya estamos listos para desarrollar el paquete he de comentarte la limitación más importante que tiene SPM a día de hoy: no admite ficheros por lo que la vista ha de ser programática y estaremos limitados a la hora de realizar otras tareas como localizar nuestro paquete, cargar jsons para moquear las respuestas del servidor en los test unitarios, etc.

Un ejemplo bastante claro de paquete puede ser aquel que pida un nombre al usuario mediante un UITextField y después lo devuelva de tal manera que lo podamos mostrar en la vista principal de nuestra app.

Probablemente te interesaste en SPM porque ya tienes cierta experiencia con Swift, así que, simplemente me centraré en cómo conseguiremos retornar el nombre del usuario a la aplicación. Para casi cualquier problemática a la que nos enfrentemos ya existe un patrón que la resuelve, en este caso, vamos a utilizar el patrón delegado. Si no conoces este patrón, te recomiendo revisar este artículo.

Para implementar el patrón delegado lo primero que vamos a hacer es implementar el protocolo.

//MARK: Protocols
    public protocol HelloSPMResultDelegate: class {
       func returnName(_ name: String)
    }
 

A continuación, vamos a crear un nuevo miembro “delegate” en el view controller de nuestro package:

public weak var delegate: ResultDelegate?

Y ya por último solo hemos de llamar al método que delegamos en algún momento, en mi caso:

 

@objc func buttonAction(sender: UIButton) {
  if let name: String = nameField.text {
      delegate?.returnName(name)
      self.dismiss(animated: true, completion: nil)
         }
     }

El package está terminado, solo nos queda instanciar y lanzar nuestro ViewController desde la aplicación para usar el package. Llegados a este punto es necesario comprobar que nuestro paquete se encuentra en el apartado de “Frameworks, Libraries and Embedded Content” de nuestro proyecto. Si no es el caso, pulsa en el botón plus y añádelo:

Importing package

 

Usa tu paquete en cualquier app

Pongamos que no solo queremos usar el paquete junto con la aplicación que estábamos desarrollando. ¿Cómo importo este paquete, que ya tengo terminado, en otras apps? A traves de GitHub.

Para añadir una dependencia de un package disponible en GitHub en tu proyecto Xcode vete a el apartado de “Frameworks, Libraries and Embedded Content” y haz click en el botón plus. En el menú que se muestra has de seleccionar Add Package Dependency:

 

En el siguiente menú simplemente introduce la URL de tu repositorio. Si no has desarrollado el tuyo puedes usar el mío:  https://github.com/solidgear/HelloSPM

 

 

Add package

¿Cómo uso el paquete?

Ahora que ya sabemos cómo ha de ser importado nuestro paquete en ambas situaciones, llega el momento de escribir el código de nuestra aplicación. 

 

Lo primero de todo es importar nuestro paquete:

import helloSPM

Una vez ya podemos acceder a los miembros públicos lo primero que tenemos que hacer es implementar el protocolo. A mí me gusta hacerlo en una extensión, pero no es obligatorio:

extension ViewController: HelloSMPResultDelegate {
    func returnName(_ name: String) {
        self.viewModel.username = name
    }
}

Tras esto solo tenemos que crear un método que nos permita instanciar el ViewController del paquete, asignarle el ViewController de la app como delegado y mostrar dicho ViewController:

@objc func buttonAction(sender: UIButton) {
        let vc = PackageViewController()
        vc.delegate = self
        vc.modalPresentationStyle = .fullScreen
        self.present(vc, animated: true, completion: nil)
   }

Conclusiones

Llevo trasteando con SPM algo más de un mes, desarrollando diferentes paquetes con UI, y la verdad es que creo que aparte de para desarrollar librerías al uso, como las que he mencionado antes, por ejemplo Alamofire, puede ser muy útil para reutilizar vistas estándar entre diferentes apps relacionadas.

Por ejemplificar esto ultimo: si tu preparas una vista login con la iconografía de tu empresa probablemente la quieras utilizar en todas las aplicaciones que tu empresa ofrezca. Al mantener esta UI en un paquete, si en un momento dado tu empresa cambia su logo o sus colores, podrás actualizarlo rápidamente en todas las apps simplemente sacando una nueva versión del paquete.

A día de hoy SPM tiene una serie de limitaciones que pueden llegar a ser exasperantes para un programador que este acostumbrado a construir sus apps gracias a storyboards o xibs. No obstante no os desaniméis porque en Swift 5.3 el soporte para recursos ya esta implementado. Puedes leer más sobre esto aquí.

Eso fue todo. Espero que os sea de utilidad. Si tenéis alguna duda o sugerencia no dudéis en contactarme por redes, no muerdo 😉

Deja un comentario

¿Necesitas una estimación?

Calcula ahora