Email me

Traduzido do original em inglês de 24 de outubro de 2026. Os números e as fontes são os mesmos do original. Leia o original em inglês →

Dev · · 10 min de leitura · Matt Busel

Arquivado em iOS e Swift Desenvolvimento de apps Guias

Crie um app iOS no Windows: como 21 apps foram publicados sem Mac

Crie um app iOS no Windows: como 21 apps foram publicados sem Mac

Resumo: Dá para criar um app iOS no Windows se você alugar a parte do Mac por minuto. 21 apps de iPhone saíram de um PC com Windows para o App Store Connect desse jeito, sem nenhum Mac na mesa. O primeiro app levou 13 execuções de CI com falha. Os últimos dez levaram zero. Aqui está o pipeline inteiro, o código e cada armadilha que custou uma execução.

Primeiro a resposta curta, porque foi isso que você pesquisou. Sim, dá para criar e publicar um app de iPhone a partir do Windows. Você escreve o Swift no seu PC, e uma máquina macOS que você nunca toca faz a compilação, a assinatura e o upload. Essa máquina é um runner macOS do GitHub Actions, e num repositório público o GitHub não cobra por ele.

A pegadinha honesta: a Apple ainda precisa de um Mac em algum lugar, porque o Xcode só roda no macOS. Você também precisa de uma assinatura paga do Apple Developer Program ($99 por ano). Todo o resto, inclusive o certificado de assinatura que a maioria dos guias diz exigir o Keychain Access, pode ser feito num terminal do Windows pela API do App Store Connect.

Entre 10 de setembro e 8 de outubro de 2026, este pipeline rodou 139 builds para a App Store em 21 repositórios de apps. Cada um dos 21 apps tem um upload no log de CI, e 8 estão no ar na App Store dos EUA em 16 de outubro. Veja como funciona, e onde quebrou.

Diagrama animado do pipeline: um PC com Windows roda gh workflow run, um runner macOS do GitHub escolhe o Xcode 26 e compila em 52 segundos, importa o certificado de assinatura em 1 segundo, depois o fastlane faz o build e o upload em 2 minutos e 31 segundos, 4 minutos e 22 segundos no total.
Como o Chain 1.2 foi de um único comando no Windows ao App Store Connect em 8 de out. de 2026. Tempos de cada etapa tirados do log da execução no GitHub Actions.

O que você precisa antes de começar

  • Um PC com Windows com git, Python e a GitHub CLI. Qualquer editor. Um agente de programação ajuda muito, já que você não vai poder clicar pelo Xcode de qualquer jeito.
  • Uma conta paga de Apple Developer.
  • Um repositório no GitHub. Público significa minutos de macOS grátis. Privado também funciona, só custa dinheiro (números abaixo).

Duas coisas que só um humano pode fazer, uma vez cada:

  1. Criar uma chave de API do App Store Connect (Users and Access, Integrations). Você recebe um arquivo .p8, um key ID e um issuer ID. Isso vai para os secrets do GitHub e para nenhum outro lugar.
  2. Criar o registro do app no App Store Connect. A API consegue ler e editar apps, mas o nosso POST /v1/apps voltou com 403, então essa parte é um clique na interface web. Todos os outros campos da página da loja (nome, subtítulo, descrição, palavras-chave, capturas de tela) são feitos por script.

Passo 1: nunca faça commit de um .xcodeproj

Um arquivo de projeto do Xcode (.pbxproj) é um blob gerado enorme que não dá para editar à mão de forma sensata, e você não pode abrir o Xcode no Windows para editá-lo por você. Então não guarde um. O XcodeGen monta o projeto a partir de um arquivo YAML curto no começo de cada execução de CI. Este é o template real do qual todos os apps partiram (templates/project.yml, resumido):

name: MyApp
options:
  bundleIdPrefix: com.example
  deploymentTarget:
    iOS: "17.0"
settings:
  base:
    DEVELOPMENT_TEAM: ${DEVELOPMENT_TEAM}
    CODE_SIGN_STYLE: Manual
    MARKETING_VERSION: "1.0.0"
targets:
  MyApp:
    type: application
    platform: iOS
    sources:
      - path: Sources
    resources:
      - path: Resources
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.example.myapp
        GENERATE_INFOPLIST_FILE: YES
        ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon

O team ID entra por uma variável de ambiente, então nunca fica no repositório. Sem arquivo de projeto no git também não há conflitos de merge num pbxproj, o que, cá entre nós, importa muito quando há agentes editando o código.

Passo 2: alugue um Mac por minuto

O Mac inteiro mora num único arquivo de workflow, .github/workflows/appstore.yml, disparado do Windows com um comando:

gh workflow run appstore.yml -f action=dry_run   # or compile, or release

compile faz o build e para. dry_run faz o build, assina e envia para o App Store Connect sem submeter. release também submete para análise. Duas armadilhas do runner custaram execuções de verdade, e as duas agora estão no arquivo como comentários.

Armadilha 1: o Xcode que não está lá. Na imagem macos-15, /Applications/Xcode_26.app existia, mas era um stub sem Contents/Developer dentro, então selecioná-lo falhava enquanto o ls o listava todo feliz. A correção escolhe o Xcode 26 mais novo que realmente tem uma toolchain:

chosen=""
for app in $(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -Vr); do
  if [ -d "$app/Contents/Developer" ]; then chosen="$app"; break; fi
done
sudo xcode-select -s "$chosen/Contents/Developer"

Armadilha 2: simuladores que mudam de lugar. Um nome de aparelho fixo no código funcionou por seis execuções e falhou na sétima com "Unable to find a device matching the provided destination specifier", porque a frota de runners não trazia sempre os mesmos simuladores. Agora o iPhone mais novo disponível ganha:

name=$(xcrun simctl list devices available \
       | grep -oE "iPhone [0-9]+ Pro Max" | sort -Vr | head -1)
echo "SIM_NAME=$name" >> "$GITHUB_ENV"

Quanto custa. A página de cobrança do GitHub diz que o Actions é grátis para repositórios públicos em runners hospedados padrão. Num repositório privado, um runner macOS de 3 ou 4 núcleos custa $0,062 por minuto. As nossas 322 execuções em macOS (workflows de App Store, Build e vídeo para a análise) somam cerca de 2.573 minutos da fila até o fim, o que daria aproximadamente $160 num repositório privado. Pagamos $0, porque os repositórios dos apps são públicos. Um único release como o Chain 1.2 (4 min 22 s) custaria uns 27 centavos.

Passo 3: assinatura sem Mac (a parte que quebrou)

A assinatura de código é como a Apple sabe que um build veio de você: um certificado (chave pública assinada pela Apple) mais a chave privada dele, e um provisioning profile que amarra os dois ao app. Num Mac, o Xcode esconde tudo isso. Num runner de CI, foi onde o primeiro app passou a manhã inteira.

Captura das execuções do workflow App Store do Glyphstorm no GitHub Actions, 10 de set. de 2026: execução 10 verde às 5h49 da manhã, execução 11 e execuções 13 a 19 com falha, execução 12 cancelada, execução 20 verde às 6h38 da manhã (EDT).
Execuções App Store do Glyphstorm em 10 de set. de 2026, das 5h49 às 6h38 da manhã, ET (github.com/Mattbusel/glyphstorm, capturado em 16 de out.).

Isto é o que aquelas execuções vermelhas de 10 de setembro realmente disseram, direto dos logs:

  • Your team has no devices from which to generate a provisioning profile. A assinatura automática pede um profile de desenvolvimento durante o archive, e um profile de desenvolvimento precisa de um iPhone registrado. Um build para a App Store não precisa de aparelho nenhum, mas você recebe essa mensagem mesmo assim.
  • Could not find the newly generated certificate installed. A etapa de certificado do fastlane criou um certificado novo no runner.
  • MAC verification failed during PKCS12 import (wrong password?). A senha estava certa. O formato do arquivo é que não estava.
  • Error resolving conflict between options: 'adhoc' and 'development'. Passar essas flags como false ainda conta como passá-las.

A segunda é o erro caro. Quando o CI cria um certificado, a chave privada nasce numa máquina que é apagada minutos depois. A Apple guarda um certificado que ninguém nunca mais vai conseguir usar, e a Apple limita quantos certificados de distribuição uma conta pode ter. Três execuções com falha já tinham deixado três deles órfãos antes que alguém percebesse.

Nunca deixe o CI criar um certificado. Gere a chave uma vez na sua própria máquina e deixe o CI apenas importá-la.

Então a chave é gerada no Windows. O Store/signing.py a cria com a biblioteca Python cryptography, envia uma solicitação de assinatura ao endpoint de criação de certificado da Apple e grava um .p12 (resumido):

key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
csr = (x509.CertificateSigningRequestBuilder()
       .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Glyphstorm Distribution")]))
       .sign(key, hashes.SHA256()))
made = call("POST", "/v1/certificates", {"data": {"type": "certificates", "attributes": {
    "certificateType": "DISTRIBUTION",
    "csrContent": csr.public_bytes(serialization.Encoding.PEM).decode()}}})

# Legacy PKCS#12 (3DES + SHA-1). macOS `security import` can't read the AES-256 form
# and reports it as "MAC verification failed ... (wrong password?)".
encryption = (PrivateFormat.PKCS12.encryption_builder()
              .key_cert_algorithm(pkcs12.PBES.PBESv1SHA1And3KeyTripleDESCBC)
              .hmac_hash(hashes.SHA1())
              .build(P12_PASSWORD.encode()))

O script manda o .p12 em base64 direto para o gh secret set, então ele nunca vai parar num lugar onde poderia ser commitado. As chamadas de API usam um token ES256 de vida curta assinado com o .p8; a Apple rejeita tokens que duram mais de 20 minutos, então um token novo é gerado a cada chamada.

No runner, a importação é um keychain descartável mais uma linha que todo guia de assinatura em CI deveria imprimir em negrito:

security import "$RUNNER_TEMP/dist.p12" -k "$keychain" \
  -P "$DIST_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
# Without the partition list, codesign blocks on a GUI prompt that
# nobody can answer and the job hangs until it times out.
security set-key-partition-list -S apple-tool:,apple:,codesign: \
  -s -k "" "$keychain" >/dev/null

(A referência do comando security, se você quiser todas as flags.) Depois disso, o build pede o profile da App Store diretamente com get_provisioning_profile e assina manualmente, o que pula a etapa do profile de desenvolvimento, que não tem como dar certo num time sem aparelhos. Oito execuções com falha e 37 minutos depois do início da briga, a execução das 6h38 da manhã ficou verde, e às 6h43 da manhã (ET) de 10 de setembro o log imprimiu Successfully submitted the app for review!. O primeiro commit daquele app tinha sido às 4h06 da manhã.

Passo 4: o fastlane faz a parte chata

O fastlane é uma ferramenta em Ruby que empacota as etapas de build e upload da Apple em "lanes". Ele lê a chave de API do secret com app_store_connect_api_key, então não existe senha de Apple ID nem prompt de autenticação em dois fatores em lugar nenhum. Mais uma armadilha mora aqui: os números de build. Envie o mesmo número de build duas vezes e a Apple recusa, então cada build pergunta ao App Store Connect qual é o mais recente e soma um (fastlane/Fastfile):

next_build = latest_testflight_build_number(
  api_key: api_key,
  app_identifier: APP_IDENTIFIER,
  initial_build_number: 0
) + 1

