Cómo exponer una interfaz HTML en una app macOS.

Durante el desarrollo de una aplicación para macOS, me encontré con la necesidad de mostrar interfaces web personalizadas desde dentro de la misma app, sin tener que construir la UI en AppKit o SwiftUI. Esto por que, en lo que estoy trabajando, a veces no hay manera de acceder directamente a la interfaz principal de la app y contando con acceso remoto accedemos a esta interfaz ligera, aquí podemos ejecutar cambios o aplicar configuraciones directamente a la app principal.

Buscaba algo simple, rápido, que pudiera cargar recursos locales (HTML, CSS, JS) almacenados en el Bundle del proyecto y que no dependiera de frameworks pesados o desactualizados como GCDWebServer, que fue una librería super popular en su momento.

Buscando alternativas modernas, encontré dos herramientas que en conjunto me resolvieron todo:

  • FlyingFox: un microservidor HTTP escrito en Swift Concurrency, ideal para apps embebidas
  • swift-html: un DSL de HTML 100% en Swift, creado por los autores de Point-Free

Este post explica cómo integrarlas, cómo servir contenido estático correctamente desde el bundle de la app, y cómo construir la UI directamente desde Swift.

¿Por qué no usar SwiftUI?

Porque como comente anteriormente, muchas veces quiero mostrar una interfaz que no necesariamente sea a travez de la vista principal de la aplicación, y hacerlo en HTML me da flexibilidad. Además, si trabajo solo (como es mi caso) y quiero tener una forma rápida de crear una UI sin depender de diseño externo ni herramientas visuales, el DSL en Swift me permite todo desde código.

También me interesa que esta interfaz se pueda acceder remotamente, o simular un frontend web sin montar un servidor real.

Montar el servidor con FlyingFox

import FlyingFox

let server = HTTPServer(port: 8002)

try await server.appendRoute("GET /static/*", to: .directory(
subPath: "",
serverPath: "static"
))

Esto monta la carpeta static/ como base pública, y cualquier archivo dentro del Bundle será accesible desde /static/... aquí podemos limitar tanto como sea posible para no abrir innecesariamente la llave ve mas, usando carpetas físicas, esto es importante, ya que si usamos los grupos de Xcode, estos, no tendrán ningún efecto.

Generar el HTML usando Swift puro (DSL)

import Html

let document: Node = .document(
.html(
.body(
.h1("Bienvenido")
.p("Esta es una interfaz embebida usando HTML DSL en Swift."),
.img(src: "/static/images/background.jpg", alt: "Imagen de fondo"),
.script(src: "/static/js/app.js"),
.link([.rel(.stylesheet), .href("/static/css/style.css")])
)
)
)

let htmlString = render(document)
return HTTPResponse(html: htmlString)

Con esto, ya no necesito archivos .html, y todo se mantiene dentro del proyecto Swift. Ideal si trabajo sin equipo de frontend.

Qué pasa si no se carga algún recurso?

Esto me costó unos minutos entenderlo: el bundle no preserva carpetas virtuales como www/ o public/. Todos los archivos se colocan directamente en la capeta base del bundle. Por eso, serverPath: “”, así vacío, funciona, pero intentar usar “carpeta” como parte de la ruta no, a menos que si tengas una carpeta real creada.

Resultado

Con este setup puedo servir HTML generado desde código, acceder a mis recursos CSS, JS, imágenes y mostrarlo en cualquier WebView o navegador sin depender de ninguna otra librería pesada. También puedo modificar todo desde código Swift sin abrir editores HTML. Otra super ventaja es que el compilador me avisa si estoy escribiendo algo mal gracias al compilador y que la librería DSL esta “tipada” entonces veras errores al tiempo que lo escribes ya que la librería soporta totalmente esta comprobación, al estar construida puramente en Swift y al funcionar muy parecido a un lenguaje funcional como lo es SwiftUI.


¿Y si necesito usar templates HTML?

Podría agregarlos como archivos .html o .stencil y cargarlos desde el bundle. FlyingFox no tiene problemas con eso, pero como en mi caso prefiero mantenerlo todo embebido en Swift (por eficiencia y portabilidad), usar el DSL fue lo más práctico.


Conclusión

A veces, es necesario buscar soluciones fuera del contexto principal de las aplicaciones, el hecho de pensar que toda la interfaz debe construirse sobre un window, luego con views en SwiftUI o UIKit, etc, no siempre nos brinda la posibilidad de resolver todas las necesidades, en ocaciones, debemos echar mano de este tipo de soluciones, exponer contenido HTML directo en nuestro navegador para ampliar la gama de opciones y al final tener felices a los usuarios.

Deja un comentario