Primeros pasos con Swift Package Manager en iOS

0
5783

Índice de contenidos

  • 1. Introducción
  • 2. Entorno
  • 3. Creando nuestro paquete
  • 4. Importando el paquete en una app iOS
  • 5. Conclusiones

 

1. Introducción

Con la salida de Swift 3.0, Apple liberó una herramienta para manejar las dependencias de los proyectos de lado servidor y por línea de comandos llamada Swift Package Manager. Con la llegada de la versión 5, Apple introdujo la compatibilidad de esta herramienta con proyectos iOS, MacOS y TvOS ademas de poder crearlos y configurarlos mediante Xcode. Swift Package Manager nos sirve para automatizar el proceso de descarga, compilación y linkado de las dependencias de un proyecto de forma mucho más sencilla que las actuales soluciones de terceros.

 

1.1 ¿Qué son los paquetes?

Los paquetes son básicamente una colección de archivos de código en Swift y un fichero llamado Package.swift que define la configuración del mismo, este fichero contiene información de sus propias dependencias con otras librerías en caso de que las hubiera. Además, los paquetes pueden existir de forma local o de forma remota como un repositorio de git, por lo que podríamos bajarnos un paquete de git, hacernos una copia local, y hacer nuestras propias modificaciones.

Con la versión de Swift 5.3, que previsiblemente se liberará a finales de este año 2020, se van a incorporar una serie de mejoras muy interesantes a tener en cuenta:

  • Poder añadir librerías binarias, esta característica va a ser la que mate finalmente a Cocoapods, Carthage, o cualquier solución de terceros para el manejo de dependencias, ya que hasta ahora solo podríamos incluir librerías de código fuente (no compiladas).
  • Poder añadir recursos como imágenes, sonido, interfaces… etc, además de soporte de localización en los mismos.
  • Dependencias condicionales que nos van a servir para manejar las dependencias en función a la plataforma de ejecución, esto nos va a servir por ejemplo para importar una dependencia cuando ejecutamos en Mac y otra cuando ejecutamos en iOS.

Vamos a ver un ejemplo de cómo crear nuestro paquete y usarlo desde una aplicación de iOS.

 

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 Ghz Intel Core i9 de 8 núcleos, 32GB DDR4).
  • Sistema Operativo: Mac OS Catalina 10.15.4
  • Entorno de desarrollo: Xcode 11.5.1

 

3. Creando nuestro paquete

Vamos a Xcode -> File -> New -> Swift Package y creamos nuestro paquete llamado StringHelperPackage indicando que genere un repositorio de git (esto nos servirá más adelante para subir nuestro repositorio):

Una vez creado vemos la siguiente estructura de carpetas:

En la carpeta Sources es donde vamos a incluir los ficheros que formen nuestra librería, en caso de que vayamos a crear distintos targets, por convención tendríamos que crear una segunda carpeta, al igual que se hace con la capeta de Test (que es un target independiente).

El fichero Package.swift es el «manifiesto» de lo que contiene nuestro paquete, aquí es donde indicamos las librerías que va a generar y las dependencias y targets que tiene, veámoslo en detalle:

import PackageDescription

let package = Package(
    name: "StringHelperPackage",
    products: [
        // Products define the executables and libraries produced by a package, and make them visible to other packages.
        .library(
            name: "StringHelperPackage",
            targets: ["StringHelperPackage"]),
    ],
    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: "StringHelperPackage",
            dependencies: []),
        .target(name: "Run", dependencies: ["StringHelperPackage"]),
        .testTarget(
            name: "StringHelperPackageTests",
            dependencies: ["StringHelperPackage"]),
        
    ]
)

En primer lugar indicamos el nombre del paquete,  a continuación, pasamos un array de products que van a ser los ejecutables o librerías que va a generar.

En el parámetro dependencies vamos a añadir las dependencias de nuestro paquete, que a su vez pueden tener dependencias de otros, esto lo va a gestionar todo de forma automática Swift Package Manager. De cara a añadir las dependencias tenemos distintas funciones disponibles:

Desde una versión específica: (instalará una versión superior si la hubiera):

.package(url: "https://foo.com/...", from: "1.1")

Especificar una versión concreta:

.package(url: "https://foo.com/...", .exact("1.2"))

Especificar una rama concreta:

.package(url: "https://foo.com/...", .branch("Development"))

También es posible especificar un hash:

.package(url: "https://foo.com/...", .revision("33hbv23m2l323k4256n4mmj45h4b"))

Por último en el array de targets vamos a definir los targets que va a tener nuestro paquete.

Si queremos especificar una plataforma y versión mínima requerida por nuestro paquete, bastaría con añadir este fragmento de código:

platforms: [
        SupportedPlatform.iOS(.v13),
        SupportedPlatform.macOS(.v10_15),
        SupportedPlatform.watchOS(.v6),
        SupportedPlatform.tvOS(.v13)
    ],

 

3.1 Añadiendo funcionalidad

Vamos a ir al fichero StringHelperPackage.swift y vamos a dejarlo de la siguiente forma:

import Foundation

public struct StringHelperPackage {
    
    func localizedString(_ string: String) -> String {
        string.localized()
    }
    
    func joinAttributedStrings(_ strings: NSAttributedString...) -> NSAttributedString {
        return NSAttributedString(string: strings.map { $0.string }.joined())
    }
}

extension String {
    public func localized(comment: String = "") -> String {
        return NSLocalizedString(self, comment: comment)
    }
}

Estas son una par de funciones que hemos creado para la prueba de este tutorial. También nos vamos al  fichero StringHelperPackageTest y lo cambiamos para que los test prueben nuestro código:

