Pular para o conteúdo
Development

Deixando o Claude Code dirigir o Xcode: o truque dos synchronized groups

Por Victor Da Luz
iosswiftxcodeclaude-codedev-logdeep-cut-atlas

Esse app foi renomeado depois para Deep Cut Atlas. Ele é chamado de “Discoverer” ao longo do texto abaixo, porque era assim que se chamava no dia em que isso aconteceu.

Estou construindo um app iOS com o Claude Code fazendo a maior parte da digitação. A primeira tarefa de verdade foi a menos glamorosa: criar o projeto Xcode e o esqueleto do app do qual tudo mais depende. Três abas, uma estrutura de pastas, os entitlements de MusicKit e CloudKit, uma configuração de StoreKit. Nada difícil por si só. A parte difícil é que o Claude não consegue clicar.

O problema: o Claude não consegue usar o assistente de New Project

Um .xcodeproj não é um arquivo que você escreve do zero. É um bundle com um project.pbxproj cheio de detalhes por dentro, e minhas próprias regras de projeto dizem para nunca deixar um agente editar esse arquivo manualmente. O Xcode reescreve ele o tempo todo e uma edição ruim quebra o projeto inteiro. Então o “Claude, cria o projeto” de sempre não funciona, e “Claude, edita o pbxproj” é proibido de propósito.

Olhei três jeitos de contornar isso:

  • A ferramenta de scaffold do XcodeBuildMCP. Ela consegue gerar um projeto a partir de um template. Mas produz um workspace mais uma estrutura de Swift package, o que é mais estrutura do que um único app precisa, e ela divide as capabilities que me importavam num app target fino. Mais peças em movimento no primeiro dia.
  • XcodeGen. Geração declarativa de projeto a partir de um spec YAML. Reproduzível, mas adiciona uma dependência de build e briga com a regra de “deixar o Xcode ser dono do projeto”.
  • Eu crio a casca, o Claude preenche. Eu rodo o assistente uma vez (uns dois minutos clicando), o Claude escreve todo arquivo Swift depois disso.

Fui com a terceira. É a menos esperta e combinou com minhas regras já existentes. Eu crio o projeto vazio, o Claude faz os outros 95%.

O truque que faz funcionar: synchronized groups

Aqui está a parte que eu não sabia que ia me salvar. O Xcode 16 mudou como projetos novos rastreiam arquivos. Em vez de registrar cada arquivo manualmente no pbxproj, um projeto novo de App usa um file-system synchronized group (PBXFileSystemSynchronizedRootGroup, se você for procurar). O acordo é simples: qualquer arquivo que apareça na pasta do target automaticamente faz parte do target. Sem diálogo “Add Files to project”, sem edição de pbxproj.

Essa é exatamente a costura de que o Claude precisa. Ele escreve um arquivo .swift na pasta e o arquivo já está no build. Conferi se estava ativado antes de escrever qualquer coisa:

grep -c PBXFileSystemSynchronizedRootGroup Discoverer.xcodeproj/project.pbxproj

Três resultados, tudo certo. A partir daí o Claude criou a árvore inteira (App/, Features/, Models/, e assim por diante) e escreveu as views, o ponto de entrada do app, e um model placeholder. A primeira compilação pegou tudo isso com zero edições no arquivo de projeto. Essa é a coisa que vou lembrar dessa tarefa: no Xcode 16+, um agente pode dominar os arquivos de código-fonte completamente, desde que fique dentro da pasta sincronizada.

As pegadinhas (sempre tem pegadinha)

Synchronized groups empacotam tudo que está na pasta, e isso me mordeu duas vezes.

Primeiro, coloquei um .gitkeep em duas pastas vazias para mantê-las no git. O build morreu:

error: Multiple commands produce '.../Discoverer.app/.gitkeep'

Os dois arquivos .gitkeep queriam copiar para o mesmo lugar dentro do bundle do app. Correção: usar um arquivo Swift só de comentário, com nome único, como placeholder da pasta em vez disso. Ele compila para nada e não é tratado como um recurso.

Depois nomeei os dois placeholders de Placeholder.swift. Pastas diferentes, mesmo nome. O build morreu de novo:

