Desenvolvimento de aplicativos iPhone
Convenções extraídas do envio de um aplicativo SwiftUI do início ao fim, desde um repositório vazio até uma build assinada rodando em um telefone físico.
Contexto assumido: aplicativos de pequena escala, uma conta Apple pessoal gratuita, sem distribuição na App Store. Uma equipe de desenvolvedores pagos altera várias regras abaixo — a seção de assinatura sinaliza quais.
Padrões de arquitetura
- SwiftUI + SwiftData, local-first. Um aplicativo pequeno não deve exigir um servidor acessível. Os dados residem no dispositivo com SwiftData; chamadas de rede são apenas para APIs públicas (https, sem exceções ATS). Se o aplicativo começar como um cliente leve para um backend existente, espere pela virada "podemos eliminar o servidor?" — portar a lógica do backend para funções Swift puras sobre os arrays do SwiftData, em vez de manter um servidor ativo para um único telefone.
- Modelagem SwiftData: relacionamentos reais de objetos, não chaves estrangeiras
inteiras.
@Attribute(.unique)para chaves naturais. Arrays to-many do SwiftData não preservam ordem — adicione um camposortOrdere uma propriedade computadasortedXquando a ordem for importante. Datas comoDate, não strings. Enums comoString-rawCodable. - Mudanças de esquema em uma instalação ativa: adicionar modelos ou propriedades
opcionais faz migração leve silenciosamente na próxima inicialização; renomeações,
exclusões ou mudanças de tipo sem um
SchemaMigrationPlanfazem o armazenamento falhar ao abrir — o aplicativo implantado então trava na inicialização com os dados do usuário intactos, mas inacessíveis. Reimplantações (mesmo bundle ID) nunca apagam o contêiner de dados; apenas excluir o aplicativo faz isso. Planeje um caminho de exportação/backup antes que os usuários acumulem dados reais. - Views falam com
@Query+modelContextdiretamente. Sem camada de Store / repositório, a menos que algo genuinamente precise de cache. Lógica pura (correspondência, análise, importação) vive em namespaces de enum (enum Foo { static func … }) — testável sem UI. - Alvo de implantação: atual-major-menos-um, a menos que uma API necessária
diga o contrário. Um alvo,
TARGETED_DEVICE_FAMILY: "1", retrato a menos que o aplicativo justifique rotação.
Geração de projeto — XcodeGen, nunca um .xcodeproj commitado
ios/project.yml é a fonte da verdade; o pacote .xcodeproj é
gerado e gitignorado (também gitignore ios/build/ e o Info.plist
gerado). Regere após adicionar ou remover arquivos — o xcodegen
percorre o diretório de fontes, então edições em arquivos existentes não precisam de nada.
- Instale o xcodegen a partir do zip de release do GitHub (
PREFIX=~/.local ./install.sh) — sem Homebrew necessário, funciona em um Mac bloqueado. - Alvos de teste unitário precisam de
GENERATE_INFOPLIST_FILE: YESou o pacote de teste falha ao assinar. - Defina o esquema em
project.ymlcomgatherCoverageData: truee o alvo de teste anexado, ouxcodebuild testnão terá nada para executar.
O loop de desenvolvimento — apenas CLI, simulador por padrão
Cada projeto iOS recebe estes alvos Makefile. Parametrize o nome do
simulador, ID do dispositivo e ID da equipe como variáveis ?= para que nada
específico da máquina seja hardcoded:
| Alvo | O que faz |
|---|---|
ios-project | xcodegen generate + abrir no Xcode |
ios-sim | build sem assinatura para simulador, iniciar sim, instalar, lançar — o loop padrão; zero assinatura, zero prompts de senha |
ios-sim-seed | ios-sim, então relançar com --seed-demo |
ios-test | xcodebuild test no simulador + relatório de cobertura por alvo xccov |
ios-deploy | build assinada para o telefone conectado + instalação com devicectl + lançamento |
ios-test imprime a cobertura em cada execução — a regra da casa é que
make test sempre mostra o número, sem nenhum limite rígido anexado.
Builds para simulador: CODE_SIGNING_ALLOWED=NO é suficiente para
verificações de compilação generic/platform=iOS; destinos de simulador não precisam de
assinatura alguma. Lembre-se de que simulador e telefone têm bancos de dados separados.
Conveniências de depuração que sempre valem a pena
- Semeador de dados de exemplo (
DemoData.swift,#if DEBUG): conjunto de dados curado com campos de expiração/data calculados relativos a.now, idempotente (reexecução não adiciona nada), exposto de três maneiras — um botão na seção "Desenvolvedor" das Configurações, um argumento de lançamento--seed-demoverificado viaProcessInfo.processInfo.arguments, e o alvoios-sim-seed. Adicione um teste que afirma que a semente exercita os estados principais do aplicativo para que não possa apodrecer silenciosamente. - Pickers pesquisáveis em vez de Pickers de roda assim que uma lista puder exceder algumas dezenas de entradas (conjuntos de dados importados garantem isso).
- Recursos de câmera/scanner: verifique
DataScannerViewController.isSupported && .isAvailablee forneça um fallback de entrada manual — ele degrada graciosamente também no simulador.
Testes
- XCTest no simulador; ao portar lógica de outra pilha, porte também o conjunto de testes de referência para que a semântica seja fixada entre linguagens.
- Helper compartilhado:
makeInMemoryContext()construindo um ModelContainer comisStoredInMemoryOnly: true; classes de teste@MainActorcomsetUp() async throws. - Leia pass/fail do pacote de resultados:
xcrun xcresulttool get test-results summary --path build/test.xcresult.
Assinatura e deploy — as pegadinhas (cada uma custou tempo real)
- O ID da equipe NÃO é o parêntese em
security find-identity— esse é o próprio ID do certificado. Obtenha o ID real da equipe de um perfil de provisionamento (security cms -D -i <profile>→ o prefixoapplication-identifier) ou do painel de Contas do Xcode. O ID errado dá um erro enganoso "No Account for Team". -allowProvisioningUpdatesnão consegue ver a sessão do Apple ID de um terminal comum. Crie o perfil de provisionamento uma vez na GUI do Xcode (Signing & Capabilities → definir Team → Try Again); depois disso, builds puramente via CLI funcionam comDEVELOPMENT_TEAM=<id> CODE_SIGN_STYLE=Automatic.- Dança do primeiro dispositivo: ative o Modo Desenvolvedor no telefone (Configurações → Privacidade e Segurança; a opção aparece apenas depois que o Xcode falou com o dispositivo; reinicializações) → confie no computador → após a primeira instalação, confie no certificado do desenvolvedor (Configurações → Geral → VPN e Gerenciamento de Dispositivos). O erro "your team has no devices" do Xcode geralmente significa que a tentativa de registro aconteceu enquanto o telefone estava reiniciando ou bloqueado — selecione o telefone como destino de execução e tente novamente.
- Solicitações do Keychain em cada build: clique em Always Allow no diálogo do keychain de codesign uma vez; os prompts de senha por build param.
- Limites da conta pessoal gratuita: as instalações expiram após 7 dias (reimplante para reassinar), máximo de 3 aplicativos sideloaded, sem notificações push, sem Grupos de App. Uma equipe paga remove todos os quatro — se o aplicativo precisar de push ou Grupos de App, esse é o gatilho para obter uma.
- CLI de deploy/lançamento:
xcrun devicectl list devices,xcrun devicectl device install app --device <id> <path>.app,… device process launch --device <id> <bundle-id>.
Bootstrap de Mac novo (headless-friendly)
- Xcode da App Store (abra
macappstore://apps.apple.com/app/id497799835). sudo xcode-select -s /Applications/Xcode.app && sudo xcodebuild -license accept && sudo xcodebuild -runFirstLaunch— precisa de um admin; em uma conta padrão, corrija a associação ao admin primeiro (su admin -c 'sudo dseditgroup -o edit -a <user> -t user admin').xcodebuild -downloadPlatform iOS— o iOS SDK + runtime do simulador é um download separado de vários GB desde o Xcode 15; builds falham com "iOS X.Y is not installed" sem ele.DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developerfunciona como uma substituição por comando antes que o xcode-select seja corrigido.- xcodegen a partir do zip de release em
~/.local(veja acima).
Clientes complementares para um aplicativo servidor existente — níveis de sincronização
Quando o aplicativo iPhone se junta a um aplicativo que já tem um servidor + webapp (o servidor permanece a única fonte da verdade), escolha o nível mais barato que o caso de uso do telefone permitir. Pergunte "o telefone precisa funcionar longe da rede do servidor?" e "captura ou edição completa?" primeiro:
- Cliente fino — sem armazenamento local; o telefone chama a API existente. Adequado quando a conectividade é garantida.
- Espelho de snapshot — cache SwiftData reconstruído totalmente a partir de um pull JSON completo (bancos de dados de usuário único são pequenos). Sem mesclagem → sem tombstones, sem timestamps por linha, sem mecanismo de conflito; exclusões/edições do lado do servidor chegam de graça. Fotos carregam lazy via cache de URL.
- Outbox com escopo — criação offline limitada a filhos de registros que já existem (logs, notas, fotos contra pais conhecidos). Os pais carregam IDs reais do servidor, então a reprodução é simples chamadas de API — esquemas com PK inteiras não precisam de migração para UUID. Evento de sincronização = enviar outbox, depois puxar snapshot (as próprias capturas voltam confirmadas). Mostre órfãos 404 ("N não pôde sincronizar"), nunca descarte silenciosamente. Mostre um badge pendente + timestamp da última sincronização. Sincronização automática apenas quando o endereço do servidor fixado pelo usuário responde — nunca explore redes estrangeiras.
- Sincronização bidirecional completa (UUIDs, tombstones, endpoints delta,
LWW/CRDT) — apenas com evidências de que os níveis 1–3 não são suficientes; o retrabalho
começa no lado do servidor (UUIDs cunhados pelo cliente, upserts idempotentes,
changes?since=+ tombstones) antes de qualquer Swift ser escrito.
Os níveis 2 e 3 compõem bem: um espelho de snapshot para leituras mais um outbox com escopo para capturas cobre "funciona em casa, captura em qualquer lugar" sem nenhum mecanismo do nível 4.
Higiene do projeto
- Fatos específicos de máquina e dispositivo (ID da equipe, UDID do dispositivo, nome do sim) vão
em variáveis
?=do Makefile ou nas notas do próprio projeto — nunca hardcoded em scripts, e nunca nesta skill. - Mantenha um documento
plans/<topic>.mdpara cada fatia de trabalho em andamento e umCHANGELOG.mdde nível superior para o que foi lançado; mova um plano paraplans/done/<topic>.mdcom um cabeçalho "Entregue em <versão> — " assim que for concluído. - Dimensione cada fatia de plano para que termine com algo executável em um telefone ou simulador. Uma fatia iOS que não pode ser iniciada não pode ser revisada.