Desarrollo de aplicaciones para iPhone
Convenciones destiladas de la publicación de una aplicación SwiftUI de principio a fin, desde un repositorio vacío hasta una compilación firmada ejecutándose en un teléfono físico.
Contexto asumido: aplicaciones a pequeña escala, una cuenta personal gratuita de Apple, sin distribución en la App Store. Un equipo de desarrolladores de pago cambia varias reglas a continuación: la sección de firma indica cuáles.
Valores predeterminados de arquitectura
- SwiftUI + SwiftData, local-first. Una aplicación pequeña no debería requerir un servidor accesible. Los datos residen en el dispositivo en SwiftData; las llamadas de red son solo para APIs públicas (https, sin excepciones ATS). Si la aplicación comienza como un cliente ligero para un backend existente, espera el giro de "¿podemos eliminar el servidor?" — porta la lógica del backend a funciones Swift puras sobre los arrays de SwiftData en lugar de mantener un servidor en funcionamiento para un solo teléfono.
- Modelado con SwiftData: relaciones de objetos reales, no claves foráneas
enteras.
@Attribute(.unique)para claves naturales. Los arrays to-many de SwiftData no preservan el orden: añade un camposortOrdery una propiedad computadasortedXcuando el orden importe. Fechas comoDate, no cadenas. Enums con rawValueStringy conformidadCodable. - Cambios de esquema en una instalación existente: agregar modelos o propiedades
opcionales migra ligeramente de forma silenciosa en el siguiente inicio; los cambios de nombre,
eliminaciones o cambios de tipo sin un
SchemaMigrationPlanhacen que el almacenamiento falle al abrirse; la aplicación desplegada entonces falla al inicio con los datos del usuario intactos pero secuestrados. Los redespliegues (mismo ID de paquete) nunca eliminan el contenedor de datos; solo la eliminación de la aplicación lo hace. Planifica una ruta de exportación/copia de seguridad antes de que los usuarios acumulen datos reales. - Las vistas se comunican directamente con
@Query+modelContext. Sin capa de Store/repositorio a menos que algo necesite genuinamente almacenamiento en caché. La lógica pura (coincidencia, análisis, importación) reside en namespaces de enum (enum Foo { static func … }) — comprobable sin UI. - Objetivo de despliegue: el actual menos uno a menos que una API
necesaria indique lo contrario. Un objetivo,
TARGETED_DEVICE_FAMILY: "1", retrato a menos que la aplicación justifique la rotación.
Generación de proyectos — XcodeGen, nunca un .xcodeproj comprometido
ios/project.yml es la fuente de verdad; el paquete .xcodeproj es
generado y excluido con git (también excluye ios/build/ y el Info.plist
generado). Regenera después de agregar o eliminar archivos: xcodegen
recorre los directorios de fuentes, por lo que las ediciones en archivos existentes no necesitan nada.
- Instala xcodegen desde el archivo zip de la versión de GitHub (
PREFIX=~/.local ./install.sh) — sin Homebrew requerido, funciona en una Mac bloqueada. - Los objetivos de pruebas unitarias necesitan
GENERATE_INFOPLIST_FILE: YESo el paquete de prueba falla al firmar. - Define el esquema en
project.ymlcongatherCoverageData: truey el objetivo de prueba adjunto, oxcodebuild testno tiene nada que ejecutar.
El bucle de desarrollo — solo CLI, simulador por defecto
Cada proyecto iOS obtiene estos objetivos de Makefile. Parametriza el nombre
del simulador, el ID del dispositivo y el ID del equipo como variables ?= para que nada
específico de la máquina esté codificado:
| Objetivo | Qué hace |
|---|---|
ios-project | xcodegen generate + abrir en Xcode |
ios-sim | compilar sin firma para simulador, iniciar sim, instalar, lanzar — el bucle predeterminado; cero firma, cero solicitudes de contraseña |
ios-sim-seed | ios-sim, luego relanzar con --seed-demo |
ios-test | xcodebuild test en el simulador + informe de cobertura por objetivo con xccov |
ios-deploy | compilación firmada para el teléfono conectado + instalación con devicectl + lanzamiento |
ios-test imprime la cobertura en cada ejecución: la regla de la casa es que
make test siempre muestra el número, sin umbral fijo adjunto.
Compilaciones para simulador: CODE_SIGNING_ALLOWED=NO está bien para
verificaciones de compilación generic/platform=iOS; los destinos del simulador no necesitan
ninguna firma. Recuerda que el simulador y el teléfono tienen bases de datos separadas.
Comodidades de depuración que siempre valen la pena
- Sembrador de datos de ejemplo (
DemoData.swift,#if DEBUG): conjunto de datos curado con campos de caducidad/fecha calculados relativos a.now, idempotente (re-ejecutar no agrega nada), expuesto de tres maneras: un botón en la sección "Desarrollador" de Ajustes, un argumento de lanzamiento--seed-demoverificado medianteProcessInfo.processInfo.arguments, y el objetivoios-sim-seed. Agrega una prueba que verifique que la siembra ejercita los estados principales de la aplicación para que no pueda pudrirse silenciosamente. - Selectores con búsqueda en lugar de selectores de rueda una vez que una lista puede exceder un par de docenas de entradas (los conjuntos de datos importados garantizan esto).
- Funciones de cámara/escáner: verifica
DataScannerViewController.isSupported && .isAvailabley envía una alternativa de entrada manual: se degrada con gracia también en el simulador.
Pruebas
- XCTest en el simulador; al portar lógica de otra pila, porta también el conjunto de pruebas de referencia para que la semántica esté fijada entre lenguajes.
- Ayudante compartido:
makeInMemoryContext()que construye un ModelContainer conisStoredInMemoryOnly: true; clases de prueba@MainActorconsetUp() async throws. - Lee el resultado de aprobado/reprobado del paquete de resultados:
xcrun xcresulttool get test-results summary --path build/test.xcresult.
Firma y despliegue: los problemas (cada uno costó tiempo real)
- El ID del equipo NO es el paréntesis en
security find-identity— ese es el ID del propio certificado. Obtén el ID real del equipo desde un perfil de aprovisionamiento (security cms -D -i <profile>→ el prefijoapplication-identifier) o desde el panel de Cuentas de Xcode. El ID incorrecto da un error engañoso "No Account for Team". -allowProvisioningUpdatesno puede ver la sesión del ID de Apple desde una terminal simple. Crea el perfil de aprovisionamiento una vez en la GUI de Xcode (Firma y Capacidades → establecer Equipo → Intentar de nuevo); después de eso, las compilaciones puramente desde CLI funcionan conDEVELOPMENT_TEAM=<id> CODE_SIGN_STYLE=Automatic.- Baile del primer dispositivo: habilita el Modo Desarrollador en el teléfono (Ajustes → Privacidad y Seguridad; el interruptor aparece solo después de que Xcode haya hablado con el dispositivo; reinicios) → confía en la computadora → después de la primera instalación, confía en el certificado de desarrollador (Ajustes → General → VPN y Gestión de Dispositivos). El error "your team has no devices" de Xcode generalmente significa que el intento de registro ocurrió mientras el teléfono se reiniciaba o estaba bloqueado — selecciona el teléfono como destino de ejecución y reintenta.
- Solicitudes de llavero en cada compilación: haz clic en Siempre Permitir en el diálogo del llavero de codesign una vez; las solicitudes de contraseña por compilación se detienen.
- Límites del equipo personal gratuito: las instalaciones caducan después de 7 días (redespliega para volver a firmar), máximo 3 aplicaciones laterales, sin notificaciones push, sin App Groups. Un equipo de pago elimina los cuatro: si la aplicación necesita push o App Groups, ese es el desencadenante para obtener uno.
- CLI de despliegue/lanzamiento:
xcrun devicectl list devices,xcrun devicectl device install app --device <id> <path>.app,… device process launch --device <id> <bundle-id>.
Inicio en una Mac nueva (compatible con headless)
- Xcode desde la App Store (abre
macappstore://apps.apple.com/app/id497799835). sudo xcode-select -s /Applications/Xcode.app && sudo xcodebuild -license accept && sudo xcodebuild -runFirstLaunch— necesita un administrador; en una cuenta estándar, primero arregla la membresía de administrador (su admin -c 'sudo dseditgroup -o edit -a <usuario> -t user admin').xcodebuild -downloadPlatform iOS— el SDK de iOS y el runtime del simulador es una descarga separada de varios GB desde Xcode 15; las compilaciones fallan con "iOS X.Y is not installed" sin él.DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developerfunciona como una anulación por comando antes de que se arregle xcode-select.- xcodegen desde el archivo zip de la versión en
~/.local(ver arriba).
Clientes complementarios para un servidor existente: niveles de sincronización
Cuando la aplicación de iPhone se une a una aplicación que ya tiene un servidor + aplicación web (el servidor sigue siendo la única fuente de verdad), elige el nivel más barato que permita el caso de uso del teléfono. Pregunta primero "¿necesita el teléfono funcionar fuera de la red del servidor?" y "¿captura o edición completa?":
- Cliente ligero — sin almacén local; el teléfono llama a la API existente. Correcto cuando la conectividad está garantizada.
- Espejo de instantáneas — caché de SwiftData reconstruida completamente a partir de una extracción JSON completa (las bases de datos de un solo usuario son pequeñas). Sin fusión → sin lápidas, sin marcas de tiempo por fila, sin motor de conflictos; las eliminaciones/ediciones del lado del servidor llegan gratis. Las fotos se cargan de forma diferida mediante la caché de URL.
- Bandeja de salida limitada — creación sin conexión limitada a hijos de registros que ya existen (registros, notas, fotos contra padres conocidos). Los padres llevan IDs reales del servidor, por lo que la reproducción son llamadas API simples: los esquemas con PK enteros necesitan ninguna migración a UUID. Evento de sincronización = enviar bandeja de salida, luego extraer instantánea (las propias capturas vuelven confirmadas). Muestra huérfanos 404 ("N no pudo sincronizarse"), nunca elimines silenciosamente. Muestra una insignia de pendientes + una marca de tiempo de la última sincronización. Auto-sincronizar solo cuando la dirección del servidor fijada por el usuario responda, nunca explorar redes externas.
- Sincronización bidireccional completa (UUIDs, lápidas, endpoints delta,
LWW/CRDT) — solo con evidencia de que los niveles 1–3 no son suficientes; el rediseño
comienza del lado del servidor (UUIDs acuñables por el cliente, upserts idempotentes,
changes?since=+ lápidas) antes de escribir cualquier Swift.
Los niveles 2 y 3 se combinan bien: un espejo de instantáneas para lecturas más una bandeja de salida limitada para capturas cubre "funciona en casa, captura en cualquier lugar" sin ninguna de la maquinaria del nivel 4.
Higiene del proyecto
- Los hechos específicos de la máquina y del dispositivo (ID del equipo, UDID del dispositivo, nombre del
simulador) van en variables
?=del Makefile o en las notas del propio proyecto — nunca codificados en scripts, y nunca en esta skill. - Mantén un documento
plans/<tema>.mdpara cada fragmento de trabajo en curso y unCHANGELOG.mdde nivel superior para lo que se ha enviado; mueve un plan aplans/done/<tema>.mdcon un encabezado "Entregado en <versión> — " una vez que se haya completado. - Tamaño de cada fragmento de plan para que termine con algo ejecutable en un teléfono o simulador. Un fragmento de iOS que no se pueda lanzar no se puede revisar.