← Nesim Apps

Publicar um app de iPhone sem Mac: Kotlin Multiplatform, um Mac na nuvem e TestFlight

Por Nesim Razon, desenvolvedor Android independente · 2026-11-14

#Notas de desenvolvimento#iOS

Eu desenvolvo no Windows e não tenho um Mac, mas queria meu pequeno jogo Android 2048 na App Store. Acontece que você não precisa de um Mac na sua mesa, só de um na nuvem por alguns minutos a cada compilação. Este é o fluxo que levou uma compilação do meu notebook ao TestFlight e à revisão da App Store em um dia, e as armadilhas que encontrei no caminho.

O formato do projeto

O jogo já estava escrito em Kotlin com Jetpack Compose, então passei para Kotlin Multiplatform com Compose Multiplatform. Um módulo compartilhado guarda agora as regras do jogo, as telas, o jogo salvo, os textos e a fonte. O app Android é uma casca fina em volta dele, e um pequeno app em Swift é a casca do iOS.

Tudo o que difere entre as plataformas fica atrás de uma interface pequena: anúncios de banner, classificação, pedido de avaliação, compartilhamento. O Android a implementa com os SDKs que já usava. No iOS, o lado Swift a implementa com o SDK do Google Mobile Ads e o Game Center. Mais um detalhe: com o Android Gradle Plugin 9, o plugin de aplicativo Android e o de Kotlin Multiplatform não podem mais dividir o mesmo módulo, então o código compartilhado vive em seu próprio módulo de biblioteca.

O que o Windows pode e não pode fazer

O Kotlin compila o código de iOS no Windows. Tarefas como a compilação para o simulador de iOS rodam localmente e informam os erros de Kotlin, então consegui conferir na minha mesa a maior parte do código compartilhado e do Kotlin específico de iOS. O que o Windows não faz é vincular o framework de iOS, compilar o app em Swift nem assinar nada. Isso exige macOS.

O efeito prático é que nunca espero um Mac para achar um erro de Kotlin, mas um erro de Swift custa uma rodada de CI, porque escrevo o lado Swift sem compilador à mão. Por isso mantive o código Swift pequeno.

Sem arquivo de projeto do Xcode no repositório

Sem o Xcode não há arquivo de projeto para editar à mão, então descrevo o app em um YAML curto e deixo o XcodeGen criar o projeto na máquina de compilação. O YAML tem o identificador do pacote, os valores do Info.plist, a permissão do Game Center, os pacotes Swift de anúncios e um script prévio que pede ao Gradle para compilar e embutir o framework compartilhado.

Um YAML quebrado custa o início de uma compilação. O meu quebrou uma vez, quando uma lista longa perdeu uma quebra de linha e o trabalho falhou em segundos, então agora analiso o arquivo localmente com um script pequeno antes de cada envio.

O Mac na nuvem

A compilação roda em um runner de macOS do GitHub Actions. O trabalho baixa o código, prepara o Java e o cache do Gradle, instala o XcodeGen com o Homebrew, gera o projeto e executa o xcodebuild. No runner que usei, o Xcode instalado era uma versão recente da série 26; isso importa porque o envio ao App Store Connect exige um SDK atual.

Um envio para o branch do iOS sempre roda uma compilação rápida, sem assinatura, para o simulador. Uma mensagem de commit com a palavra testflight ativa em vez disso o outro trabalho, que arquiva, assina e envia. Assim um envio normal sai barato e eu decido quando uma compilação vai para a Apple.

Assinar sem Mac

Ainda é preciso uma assinatura do Apple Developer, mas não um chaveiro em um Mac. No App Store Connect você cria uma chave de API e guarda o arquivo da chave, o ID da chave e o ID do emissor como segredos do repositório. O xcodebuild os aceita como argumentos junto com a opção que permite atualizar o provisionamento, e a Apple cria e gerencia na nuvem os certificados e perfis.

A etapa de exportação usa um arquivo de opções com o método app-store-connect e o destino upload, de modo que o mesmo comando que exporta o arquivo também o envia ao TestFlight.

A única armadilha real foi o papel da chave. Uma chave com o papel App Manager deixou a etapa de arquivo passar, mas a exportação falhou com um erro de permissão de assinatura na nuvem. Uma chave nova com o papel Admin resolveu.

Quanto custa

Em um plano gratuito do GitHub o uso incluído é medido em dólares. Na hora em que escrevo, um minuto de macOS custa cerca de dez vezes um minuto de Linux. Meu primeiro dia completo de testes usou 54 minutos de macOS, um pouco mais de três dólares do crédito incluído, e uma execução do TestFlight leva uns seis minutos.

Dois hábitos mantêm o custo baixo: rodar a compilação do simulador só quando não pedi envio e conferir os arquivos localmente antes de enviar. Se o crédito incluído acabar, as execuções simplesmente param, então em um plano sem forma de pagamento não há fatura surpresa.

Pequenas armadilhas que vale conhecer

Testar em um celular de verdade

A compilação roda em um iPhone 7 com iOS 15.8 pelo TestFlight. Definir o destino mínimo como iOS 15 foi suficiente, e um celular antigo é um bom aparelho de teste de baixo desempenho. Depois de algumas rodadas, o jogo se comportava como no Android, com anúncios e classificação funcionando pelo lado Swift.

Perguntas frequentes

Preciso de um Mac para publicar um app na App Store?

Não na sua mesa. Você precisa de uma máquina Mac para compilar, mas um runner na nuvem como o de macOS do GitHub Actions faz esse trabalho. Você ainda precisa de uma assinatura do Apple Developer.

O Windows consegue compilar código Kotlin para iOS?

Sim, na parte de Kotlin: os destinos de iOS compilam no Windows. Vincular o framework, compilar o app em Swift e assinar exigem macOS.

Como o app é assinado sem Mac?

Com uma chave de API do App Store Connect guardada como segredo. O Xcode no runner da nuvem a usa para a assinatura gerenciada na nuvem e envia direto ao TestFlight.

Quanto tempo leva uma compilação na nuvem?

Uma execução do TestFlight levou cerca de seis minutos na minha configuração, um pouco mais na primeira vez, quando nada estava em cache.

Mais do blog