Cómo exponer una interfaz HTML en una app macOS
FlyingFox + swift-html: cómo montar un servidor HTTP embebido en una app macOS para servir interfaces web desde el Bundle, sin frameworks pesados.
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. 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 para 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.
Buscando alternativas modernas, encontré dos herramientas que en conjunto me resolvieron todo:
FlyingFox: un microservidor HTTP escrito en Swift Concurrency, ideal para apps embebidasswift-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?
Muchas veces quiero mostrar una interfaz que no necesariamente sea a través 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 el acceso. Esto es importante: si usamos los grupos de Xcode, no tendrán ningún efecto — hay que usar carpetas físicas.
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 carpeta base del bundle. Por eso, serverPath: "" así vacío funciona, pero intentar usar “carpeta” como parte de la ruta no, a menos que tengas una carpeta real creada físicamente en el proyecto.
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 ventaja enorme: el compilador me avisa si estoy escribiendo algo mal, gracias a que la librería DSL está tipada y funciona muy parecido a un lenguaje funcional como 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, con views en SwiftUI o UIKit, no siempre nos brinda la posibilidad de resolver todas las necesidades. En ocasiones, debemos echar mano de este tipo de soluciones — exponer contenido HTML directamente en el navegador para ampliar la gama de opciones y al final tener felices a los usuarios.