wandres.dev
MÁS ALLÁ DEL IPHONE · iPad, Mac, watch, visionOS

Mac: Catalyst frente a SwiftUI nativo y las expectativas del escritorio

Llevar una app al Mac ofrece dos caminos con costes y techos muy distintos: una capa de compatibilidad que traduce el vocabulario de iOS y una recompilación nativa que habla el idioma del escritorio. Esta lección compara ambos con criterio, y después examina lo que ninguno de los dos regala: menús que son la documentación de tu app, ventanas que son estado, teclado que es camino principal y una noción del error que el escritorio no perdona.

⏱ 19 min

Hay una prueba informal que los usuarios de Mac aplican en los primeros treinta segundos y que casi nadie escribe en una especificación: abren el menú y miran. No buscan una función concreta; comprueban si la app tiene una gramática. Un menú bien construido es simultáneamente el índice de las capacidades del programa, su manual de atajos y la promesa de que alguien pensó en quien trabaja ocho horas seguidas con teclado. Un menú con cinco elementos genéricos heredados de la plantilla dice lo contrario con la misma claridad, y lo dice antes de que la persona haya usado una sola función. El escritorio tiene expectativas, y son antiguas, específicas y perfectamente conocidas por quien va a juzgarte.

🎯 Al terminar esta lección sabrás
  • Elegir entre Catalyst y SwiftUI nativo con criterios de techo, coste y horizonte.
  • Construir menús como estructura de comandos y no como decoración obligatoria.
  • Modelar ventanas y escenas como estado de la app, no como efectos de una acción.
  • Reconocer las expectativas del escritorio que ninguna capa de compatibilidad satisface.

Dos caminos con techos distintos

Catalyst toma tu app de iPad y la ejecuta sobre macOS traduciendo el vocabulario de iOS al del escritorio: los controles se redibujan con métrica de Mac, los gestos se convierten en clics, la barra de navegación se transforma en barra de herramientas. Su virtud es la economía brutal del punto de partida, porque una app existente arranca en el Mac con un cambio de destino de compilación y unas semanas de pulido. Su límite es igual de real: partes de un modelo mental de tableta y lo traduces, de modo que todo lo que el escritorio espera y la tableta no tiene —inspectores, ventanas múltiples, un menú profundo, comportamientos de teclado finos— llega como añadido y no como sustrato.

SwiftUI nativo recorre el camino contrario. Compilas la misma declaración de interfaz para macOS y el sistema la materializa con los controles reales del escritorio, sin capa intermedia. El techo es más alto y la afinidad con el sistema es mayor, pero el coste no es cero: la misma declaración produce resultados distintos en cada plataforma y las diferencias aparecen justo donde el diseño era más específico.

🚀

Coste inicial

Catalyst arranca en días; nativo exige revisar cada pantalla. Si tu app existe y funciona, la diferencia inicial es de un orden de magnitud.

🏔️

Techo alcanzable

Nativo llega a inspectores, barras de herramientas configurables y comportamientos de ventana que en Catalyst requieren rodeos o son inalcanzables.

🧩

Superficie de API

Catalyst te da el catálogo de iOS con adaptaciones; nativo te da además lo específico de macOS, con el precio de comprobar en ambos lados.

🧭

Horizonte

Nativo es también el camino hacia otras plataformas de la familia. Catalyst resuelve el Mac y solo el Mac.

El criterio de elección no es ideológico. Si tienes una app de iPad madura, con inversión hecha en UIKit y una necesidad de estar en el Mac este trimestre, Catalyst es la respuesta correcta y sostener lo contrario es doctrina. Si estás empezando, si tu interfaz ya está escrita en SwiftUI o si el Mac va a ser un ciudadano de primera con funciones propias, nativo compensa desde el principio. El error caro no es elegir mal: es elegir Catalyst como atajo y después intentar alcanzar el techo de lo nativo a base de rodeos, pagando dos veces.

⚠️
Lo que ninguna de las dos vías te regala

Ni Catalyst ni una recompilación nativa convierten sola una app de tableta en una app de escritorio. Ambas resuelven el renderizado y el empaquetado; ninguna decide qué va en el menú, cuántas ventanas tiene sentido abrir, qué se puede seleccionar con la tecla de mayúsculas o qué ocurre al cerrar la última ventana. Eso es diseño de producto, y es exactamente el trabajo que distingue una app portada de una app que pertenece.

El menú es la estructura, no el adorno

En el escritorio la barra de menús cumple una función que no tiene equivalente en el teléfono: es el inventario completo y jerárquico de lo que la app puede hacer. Quien no encuentra un botón lo busca ahí, y si no está concluye que la función no existe. Por eso construir el menú no consiste en rellenar plantillas, sino en enumerar los comandos de tu app y darles nombre, lugar y atajo.

@main
struct MiApp: App {
    var body: some Scene {
        WindowGroup { Principal() }
            .commands {
                CommandGroup(replacing: .newItem) {
                    Button("Nueva nota", action: crearNota)
                        .keyboardShortcut("n", modifiers: .command)
                }
                CommandMenu("Nota") {
                    Button("Archivar", action: archivar)
                        .keyboardShortcut("e", modifiers: [.command, .shift])
                    Divider()
                    Button("Exportar…", action: exportar)
                }
            }
    }
}

Dos matices separan un menú correcto de uno merecido. El primero es la disponibilidad: un elemento que no aplica debe aparecer atenuado, no desaparecer, porque su presencia enseña que la función existe y su atenuación explica que ahora no procede. El segundo es la coincidencia exacta entre lo que ofrece el menú y lo que ofrece el resto de la interfaz: si una acción está en un menú contextual pero no en la barra, quien la aprendió por el menú contextual no podrá enseñársela a otra persona, y quien buscó en la barra concluirá que no existe.

💡
Un solo origen para cada comando

La forma que mejor envejece consiste en definir cada acción una vez, en el modelo, y exponerla desde tres sitios: el menú, la barra de herramientas y el menú contextual. Cuando la lógica de disponibilidad vive en un único lugar, la coherencia deja de depender de la disciplina de quien edita y pasa a ser una propiedad del código.

Las ventanas son estado

En iOS hay una pantalla y la app la ocupa. En macOS hay tantas ventanas como la persona decida abrir, cada una con su tamaño, su posición y su contenido, y todas ellas se restauran al reabrir la app. Esto obliga a un desplazamiento conceptual que suele ser el más difícil de la migración: la ventana deja de ser un contenedor implícito y pasa a ser una escena declarada, con su propio ciclo y su propio estado.

var body: some Scene {
    WindowGroup { Principal() }                    // varias instancias, una por documento

    Window("Actividad", id: "actividad") {         // singleton: una sola, reutilizable
        PanelActividad()
    }
    .defaultSize(width: 420, height: 320)

    Settings { Preferencias() }                    // el lugar canonico de los ajustes
}

La distinción entre un grupo de ventanas y una ventana única no es un detalle de sintaxis: expresa si el contenido es plural —muchos documentos abiertos a la vez— o singular —un panel de actividad que no tiene sentido duplicar—. Elegir mal produce una app que abre catorce ventanas idénticas de preferencias, un síntoma inconfundible de que alguien trató la ventana como un efecto de pulsar un botón en lugar de como una entidad con identidad propia.

flowchart TB
a[Estado de la app] --> b[WindowGroup: documentos abiertos]
a --> c[Window unica: panel auxiliar]
a --> d[Settings: ajustes canonicos]
b --> e[Restauracion al reabrir]
c --> e
e --> f[La persona reencuentra su sesion]

Del mismo desplazamiento se sigue una cuestión que el teléfono nunca plantea: qué ocurre al cerrar la última ventana. En el escritorio cerrar no es salir, y una app que termina al cerrar su única ventana contradice una convención de cuarenta años. La respuesta correcta depende del tipo de app —una utilidad puede terminar, un editor debe permanecer—, pero la respuesta que nunca es correcta consiste en no haberse hecho la pregunta.

Lo que espera quien lleva décadas en el escritorio

Más allá de menús y ventanas hay un conjunto de comportamientos que en el teléfono son refinamientos opcionales y aquí forman el suelo. Conviene enumerarlos porque su ausencia se percibe de inmediato aunque casi nadie sepa nombrarla.

El teclado es el camino principal, no un atajo. Eso implica que la tecla de tabulación recorre los controles en un orden sensato, que la tecla de retorno confirma el botón por defecto, que la de escape cancela, y que la selección múltiple responde a mayúsculas para rangos y a comando para adiciones sueltas. El puntero también tiene gramática: el cursor cambia de forma sobre lo que se puede redimensionar, el clic derecho abre un menú contextual pertinente, y pasar por encima de un elemento produce realimentación antes de pulsar.

List(notas, selection: $seleccionadas) { nota in     // seleccion multiple, no una
    FilaNota(nota)
        .contextMenu {                               // clic derecho pertinente
            Button("Duplicar", action: { duplicar(nota) })
            Button("Eliminar", role: .destructive, action: { borrar(nota) })
        }
}
.onDeleteCommand { borrarSeleccionadas() }           // la tecla de borrado funciona

Y hay una expectativa final, más profunda que las anteriores, que tiene que ver con la reversibilidad. En el escritorio la persona trabaja con documentos que le importan durante horas, y la convención de deshacer y rehacer es una garantía casi contractual. Una app que ofrece confirmaciones modales donde el Mac espera un deshacer no es solo más lenta de usar: transmite que no confía en el usuario, lo cual en esta plataforma es una descortesía perceptible.

ℹ️
Comprobaciones que solo existen aquí

Antes de dar por terminada una app de Mac conviene verificar cuatro cosas que el teléfono nunca pide: que la ventana se restaura con su tamaño y posición al reabrir, que redimensionarla hasta el mínimo no rompe el diseño, que la app se comporta con dos ventanas del mismo documento abiertas a la vez, y que todo lo accesible con ratón lo es también sin soltar el teclado.

Portar es traducir, y toda traducción decide qué se conserva y qué se pierde

La pregunta sobre Catalyst frente a nativo se plantea casi siempre como una comparación de herramientas, y por eso casi siempre se responde mal. La pregunta real es de qué idioma a qué idioma estás traduciendo, y qué considera intraducible cada camino. Catalyst traduce el vocabulario de la tableta al del escritorio conservando la sintaxis original: las frases resultantes son correctas y comprensibles, y aun así un hablante nativo detecta el acento en el primer párrafo. Recompilar nativo permite reescribir la sintaxis, pero solo permite; no obliga. Y ahí está el hallazgo que suele llegar demasiado tarde en los proyectos: una app escrita en SwiftUI nativo con mentalidad de teléfono —una sola ventana, sin menús, con confirmaciones en vez de deshacer, con el teclado como accesorio— se siente tan extranjera como cualquier Catalyst, porque el acento no estaba en el compilador sino en el modelo mental. La elección técnica fija tu techo; la elección de diseño determina a qué altura te quedas por debajo de él. Y la parte incómoda es que el techo se puede subir después contratando ingeniería, mientras que el modelo mental solo cambia cuando alguien del equipo usa el Mac como lo usan quienes lo eligieron: con las dos manos en el teclado, con seis ventanas abiertas y con la expectativa, formada durante décadas, de que todo error se deshace.

📝
Lo esencial

Catalyst da velocidad inicial con un techo definido; SwiftUI nativo cuesta más y llega más lejos, y es también el camino a las demás plataformas. Ninguno de los dos decide por ti el menú, las ventanas ni el comportamiento del teclado. El menú es el inventario jerárquico de tus comandos, con elementos atenuados en vez de ausentes. Las ventanas son escenas declaradas con identidad, plurales o únicas según el contenido, y se restauran. El teclado es camino principal, la selección es múltiple, el clic derecho es pertinente y el error se deshace en lugar de confirmarse.

⚔️ Aprobar la prueba de los treinta segundos
  1. Enumera todos los comandos de tu app en una lista plana y agrúpalos en menús con nombre, orden y atajo, marcando cuáles deben atenuarse y bajo qué condición.
  2. Declara al menos dos tipos de escena: un grupo para el contenido plural y una ventana única para un panel auxiliar, y comprueba que la segunda no se duplica.
  3. Cierra la última ventana y decide de forma explícita si la app debe terminar; documenta la decisión y su motivo.
  4. Recorre una tarea completa sin tocar el ratón y anota cada punto donde el foco se pierde o el orden de tabulación sorprende.
  5. Sustituye una confirmación modal destructiva por una acción reversible con deshacer y compara ambas experiencias con alguien que use Mac a diario.