error: Multiple commands produce '.../Placeholder.stringsdata'

O compilador deriva um .stringsdata por arquivo, nomeado a partir do arquivo de origem, então os nomes de arquivo precisam ser únicos em todo o target, não só dentro de uma pasta. Renomeá-los para ServicesPlaceholder.swift e UtilitiesPlaceholder.swift resolveu.

A armadilha do deployment target

Projetos novos no Xcode 26 definem por padrão o deployment target mínimo como o sistema operacional atual. O meu saiu como iOS 26.5. Quando tentei compilar para um simulador de iPhone 16 Pro, o xcodebuild me disse que nenhum dispositivo dava match. O motivo: o runtime de simulador mais novo que eu tinha instalado era o 26.2, que é menor que 26.5, então todo simulador ficou inelegível. Um app não pode rodar num SO mais antigo que seu próprio piso.

Baixar o target para iOS 17 resolveu isso. Enquanto estava nisso, esbarrei numa armadilha menor. Esse comando falha:

xcodebuild build -scheme Discoverer -destination 'platform=iOS Simulator,name=iPhone 16 Pro'

Um nome de dispositivo puro significa “o SO mais recente”. O runtime mais recente na minha máquina era o 26.x, que só traz modelos de iPhone 17. O iPhone 16 Pro vive no runtime 18.5. Então tive que dizer qual SO eu queria:

xcodebuild build -scheme Discoverer -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.5' | xcbeautify

Mantendo a casca executável antes do CloudKit existir

Marquei “Host in CloudKit” no assistente, o que adiciona o entitlement de CloudKit mas deixa o identificador de container vazio até você escolher um team. Um container padrão de SwiftData com CloudKit ligado e nenhum container pode falhar ao iniciar, e eu queria um “funciona” limpo antes de conectar o iCloud. Então forcei armazenamento local por enquanto:

let configuration = ModelConfiguration(cloudKitDatabase: .none)
modelContainer = try ModelContainer(for: Item.self, configurations: configuration)

O entitlement ainda declara CloudKit para depois; o runtime só continua local até o container ser real. Uma regra relacionada que já embuti desde o início: toda propriedade de SwiftData recebe um valor padrão ou é opcional, porque stores com CloudKit não conseguem impor colunas não opcionais.

O que eu diria para mim mesmo antes de começar

O modelo mental que funcionou: eu domino o arquivo de projeto e os cliques na interface, o Claude domina o código-fonte. Os synchronized groups do Xcode 16 são o que torna essa divisão limpa em vez de um fluxo constante de “adiciona esse arquivo ao target” repetitivo. Confira se o synced group está ativado, mantenha os nomes de arquivo únicos, defina seu deployment target numa versão que um simulador consiga de fato rodar, e não ligue o CloudKit em runtime antes de existir um container por trás. O app compila, três abas aparecem, e nenhuma linha de pbxproj foi editada manualmente.

Nota de ferramental: a saída do build passa pelo xcbeautify para logs legíveis, e o XcodeBuildMCP está instalado para depois (a instalação dele é brew tap getsentry/xcodebuildmcp && brew install xcodebuildmcp, não o brew install simples que você vai ver em notas desatualizadas).

Leitura relacionada

Development

Configurando o Claude Code para um projeto iOS

Escrevendo o CLAUDE.md de um app iOS antes de existir Swift: a armadilha do MusicKit no simulador, as regras do CloudKit para SwiftData, e o enquadramento de aprendizado que veio primeiro.

Ler
Development

Uma seção de tracklist, e por que levou 30 minutos

Um método de protocolo, um enum de estado reaproveitado, uma convenção de roteamento que se manteve, e um orçamento de lint que forçou uma divisão que valia a pena fazer de qualquer forma.

Ler

Você também pode achar útil

RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais
Airalo

eSIM Airalo

eSIM de dados local para viagens - sem necessidade de trocar um SIM físico.

Este é meu link de indicação da Airalo. Você recebe um desconto no seu primeiro eSIM e eu ganho crédito da Airalo para o meu.

Saiba mais
Proton

Proton VPN

VPN comercial com filtragem NetShield e interruptor de desligamento automático.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais