Command line tool en Swift usando la API de la NASA
Command line tool en Swift usando la API de la NASA

Crea tu primera Swift app de línea de comandos y ejecútala en tu terminal

Por increíble que parezca, también podemos crear apps en Swift que se ejecuten desde nuestro terminal. Estás apps se llaman Command Line Tools y podemos crear auténticas maravillas y automatizar procesos.

SwiftBeta

Tabla de contenido

Crea tu primera app en Swift y ejecútala en un Terminal

Hoy en SwiftBeta vamos a crear nuestra primera command line tool. Esto no es más que crear un programa que se ejecute desde nuestro terminal. Es tremendamente potente y no sé por que no te lo he explicado antes. El programa que vamos a crear lo podemos llamar desde Shortcuts.app (la app de atajos de macOS) y verás que podemos automatizar procesos.

En el video de hoy vamos a aprender a crear un programa que se ejecute desde cualquier terminal. En este caso vamos a llamar a la API de la NASA para extaer información. Este será un ejemplo, pero tu podrías utilizar otras APIs como la de Twitter, Github, etc para automatizar procesos. Al final del video aprenderás a usar tu app de terminal para añadirla a un shortcut y así descargar la foto del día de la NASA (esto último lo veremos al final del video).

Vamos a crear nuestra app en Xcode

Lo primero de todo que vamos hacer es abrir nuestro terminal. En mi caso voy a abrir iTerm. Y esta vez vamos a crear nuestro proyecto usando un comando que tampoco hemos visto en el canal hasta ahora.

Al abrir el terminal, nos vamos al escritorio y creamos la carpeta de nuestro proyecto:

mkdir NasaApp && cd NasaApp
Comando para crear la carpeta NasaApp y acceder a ella

Dentro de la carpeta NasaApp, ejecutamos el siguiente comando:

swift package init --type executable
Creamos nuestra Command Line Tool

El resultado que te aparecerá por consola, será parecido a:

Contenido de ficheros que aparecen al crear nuetra app desde el terminal
Contenido de ficheros que aparecen al crear nuetra app desde el terminal

Acabamos de crear nuestro proyecto. Ahora vamos a abrir Xcode. Para hacerlo, escribe el siguiente comando en la terminal:

open Package.swift

De esta manera se te abrirá Xcode y podrás empezar a añadir código:

Contenido del fichero Package.swift
Contenido del fichero Package.swift
Si ves que se demora demasiado, cierra Xcode y vuélvelo a abrir

Antes de ponernos a programar, es importante tener acceso a la API de la NASA. Solo hay que hacer un paso y ya tendremos una token que enviaremos en cada petición HTTP.

Tan solo tienes que ir al siguiente enlace https://api.nasa.gov

NASA Open APIs

Una vez dentro, rellena el formulario que aparece. Al hacerlo tendrás tu token.

Accedemos a la web de la api de la Nasa para crear nuestro Token
Accedemos a la web de la api de la Nasa para crear nuestro Token

En mi caso, este es el token que me han asignado, y también aparece una URL donde lo podemos probar.

Una vez obtenido el token podemos acceder a los recursos de la API de la Nasa
Una vez obtenido el token podemos acceder a los recursos de la API de la Nasa

Si clickamos en esa URL, vemos el siguiente resultado (quizás tu ves otro resultado ya que llamar esta API suele dar un resultado diferente cada día.):

Al acceder a un endpoint de la API de la Nasa obserbamos el JSON recibido
Al acceder a un endpoint de la API de la Nasa obserbamos el JSON recibido

Fíjate bien en el JSON que recibimos al realizar una petición HTTP ya que vamos a transformar ese JSON a un modelo de nuestro dominio dentro de nuestra app (Como ya hemos visto muchas veces en el canal).

Copiamos el JSON y vamos a Xcode. En este caso, vamos a crear un modelo muy sencillo:

struct NasaModel: Decodable {
    let title: String
    let url: String
}
Struct que conforma Decodable para transformar el JSON a este modelo

Solo queremos extraer el title y la url. Lo siguiente que vamos hacer es crear la petición HTTP y mapear el JSON a un modelo de nuestro dominio. Esto también lo hemos visto mucho en el canal:

let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=n3Xy6XEZQIpLubchGiZCNTc86IqFyQSYt45LJzxb")!
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let model = try JSONDecoder().decode(NasaModel.self, from: data)
            print(model)
        } catch {
            print(error.localizedDescription)
        }
Petición HTTP y mapeo del JSON al modelo de nuestro dominio

Todo este código lo vamos a meter dentro de una struct llamada CLI y con la keyword @main:

@main
struct CLI {
    static func main() async {
        let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=n3Xy6XEZQIpLubchGiZCNTc86IqFyQSYt45LJzxb")!
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let model = try JSONDecoder().decode(NasaModel.self, from: data)
            print(model)
        } catch {
            print(error.localizedDescription)
        }
    }
}
Añadimo punto de entrada de nuestra app y creamos la Struct CLI

Si ahora compilamos el código, obtenemos el siguiente error:

'main' attribute cannot be used in a module that contains top-level code
Primer error que vamos a corregir dentro de Xcode

Para arreglarlo, vamos a renombrar el fichero de main a cli. Volvemos a compilar.  vemos que ahora obtenemos otro error, indicándonos que no podemos usar la API de async/await. Lo vamos a arreglar especificando la versión mínima que debe tener macOS para poder instalar esta app.

Dentro de Package.swift, añadimos platforms y especificamos la versión de macOS v12.

let package = Package(
    name: "NasaApp",
    platforms: [.macOS(.v12)],
Segundo error que corregimos dentro del fichero Package.swift

Una vez hecho si compilamos, vemos que todo funciona perfectamente. Se está printando por consola el mensaje con la información recibida de nuestra petición HTTP.

Ahora vamos a coger la URL de nuestro modelo, y vamos a descargar la imagen del día de la NASA, esta imagen se guardará en una de las carpetas de nuetro PC. para hacerlo, vamos a añadir el siguiente código:

@main
struct CLI {
    static func main() async {
        let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=n3Xy6XEZQIpLubchGiZCNTc86IqFyQSYt45LJzxb")!
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let model = try JSONDecoder().decode(NasaModel.self, from: data)
            
            let url = URL(string: model.url)!
            let (fileDownloadedURL, _) = try await URLSession.shared.download(from: url)
            print(fileDownloadedURL)
        } catch {
            print(error.localizedDescription)
        }
    }
}
Código final que podemos probar compilando Xcode, abriremos la ruta de la imagen descargada 

Es importante añadir el print, para saber exactamente dónde está la imagen que acabamos de descargar. Si compilamos, vemos por consola que aparece una ruta, vamos a navegar hasta allí, abrimo el Finder y vamos a la siguiente opción:

Vamos al Finder para navegar a la ruta que nos ha mostrado la consola de Xcode
Vamos al Finder para navegar a la ruta que nos ha mostrado la consola de Xcode

Aquí vamos a pegar la URL que aparece por pantalla, en mi caso es esta de aquí:

file:///var/folders/p8/t27s1wpx4nqfy1vlm4mdrwz40000gq/T/CFNetworkDownload_ffKJ5r.tmp
Ruta de la imagen descargada

Y si la abrimos, vemos que es la imagen que hemos descargado. La ruta de la imagen descargada podríamos especificar un path, es decir, que se guardara en alguna carpeta de nuestro PC más acorde a lo que estamos haciendo, por ejemplo, guardar la imagen en la carpeta downloads.

Crear comando para usar en el terminal

Esto está muy bien, pero imagina que quieres ejecutar este código desde la terminal, como si fuese un comando. Para hacerlo debes ejecutar los siguientes comandos desde tu terminal.

  1. Preparamos para release
swift build --configuration release
Comando para generar una build de nuestro Command Line Tool
Build generado correctamente
Build generado correctamente

2. Copiamos el ejecutable en la ruta /usr/local/bin/

cd .build/release/NasaApp /usr/local/bin/nasa
Copiamos el ejecutable en la carpeta del sistema /usr/local/bin
Resultado al ejecutar el último comando
Resultado al ejecutar el último comando

Fíjate que aunque la build se llama NasaApp, para simplificar mi comando, lo he guardado como nasa. Esto significa que puedo ir a mi terminal ahora mismo y poner nasa.

Vamos a ver qué ocurre:

Al ejecutar el comando obtenemos la información del print de nuestra app
Al ejecutar el comando obtenemos la información del print de nuestra app
⚠️ Para los que habéis visto el video en Youtube, primero he realizado la prueba para mostrar la ruta de dónde se descarga la imagen y luego he creado otra build para mostrar el valor de NasaModel

Acabamos de crear nuestro primer comando en Swift para usar desde la terminal, y hemos realizado una petición HTTP. Tu comando podría realizar cualquier lógica, en este caso, al ser nuestro primer comando lo hemos simplificado realizando una acción, pero podríamos pasarle parámetros (esto lo veremos más adelante).

Ahora lo que vamos hacer es usar este nuevo comando en la app Shortcuts.app de macOS. Queremos automatizar un proceso para descargar la imagen que viene en la URL de nuestro modelo NasaModel.

Automatizar nuestra app de terminal en Shortcuts.app

Vamos abrir la app de Shortcuts.app (atajos del mac), y una vez abierta la app pulsamos el button para crear un nuevo atajo:

App Shortcuts.app de macOS
App Shortcuts.app de macOS

Al crearlo, vamos a llamarlo SwiftBeta Shortcut y vamos a añadir nuestro primera paso del Workflow que vamos a crear. Este paso, es de la categoria Scripting, el que se llama Run Shell Script. Al hacerlo por primera vez nos aparece un mensaje que debemos darle permisos. Abrimos las preferencias del sistema y lo hacemos.

Añadimo nuestro primer step para ejecutar nuestra app de la línea de comandos
Añadimo nuestro primer step para ejecutar nuestra app de la línea de comandos

Damos al Button Open Preferences

Aceptamos estos permisos para poder ejecutar nuestra app del terminal
Aceptamos estos permisos para poder ejecutar nuestra app del terminal

Ahora volvemos a nuestro Shortcut. Y eliminamos el echo "Hello World" y los sustituimos por nuestro nuevo comando nasa. Vamos a añadir dos steps más:

  • Guardar en una variable el resultado obtenido por nuestro comando nasa
  • Mostrar el valor que acabamos de almacenar en la variable
Añadimos diferentes steps dentro de nuestro Shortcut
Añadimos diferentes steps dentro de nuestro Shortcut

Si lo ejecutamos vemos el mismo print que veíamos por consola. Vamos a hacer una pequeña mejora a nuestro código, para ello volvemos a Xcode.

Vamos a modificar nuestra app, ahora retornará el JSON que ha recibido, directamente, de esta manera podremos recuperar las keys que queramos desde nuestro Shortcut:

@main
struct CLI {
    static func main() async {
        let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=n3Xy6XEZQIpLubchGiZCNTc86IqFyQSYt45LJzxb")!
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            //let model = try JSONDecoder().decode(NasaModel.self, from: data)
            print(String(data: data, encoding: .utf8)!)
        } catch {
            print(error.localizedDescription)
        }
    }
}
Modificamos nuestra app para que muestre todo el JSON recibido de la petición HTTP 

Ahora volvemos a ejecutar estos dos comandos:

Borramos y volvemos a generar la build y movemos el nuevo ejecutable a la carpeta del sistema /usr/local/bin
Borramos y volvemos a generar la build y movemos el nuevo ejecutable a la carpeta del sistema /usr/local/bin

Si tienes algún problema, puedes borrar el comando y volver a realizar los pasos anteriores. Para borrarlo tan solo debes ejecutar el siguiente comando:

rm /usr/local/bin/nasa
Comando para borrar nuestra app

Al ejecutar el shortcut, vemos que aparece la siguiente información:

Seguimos añadiendo steps para recuperar la url de la petición HTTP
Seguimos añadiendo steps para recuperar la url de la petición HTTP

Ahora lo único que tenemos que hacer es acceder a las Keys del JSON que queremos. En nuestro caso vamos a escoger la key URL. Y vamos a usar una serie de steps para descargar la imagen JPG.

Al final nuestro Shortcut queda de la siguiente manera:

Una vez añadidos todos los steps necesarios, la imagen se descarga correctamente en el directorio que hemos especificado
Una vez añadidos todos los steps necesarios, la imagen se descarga correctamente en el directorio que hemos especificado

Y si vamos a nuestro escritorio obtenemos la siguiente imagen:

Imagen descargar de la API de la NASA
Imagen descargar de la API de la NASA

Nosotros cada día podríamos obtener una imagen diferente al ejecutar nuestro shortcut. Incluso podemos ir al terminal y ejecutar el shortcut que acabamos de crear.

shortcuts run "SwiftBeta Shortcut"
Podemos ejecutar shortcuts desde nuestro terminal

Conclusión

Hoy hemos aprendido a crear nuestra primera app en Swift para usarla desde el terminal. Lo que hemos hecho ha sido crear una petición HTTP a la API de la NASA para extraer información. A continuación, hemos usado esta app para añadirla a una automatización y extraer la imagen del día. Podríamos realizar esta petición directamente desde la app de Shortcuts, pero quería mostrarte que se pueden integrar apps de la linea de comandos en tus shortcuts.

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