← Nesim Apps

Publicar una app de iPhone sin Mac: Kotlin Multiplatform, un Mac en la nube y TestFlight

Por Nesim Razon, desarrollador Android independiente · 2026-11-14

#Notas de desarrollo#iOS

Desarrollo en Windows y no tengo un Mac, pero quería mi pequeño juego Android 2048 en la App Store. Resulta que no hace falta un Mac en tu mesa, solo uno en la nube durante unos minutos por compilación. Este es el flujo que llevó una compilación de mi portátil a TestFlight y a la revisión de la App Store en un día, y las trampas que encontré por el camino.

La forma del proyecto

El juego ya estaba escrito en Kotlin con Jetpack Compose, así que lo pasé a Kotlin Multiplatform con Compose Multiplatform. Un módulo compartido contiene ahora las reglas del juego, las pantallas, la partida guardada, los textos y la fuente. La app Android es una carcasa fina a su alrededor, y una pequeña app en Swift es la carcasa de iOS.

Todo lo que difiere entre plataformas está detrás de una interfaz pequeña: anuncios de banner, clasificación, aviso para valorar, compartir. Android la implementa con los SDK que ya usaba. En iOS, el lado Swift la implementa con el SDK de Google Mobile Ads y Game Center. Un detalle más: con Android Gradle Plugin 9, el plugin de aplicación Android y el de Kotlin Multiplatform ya no pueden compartir módulo, así que el código compartido vive en su propio módulo de biblioteca.

Qué puede y qué no puede hacer Windows

Kotlin compila el código de iOS en Windows. Tareas como la compilación para el simulador de iOS se ejecutan en local e informan de los errores de Kotlin, así que pude revisar en mi mesa la mayor parte del código compartido y del Kotlin específico de iOS. Lo que Windows no puede hacer es enlazar el framework de iOS, compilar la app en Swift ni firmar nada. Eso necesita macOS.

El efecto práctico es que nunca espero a un Mac para encontrar un error de Kotlin, pero un error de Swift cuesta una vuelta de CI, porque escribo el lado Swift sin un compilador a mano. Por eso mantuve el código Swift pequeño.

Sin archivo de proyecto de Xcode en el repositorio

Sin Xcode no hay un archivo de proyecto que editar a mano, así que describo la app en un YAML corto y dejo que XcodeGen cree el proyecto en la máquina de compilación. El YAML contiene el identificador del paquete, los valores del Info.plist, el permiso de Game Center, los paquetes Swift de anuncios y un script previo que pide a Gradle compilar e incrustar el framework compartido.

Un YAML roto cuesta un arranque de compilación. El mío se rompió una vez, cuando una lista larga perdió un salto de línea y el trabajo falló en segundos, así que ahora analizo el archivo en local con un script pequeño antes de cada envío.

El Mac en la nube

La compilación corre en un runner de macOS de GitHub Actions. El trabajo descarga el código, prepara Java y la caché de Gradle, instala XcodeGen con Homebrew, genera el proyecto y ejecuta xcodebuild. En el runner que usé, el Xcode instalado era una versión reciente de la serie 26; importa porque subir a App Store Connect exige un SDK actual.

Un envío a la rama de iOS siempre ejecuta una compilación rápida y sin firmar para el simulador. Un mensaje de commit que contiene la palabra testflight activa en su lugar el otro trabajo, que archiva, firma y sube. Así un envío normal sale barato y yo decido cuándo va una compilación a Apple.

Firmar sin Mac

Sigue haciendo falta una membresía de Apple Developer, pero no un llavero en un Mac. En App Store Connect creas una clave de API y guardas el archivo de la clave, el ID de clave y el ID de emisor como secretos del repositorio. xcodebuild los acepta como argumentos junto con la opción que permite actualizar el aprovisionamiento, y Apple crea y gestiona en la nube los certificados y perfiles.

El paso de exportación usa un archivo de opciones con el método app-store-connect y el destino upload, de modo que el mismo comando que exporta el archivo también lo sube a TestFlight.

La única trampa real fue el rol de la clave. Una clave con el rol App Manager dejó pasar el paso de archivo, pero la exportación falló con un error de permiso de firma en la nube. Una clave nueva con el rol Admin lo arregló.

Cuánto cuesta

En un plan gratuito de GitHub el uso incluido se mide en dólares. En el momento de escribir, un minuto de macOS cuesta unas diez veces un minuto de Linux. Mi primer día completo de pruebas usó 54 minutos de macOS, algo más de tres dólares del crédito incluido, y una ejecución de TestFlight dura unos seis minutos.

Dos hábitos lo mantienen barato: ejecutar la compilación del simulador solo cuando no pedí subida y revisar los archivos en local antes de enviar. Si algún día se acaba el crédito incluido, las ejecuciones simplemente se detienen, así que en un plan sin método de pago no hay facturas sorpresa.

Pequeñas trampas que conviene conocer

Probar en un teléfono real

La compilación funciona en un iPhone 7 con iOS 15.8 a través de TestFlight. Con fijar el destino mínimo en iOS 15 bastó, y un teléfono antiguo es un buen dispositivo de pruebas de gama baja. Tras unas cuantas vueltas, el juego se comportaba igual que en Android, con anuncios y clasificación funcionando desde el lado Swift.

Preguntas frecuentes

¿Necesito un Mac para publicar una app en la App Store?

No en tu mesa. Necesitas una máquina Mac para compilar, pero un runner en la nube como el de macOS de GitHub Actions hace ese trabajo. Sigues necesitando una membresía de Apple Developer.

¿Puede Windows compilar código Kotlin para iOS?

Sí en la parte de Kotlin: los destinos de iOS se compilan en Windows. Enlazar el framework, compilar la app en Swift y firmar necesitan macOS.

¿Cómo se firma la app sin Mac?

Con una clave de API de App Store Connect guardada como secreto. Xcode en el runner de la nube la usa para la firma gestionada en la nube y sube directamente a TestFlight.

¿Cuánto tarda una compilación en la nube?

Una ejecución de TestFlight tardó unos seis minutos en mi configuración, algo más la primera vez, cuando no había nada en caché.

Más del blog