Ele entra como build setting (CURRENT_PROJECT_VERSION=#{next_build}), não pelo increment_build_number, que aciona o agvtool da Apple e falhou num projeto gerado com Cannot find "MyApp.xcodeproj/../YES". Depois o upload_to_app_store envia o binário, os textos e as capturas de tela, com submit_for_review true para release e false para dry_run.

Quando tudo isso está no lugar, um release é entediante, que é exatamente o objetivo:

Captura do job do GitHub Actions para o Chain 1.2: selecionar o Xcode 26 4s, instalar xcodegen e fastlane 7s, escolher um simulador 25s, compilar 52s, gravar a chave de assinatura 0s, importar o certificado de assinatura 1s, build, upload e submissão 2m 31s. Job concluído com sucesso em 4m 9s.
Chain 1.2, 8 de out. de 2026: 4m 9s de tempo de job, 4m 22s com a fila. A etapa se chama Build, upload and submit; esta foi um dry_run, então parou depois do upload.

Capturas de tela para a App Store sem Mac

A Apple quer capturas de tela no tamanho de 6,9 polegadas (1320 por 2868). Sem Mac não dá para ficar clicando num simulador, então o app recebe um argumento de inicialização e abre a tela certa com dados de demonstração. Isto vem do Sources/App.swift do Chain:

func applyShotArgs(_ s: Store) {
    let a = ProcessInfo.processInfo.arguments
    guard let i = a.firstIndex(of: "-shot"), i + 1 < a.count else { return }
    switch a[i + 1] {
    case "year": tab = .year
    case "stats": tab = .stats
    case "rescue": rescue = s.rescues().first
    case "themes": shop = true
    default: break
    }
}

O workflow Build então cria um simulador novo, ajusta a barra de status para o clássico 9:41 com bateria cheia e faz o loop:

for shot in today widgets year detail rescue themes week stats edit paywall onboarding done; do
  xcrun simctl terminate "$udid" com.mattbusel.chainhabits || true
  xcrun simctl launch "$udid" com.mattbusel.chainhabits -shot $shot
  sleep 8
  xcrun simctl io "$udid" screenshot "shots/$(printf %02d $i)-$shot.png"
  i=$((i+1))
done

Um simulador novo faz diferença: inicializar o simulador padrão do runner travou por mais de 15 minutos uma vez. Os PNGs brutos voltam como artefato do build, e um pequeno script no Windows coloca um título sobre cada um.

Quatro capturas de tela do Chain na App Store: Five seconds a day, Watch your year fill in, Missed a day? Save the chain e Know your best days, cada uma mostrando a interface escura do app com quadrados de hábito em verde-limão.
Quatro das dez capturas de tela do Chain na App Store: capturas brutas do simulador do CI, com títulos adicionados por um script no Windows.

Uma opinião aqui: as capturas que vão para a loja ficam commitadas no repositório, e a lane release nunca captura de novo. As imagens que a Apple vê são exatamente as que um humano olhou, e cada nova tentativa de resolver um problema de assinatura fica dez minutos mais curta.

O que 139 execuções nos ensinaram

Puxar todas as execuções pela API do GitHub Actions mostra uma curva de aprendizado limpa. Em 21 repositórios houve 139 execuções do workflow App Store: 81 com sucesso, 43 com falha, 15 canceladas. A execução bem-sucedida mediana levou cerca de 8 minutos da fila até o fim.

Gráfico de barras das execuções com falha do workflow App Store por app, na ordem de lançamento: Glyphstorm 13, Clear the Strait 13, Pocket Beings 5, Minder 3, Fairway Ledger 2, Baseline Ledger 2, Ironbook 2, Race Fuel 1, Chain 1, Quiver 0, Rooms 1, e depois Clockout, Odometer, Curve, Pricebook, Pawprint, Chores, Climbbook, Cutoff, Parked e Race Pace, todos com 0.
Execuções com falha do workflow App Store por app, em todo o histórico, apps na ordem da primeira execução (de 10 de set. a 8 de out. de 2026). Fonte: API do GitHub Actions, dados puxados em 16 de out. de 2026.

Os dois primeiros apps, Glyphstorm e Clear the Strait, consumiram 13 falhas cada, a maioria em 10 de setembro. O Pocket Beings, iniciado um dia depois, teve 5 no total. No lote de 24 de setembro já era quase sempre zero, e os últimos dez apps foram publicados sem uma única execução App Store com falha. Climbbook, Cutoff, Parked e Race Pace chegaram ao App Store Connect na primeira execução, em 27 de setembro. Mesmo pipeline, mesmos comentários no arquivo. Cada correção ficou anotada onde o próximo app ia ler.

Mas a reviravolta é real. Esses apps sem nenhuma falha são justamente os que o App Review barrou. Em 5 de outubro, a Apple rejeitou 11 deles pela Guideline 4.3 (spam) e pela 4.2.6. A minha leitura: quando publicar não custa nada, o gargalo sai do CI e vai para "este app é diferente o bastante para merecer uma página na loja". O pipeline não responde isso por você. O seu produto é que tem de responder.

Contas novas também devem esperar uma Guideline 2.1 "Information Needed" na primeira submissão. Os nossos dois primeiros apps receberam uma cada. Uma gravação de tela curta do app resolve, e o mesmo runner pode gravar isso também.

Faça isto no seu projeto hoje

  1. Apague o .xcodeproj do git e escreva um project.yml para o XcodeGen. Gere o projeto no CI.
  2. Crie o certificado na sua própria máquina pela API, salve como .p12 no formato legado e coloque num secret do GitHub. Se o CI já criou alguns, liste e revogue os órfãos.
  3. No CI: keychain descartável, importação, set-key-partition-list. Pular o último é travamento garantido.
  4. Assine manualmente com um profile da App Store. Não deixe a assinatura automática sair procurando um aparelho que você não tem.
  5. Número de build = build mais recente do TestFlight + 1, passado como build setting.
  6. Nunca deixe fixo no código o caminho do Xcode nem o nome do simulador. Escolha o mais novo de verdade em tempo de execução.
  7. Adicione um argumento de inicialização -shot para que o CI possa tirar as suas capturas de tela da App Store.
  8. Use dry_run até a página da loja ficar certa, e depois release.

Se você cria apps em SwiftUI desse jeito, o texto sobre a spring animation do Minder é sobre um dos apps que saíram deste pipeline. E se um agente de programação está pilotando o loop de CI para você, leia antes como impedir agentes de programação de editar os seus testes e o que quebrou rodando 20 agentes num único PC, porque um agente consertando assinatura às 6 da manhã vai, todo contente, "consertar" a verificação em vez do problema.

O pipeline completo (o workflow com cada armadilha comentada, o Fastfile, o signing.py, os scripts do App Store Connect e uma skill de agente que conduz o processo) está empacotado como o Ship iOS from Windows kit. Se preferir que alguém crie o app em si, aqui estão os nossos preços fechados.

Qual é a etapa da Apple que você ainda acha que precisa de um Mac? Conte para a gente e vamos verificar se a API consegue fazer.

Publicado originalmente aqui no tensorust.com em 16 de out. de 2026. Também no X (@TensorustAI) e no dev.to. Relacionados: Configuramos este pipeline no seu repositório: $299, Apps para iPhone com preço fechado, os apps para iPhone, Ship iOS from Windows kit.

Leia também

Todos os artigos →

Mais em português

Português (Brasil): Tensorust →

Up next

Most read on the site this weekcrates.io download count: 80% of ours arrived with a new versionWhat the crates.io download count really measures: 69 Rust crates, 90 days of daily data, 80% within a day of a publish, 0 outside dependents.Read it next →

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