import XCTest
@testable import StringHelperPackage

final class StringHelperPackageTests: XCTestCase {
    func testExample() {
        XCTAssertEqual(StringHelperPackage().localizedString("Hello, World!"), "Hello, World!")
        XCTAssertEqual(StringHelperPackage().joinAttributedStrings(
            NSAttributedString(string: "Hello, "),
            NSAttributedString(string: "World!"))
            .string, "Hello, World!")
    }

    static var allTests = [
        ("testExample", testExample),
    ]
}

 

3.2. Publicar el paquete

Ya tenemos hecho un paquete con una funcionalidad mínima, es hora de subirlo a git,  vamos a la barra de menú de Xcode y en Source Controlo hacemos un commit de los cambios realizados:

Para subir nuestro proyecto a GitHub, si no tienes una cuenta, es un buen momento para hacerla, ¡es gratuito! También es posible hacerlo en otra plataforma como BitBucket o GitLab.

Una vez hecho el commit, tenemos que tener linkada nuestra cuenta de Github en Xcode. Para hacerlo vamos a preferencias de Xcode y en el apartado cuentas, añadimos la cuenta:

Una vez configurada la cuenta vamos a la pestaña de Xcode de control de código (command +2) y pulsamos con el botón derecho sobre nuestra rama, se desplegará un menú y pulsamos en Create remote:

Nos pedirá que seleccionemos la cuenta, el propietario, el nombre y la descripción, además de indicar si el repositorio va a ser público o privado, es este caso seleccionaremos público:

Tardará unos segundos en crearla y una vez terminado el proceso ya tenemos nuestro paquete subido al repositorio.

Pero aún nos falta un detalle importante, todos nuestros paquetes han de tener un tag identificando la versión en la que se encuentran, para hacer esto vamos a pulsar con el botón derecho del ratón sobre nuestro commit y vamos a pulsar sobre «Tag…»:

Nos va a pedir el Tag, que al ser nuestra primera versión escribimos «1.0.0», opcionalmente podremos indicar un comentario:

Una vez hecho, tenemos que subir al repositorio nuestro tag, pulsamos en la barra de menús el botón Source Control -> Push:

Tenemos que seleccionar el checkbox de «Include tags»:

Y listo, ¡ya tenemos nuestro paquete etiquetado con su versión 1.0.0 en el repositorio listo para usar!

 

4. Importando el paquete en una app iOS

Ahora vamos a probar nuestro paquete en una aplicación iOS. En Xcode vamos a crear una aplicación iOS de tipo Single View a la que llamaremos TestPackage:

Ahora vamos a importar nuestro paquete, para ello, debemos obtener la url del mismo, esta la vamos a encontrar en GitHub pulsando el botón «Clone or Download»:

Pegamos la url en nuestro portapapeles y nos vamos al menu Xcode -> File -> Swift Packages -> Add Package Dependency:

Ahora pegamos nuestra dirección y pulsamos Next:

Nos pedirá que especifiquemos una número de versión, una rama o un commit específico, nosotros vamos a seleccionar la versión 1.0.0 que es la que hemos etiquetado anteriormente:

Si pulsamos en Next se hace la importación y ya podemos ver en nuestro árbol nuestra dependencia creada:

Ahora vamos a nuestro ViewController y escribimos lo siguiente:

import UIKit
import StringHelperPackage

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        print(StringHelperPackage().localizedString("foo"))
    }
}

Hemos importado nuestra librería y hemos llamado a su función localizedString(), y como has podido comprobar…Error! Xcode te indica que el struct no tiene constructores accesibles ya que son de tipo internal. Es muy importante el control de acceso de nuestras clases, structs o funciones en un paquete, ya que al ser una librería importada y no formar parte del módulo de la aplicación donde la usemos, vamos a tener que especificar como public aquellas funciones que queramos que sean accesible.

Para arreglar esto vamos a abrir nuestro paquete, en caso de que lo hayamos cerrado basta con pulsar dos veces sobre el fichero Package.swift (no existe un .xcproj o .xcworkspace).

Nos vamos a nuestro fichero StringHelperPackage.swift  y cambiamos las funciones dándoles el acceso public y escribimos un constructor public también:

public struct StringHelperPackage {
    public init() {}
    public func localizedString(_ string: String) -> String {
        string.localized()
    }
    
    public func joinAttributedStrings(_ strings: NSAttributedString...) -> NSAttributedString {
        return NSAttributedString(string: strings.map { $0.string }.joined())
    }
}

Ahora hacemos un commit con los cambios y marcamos la casilla «push to remote». Ya solo nos queda hacer el tag nuevo como hemos hecho antes y hacer push. Al ser un fix, por convención se le etiquetaría como versión 1.0.1.

Ahora nos vamos a nuestro proyecto iOS y seleccionamos en el menú de Xcode -> File -> Swift Packages -> Update to latest package version:

Xcode comprobará en git si hay una nueva etiqueta y se descargará la nueva versión. Tras esta acción ya nos compila perfectamente el proyecto y si compilamos y ejecutamos podremos ver que se ejecuta el print correctamente:

¡Hemos terminado! ya tenemos los conocimientos necesarios para crear nuestro propios paquetes e importarlos en nuestros proyectos de Xcode.

 

5. Conclusiones

Por fin, los desarrolladores de aplicaciones iOS tenemos una herramienta que funciona de forma limpia, rápida y cómoda para importar nuestras dependencias, y todo incorporado de forma nativa en el IDE. Ya solo queda que los grandes como FireBase por ejemplo, se animen a publicar sus librerías en forma de paquetes para que podamos olvidar las actuales soluciones de terceros que tantos problemas nos generan.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad