Crear módulos con Swift Package Manager y Xcode
Crear módulos con Swift Package Manager y Xcode

Cómo crear un módulo con Swift Package Manager y hacerlo open source en Español

Crea módulos con Swift Package Manager en Xcode de una manera rápida y fácil. Swift Package Manager nos permite crear módulos y así agrupar código que está relacionado. Normalmente estos módulos tienen el código, assets, tests, etc. Al tener el código en módulos es mucho más limpio.

SwiftBeta
Aprende a crear módulos con Swift Package Manager

Hoy en SwiftBeta vamos a aprender a crear módulos con Swift Package Manager. Pero ¿por qué es importante crear módulos? Cuando tienes una app en la que no solo empieza a crecer el código sino que también los developers que trabajan en ella, lo mejor que se puede hacer es crear módulos. La finalidad de los módulos es extraer código que está relacionado, hay distintas maneras de agrupar este código una podría ser una feature. Es decir, tener un módulo del Chat, Wallet, DesignSystem, etc.
Normalmente dentro de las empresas, cada feature es asignada a un equipo y ese equipo es el encargado de iterarla, arreglar bugs, añadir los tests, etc.

Si tenemos esta clara separación de features/responsabilidad es más fácil que:

  • No hayan conflictos, es decir, no tendremos a varios developers tocando el mismo código.
  • Tiempos de compilación más rápidos. Puedo probar mis cambios en mi módulo en lugar de compilar la app principal.
  • Mejor categorización de bugs.
  • Evitamos duplicar código.
  • Podemos reusar módulos en otras apps.

Hoy lo que vamos a ver es a crear una app muy sencilla simulando un login (con su formulario, imagen, button, etc), no haremos ninguna petición HTTP, el propósito del video es crear un módulo en Swift Package Manager.
Una vez creado el login extraeremos esta pantalla a un módulo, lo llamaremos el módulo de Login y lo que haremos será importar el módulo de Login a nuestra app. Ese login lo podremos reutilizar en varias apps.

Desde Swift 5.3 podemos añadir a nuestros módulos de SPM imágenes, sonidos, etc. Al crear el paquete SPM generará el Bundle con el contenido que hayas añadido. Pudiéndolo usar sin ningún tipo de problemas. Antes de la versión 5.3 era imposible añadir recursos.

Creamos nuestro Login

Vamos a crear la siguiente vista

Login en SwiftUI
Login en SwiftUI

Y vamos a utilizar el siguiente código para representar nuestro Login, fíjate que hemos usado una imagen llamada swiftbeta, en este caso podéis usar cualquier otra, pero te la dejo aquí por si quieres usar la misma:

Logo de SwiftBeta
Logo de SwiftBeta

y aquí te dejo el código de la vista en SwiftUI:

import SwiftUI

struct ContentView: View {
    @State var email: String = ""
    @State var password: String = ""
    
    var body: some View {
        VStack {
            Image("swiftbeta")
                .resizable()
                .frame(width: 200, height: 200)
            Group {
                HStack {
                    Image(systemName: "envelope.fill")
                    TextField("Email", text: $email)
                        .keyboardType(.emailAddress)
                }
                HStack {
                    Image(systemName: "key.fill")
                    TextField("Password", text: $password)
                        .keyboardType(.emailAddress)
                }
            }
            .padding()
            .overlay(
                    RoundedRectangle(cornerRadius: 12)
                        .stroke(.black, lineWidth: 1)
            )
            .padding(.horizontal, 80)
            Button("Login") {
                print("Login...")
            }
            .buttonStyle(.bordered)
            .tint(.black)
            Text("Suscríbete a SwiftBeta para apoyar el canal")
                .font(.footnote)
                .underline()
                .foregroundStyle(.tertiary)
                .padding(.top, 40)
            Spacer()
        }
    }
}
Vista Login en SwiftUI

Y a continuación vamos a crear el ViewModel con la lógica simulada de nuestro Login, ¿qué significa que simularemos el login? pues que para simplificar el código lo que haremos es que solo validaremos que el email introducido sea swiftbeta.blog@gmail.com, el propósito del video no es crear un Login, es crear un módulo. Cuando el user se loguee mostraremos un alert para mostrar si ha sido exitoso o no.
En esta clase también añadiremos funciones que simularan el registro,  recover password, etc.

import Foundation

enum LoginStatus: Error {
    case success
    case error
}

final class AuthenticationViewModel: ObservableObject {
    @Published var loginStatus: LoginStatus = .success
    @Published var didUpdateLoginStatus: Bool = false
    
    func login(email: String, password: String) {
        print("login...")
        if email.lowercased() == "swiftbeta.blog@gmail.com" {
            loginStatus = .success
        } else {
            loginStatus = .error
        }
        didUpdateLoginStatus = true
    }
    
    func signup(email: String, password: String) {
        print("registrarse...")
    }
    
    func recoverPassword(email: String) {
        print("Recuperar contraseña...")
    }
    
    func getAlertTitle() -> String {
        loginStatus == .success ? "Login Success" : "Login Error"
    }
}
ViewModel para simular nuestro Login

Una vez tenemos nuestro AuthenticationViewModel vamos a usarlo en la vista:

import SwiftUI

struct ContentView: View {
    @StateObject var authenticationViewModel = AuthenticationViewModel()
    @State var email: String = ""
    @State var password: String = ""
    
    var body: some View {
        VStack {
            Image("swiftbeta")
                .resizable()
                .frame(width: 200, height: 200)
            Group {
                HStack {
                    Image(systemName: "envelope.fill")
                    TextField("Email", text: $email)
                        .keyboardType(.emailAddress)
                }
                HStack {
                    Image(systemName: "key.fill")
                    TextField("Password", text: $password)
                        .keyboardType(.emailAddress)
                }
            }
            .padding()
            .overlay(
                    RoundedRectangle(cornerRadius: 12)
                        .stroke(.black, lineWidth: 1)
            )
            .padding(.horizontal, 80)
            Button("Login") {
                print("Login...")
                authenticationViewModel.login(email: email, password: password)
            }
            .buttonStyle(.bordered)
            .tint(.black)
            Text("Suscríbete a SwiftBeta para apoyar el canal")
                .font(.footnote)
                .underline()
                .foregroundStyle(.tertiary)
                .padding(.top, 40)
            Spacer()
        }
        .alert(authenticationViewModel.getAlertTitle(),
               isPresented: $authenticationViewModel.didUpdateLoginStatus) {
            Button("Aceptar") {
                print("Dismiss Alert")
            }
        }

    }
}
Vista en SwiftUI con ViewModel integrado

Todo este código lo tenemos en la aplicación principal, pero lo que vamos hacer es crear un módulo, mover el código que acabamos de crear e importar el módulo en la app principal.

Creamos el módulo de Login con Swift Package Manager

Vamos a crear el módulo de Swift Package Manager, para hacerlo nos vamos a:

File -> New -> Package

Cómo crear un módulo con Swift Package Manager
Cómo crear un módulo con Swift Package Manager

Al hacerlo, aparecerá la siguiente pantalla:

Escoger el nombre del módulo en Swift Package Manager, Lo llamaremos LoginModule
Escoger el nombre del módulo en Swift Package Manager

Aquí, vamos a poner el nombre de nuestro nuevo módulo, vamos a llamarlo LoginModule y fíjate que en mi caso estoy en un raíz de proyectos diferentes. Aquí nos vamos a ir a los dos selectores de abajo, donde pone Add to y Group y justo en Add to vamos a seleccionar el proyecto en el que estábamos

Seleccionar la opción "Add to" al crear un módulo para escoger nuestro proyecto de Xcode
Seleccionar la opción "Add to" al crear un módulo para escoger nuestro proyecto de Xcode

Al hacerlo, selecciona solo el selector de Add to me rellena también el Group de la siguiente manera:

Escogemos la carpeta donde queremos crear el módulo
Escogemos la carpeta donde queremos crear el módulo

Antes de darle a Create, vamos a buscar  nuestro proyecto y vamos abrir la carpeta que lo contiene, así al crear el módulo la carpeta que contiene todo su código también estará ahí. Es decir, en mi caso busco la carpeta con el nombre de mi proyecto llamado SwiftBeta-SPM-Modulos y cuando estoy dentro de la carpeta finalmente creo el módulo pulsando en el botón de Create.

A pulsar en Create vemos como la estructura de nuestros ficheros en Xcode ha cambiado, ahora podemos ver el módulo de LoginModule que acabamos de crear.

Package.swift de nuestro módulo
Este es el Package.swift de nuestro módulo

Tal y como explicamos en el primer video de Swift Package Manager, al usar código de terceros o al crear nuestro propio módulo, se genera un fichero package que contiene información relevante. Desde el nombre del módulo, targets, dependencias que puedas tener etc.

Al crear el módulo este está completamente vacío, hay que mover clases de la aplicación principal al módulo. La primera que vamos a mover va a ser AuthenticationViewModel

Pues vamos a arrastrar la clase al módulo y vamos a compilar a ver qué ocurre.

Arrastramos AuthenticationViewModel a nuestro LoginModule
Movemos código a nuestro nuevo módulo

Al compilar obtenemos varios errores, el compilador se queja de que no encuentra la clase AuthenticationViewModel.

Errores al compilar nuestro módulo, no se encuentra AuthenticationViewModel
Errores al compilar nuestro módulo

Para poder usar la clase AuthenticationViewModel hay que hacer varios cambios en nuestro código. Lo primero de todo es importar el nuevo módulo LoginModule en ContentView. Vamos a compilar otra vez para ver qué ocurre.

Importamos LoginModule para ver si se corrige el error
Errores al compilar nuestro módulo #2

Al compilar seguimos obteniendo el mismo error. ¿Qué está pasando? Pues igual que podemos compilar el código de nuestra app, también podemos cambiar el target y probar el código que tenemos en nuestro módulo, esto nos ayudará a saber qué está pasando. Vamos a cambiar de target a LoginModule y vamos a compilar:

Cambiamos target para compilar solo el código del módulo
Cambiamos target para compilar solo el código del módulo que acabamos de crear con Swift Package Manager

Al cambiar el target a LoginModule y compilar vemos el siguiente error:

Error que aparece en nuestro módulo, el property wrapper que usamos en AuthenticationViewModel @Published solo se puede usar a partir de iOS 13
Errores al compilar nuestro módulo #4

Ahora ya sabemos qué estaba pasando. Al tener un error en el módulo, la app no compilaba. Vamos a arreglar el error del módulo, para ellos nos vamos al Package de nuestro LoginModule y añadimos cuál es el mínimo sistema operativo que puede usar nuestro módulo. En nuestro caso vamos a usar que sea iOS 15.

let package = Package(
    name: "LoginModule",
    platforms: [.iOS(.v15)],
    ...
Añadimos a Package.swift un nuevo parámetros platforms con iOS 15

Si ahora compilamos vemos, que en nuestro nuevo módulo han desaparecido los errores. Vamos a cambiar de target otra vez y vamos a seleccionar la app principal, a ver qué ocurre.

Cambiamos el target para compilar otra vez la app principal
Cambiamos el target para compilar otra vez la app principal

Al compilar seguimos teniendo un error, no encuentra la clase AuthenticationViewModel. Esto es debido a que cuando metemos código en un módulo debemos utilizar los Access Levels adecuados. Para no pasarnos con el video de hoy, vamos a decir que todo el código que queremos usar de nuestro módulo en la app principal debe ser público. Por eso debemos exponer qué clases y métodos queremos usar desde la app principal y para hacerlo debemos colocar la palabra public. Vamos a verlo mejor en código:

import Foundation

enum LoginStatus: Error {
    case success
    case error
}

final public class AuthenticationViewModel: ObservableObject {
    @Published var loginStatus: LoginStatus = .success
    @Published public var didUpdateLoginStatus: Bool = false
    
    public init() { }
    
    public func login(email: String, password: String) {
        print("login...")
        if email.uppercased() == "swiftbeta.blog@gmail.com" {
            loginStatus = .success
        } else {
            loginStatus = .error
        }
        didUpdateLoginStatus = true
    }
    
    func signup(email: String, password: String) {
        print("registrarse...")
    }
    
    func recoverPassword(email: String) {
        print("Recuperar contraseña...")
    }
    
    public func getAlertTitle() -> String {
        loginStatus == .success ? "Login Success" : "Login Error"
    }
}
Añadimos Access Level public en nuestro ViewModel
Fíjate que hay método en los que no hemos añadido public, ya que de momento no los queremos exponer fuera del módulo

Y el último paso que nos falta es importar el Framework a la app principal:

Importamos el módulo en Frameworks, Libraries and Embedded Content en Xcode
Importamos el módulo en Frameworks, Libraries and Embedded Content en Xcode

Una vez hemos hecho estos pasos:

  • Añadir platforms: [.iOS(.v15)] en Package.swift
  • Añadir public en AuthenticationViewModel para exponer el código que queremos usar desde la app principal.
  • Hacer import de LoginModule
  • Añadir nuestro módulo en la sección de Frameworks, Libraries and Embedded Content

Podemos compilar nuestro código y funciona perfectamente, al poner un email cualquiera y darle a login vemos como aparece el alert. Lo que hemos hecho ha sido mover una clase a un módulo pero podríamos mover la vista e incluso la imagen que usamos en la pantalla de Login. Todo esto lo vamos a ver en la siguiente sección.

Mover la Vista e Imagen al módulo LoginModule

La vista ContentView la vamos a arrastrar al módulo y vamos a crear un inicializador público para poderlo usar desde la app principal. También expondremos la struct y su propiedad body usando public.

Movemos la vista ContentView a nuestro módulo LoginModule (tal y como hemos hecho con el AuthenticationViewModel)
Movemos la vista en SwiftUI a nuestro módulo

El código quedaría de la siguiente manera:

public struct ContentView: View {
    @StateObject var authenticationViewModel = AuthenticationViewModel()
    @State var email: String = ""
    @State var password: String = ""
    
    public init() { }
    
    public var body: some View {
    ...
Añadimos public para poder acceder al código desde fuera del módulo

Y lo único que tenemos que hacer en la app principal es importar LoginModule donde se esté utilizando ContentView(), en mi caso el código quedaría:

import SwiftUI
import LoginModule

@main
struct SwiftBeta_SPM_ModulosApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Importamos nuestro módulo

Ahora lo único que nos falta por meter en el módulo es la imagen que hemos usado para mostrar el Login. Para hacerlo vamos a crear un nuevo fichero llamado Assets (como el que tenemos en la app principal). Pulsamos CMD+N y buscamos Asset Catalog.

Añadimos un Asset Catalog en nuestro LoginModule para añadir el logo de SwiftBeta
Añadimos un Asset Catalog para añadir el logo de SwiftBeta

Cuando lo creemos añadimos la imagen que estábamos usando en la app principal (y la borramos de allí, así nos aseguramos que la única imagen que tenemos está en el módulo LoginModule).

Si compilamos la app principal veremos que no nos aparece el icono, debemos hacer un pequeño cambio. Lo que vamos hacer es irnos a la vista ContentView que tenemos en el módulo LoginModule y vamos a añadir un nuevo parámetro cuando inicializamos la Image.

Image("swiftbeta", bundle: .module)
                .resizable()
                .frame(width: 200, height: 200)
Cambiamos el Bundle de la imagen

Vamos a añadir un nuevo parámetro llamado bundle y vamos a darle el valor .module. De esta manera si compilamos la app vemos como la imagen de swiftbeta vuelve a aparecer, pero esta vez la imagen la tenemos dentro del módulo.

Lo que acabamos de hacer es muy potente, hemos creado un módulo que tiene una única finalidad y es la de tener código relacionado con el Login. Al tener este módulo totalmente aislado y funcional, podríamos crear un repo en Github y hacerlo open source para que otros developer lo utilicen (o incluso nosotros en otras app). Vamos a subir nuestro código a Github y vamos a crear un proyecto de cero para ver lo fácil que sería usarlo en un proyecto de cero.

Publicar nuestro módulo Swift Package Manager en Github

Para poder publicar nuestro código en Github primero debemos tener inicializado git en la carpeta del módulo.

Ejecutamos "git init" en la carpeta de nuestro módulo. Esto lo hacemos para poder comitear todos nuestros ficheros
Creamos la carpeta git en nuestro repositorio

Para ello nos vamos a la carpeta de nuestro módulo y pulsamos en la terminal

git init

Y comiteamos todos los cambios que tenemos. Te aconsejo que sigas este post que hice para crear un repo en Github y subir todo tu código local.

Github: Primeros Pasos - Comparte tu código con la comunidad
En este post vemos como subir nuestro proyecto a Github. Es una guía paso a paso para que puedas compartir con la comunidad todos tus avances con Swift.
Ejecutamos git add . y git commit -m "First Commit"
Comiteamos los cambios de nuestro módulo

Una vez hemos comiteado los cambios y creado nuestro proyecto en Github (el cual hemos llamado LoginModule), vamos a pushear nuestro código a nuestro repo en Github. Y una vez subido quedaría como en la siguiente imagen:

Pusheamos los cambios a nuestro repo creado en Github
Pusheamos los cambios de nuestro repo a Github
Si tienes alguna duda por favor déjame un comentario en el video de Youtube.

Utilizar nuestro módulo Swift Package Manager en un nuevo Proyecto

Ahora, si quisieramos utilizar este código en un nuevo proyecto o lo quisiera utilizar otro iOS developer, lo único que tendríamos que hacer es crear el proyecto de cero e instalar el paquete usando la URL de Github

📦 Aprender a usar Swift Package Manager en Español | Primeros pasos
¿Qué es Swift Package Manager? es una herramienta para manejar la distribución de código Swift. Aprender Swift Package Manager es muy sencillo, está integrado con Xcode y podemos añadir código de otros developers para usarlo en nuestras apps. También podemos crear módulos y reusar en otras apps.
En lugar de usar Alamofire, usamos la URL del repositorio que acabamos de crear llamado LoginModule. Es decir, https://github.com/SwiftBeta/LoginModule

Después de crear un proyecto en Xcode de 0, y cambiar el deployment info a iOS 15. Si vamos a añadir una dependencia con Swift Package Manager, vemos lo siguiente:

Añadimos nuestro módulo creado con Swift Package Manager en un proyecto de Xcode nuevo
Añadimos nuestro módulo en un proyecto de Xcode nuevo

Le damos a añadir paquete

Integramos nuestro módulo con Swift Package Manager
Integramos nuestro módulo con Swift Package Manager y le damos a "Add Package"

Y al añadirlo, debemos ir a la struct que instancia ContentView e importar el módulo de LoginModule y borrar la ContentView de la app principal, ya que utilizaremos el ContentView del módulo LoginModule

Y el resultado es el siguiente:

Toda la pantalla y lógica de nuestro login ahora está en un módulo creado con Swift Package Manager
Toda la pantalla y lógica de nuestro login ahora está en un módulo creado con Swift Package Manager

Conclusión

Hoy hemos aprendido a crear un módulo con Swift Package Manager. Después de crearlo hemos aprendido a cómo subirlo a Github para que otros developers puedan usarlo y crear Pull Requests para añadir mejoras.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple


SPM