Un campo que esta en el @Model pero no en el esquema desplegado no sincroniza, y no falla nada: los datos se quedan en el dispositivo que los escribio. Asi se colaron nueve campos fuera de Production entre la 2.0 y la 2.1.2 sin que nadie lo notara, incluidas las reglas de dia de las etiquetas y el desayuno/merienda activados. scripts/check_cloudkit_schema.py compara las propiedades almacenadas de cada @Model con los CD_<campo> del esquema real (exportado con cktool), saltando relaciones, computadas y el sufijo _ckAsset de los binarios. fastlane submit y release abortan si falta algo; beta solo avisa, porque en TestFlight es normal que el deploy a Production aun no se haya hecho. La lane check_schema lo ejecuta suelto. El deploy a Production sigue siendo manual: cktool no tiene subcomando y Apple bloquea el endpoint de esquema en ese entorno. Refs #38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
6.1 KiB
MealMood — Project Context for Claude Code
App
- Name: MealMood: Family Meal Planner
- Bundle ID: com.alexandrevazquez.mealmood
- Platform: iOS/iPadOS (also runs as iOS app on Mac)
- Current version: 2.1.1 (dev; 2.1.0 with Apple Watch live in the App Store)
- Main branch:
1.0.1(production), feature branches named after versions
Branch ↔ version convention (IMPORTANT)
The active dev branch is named after the current MARKETING_VERSION, not the one it was created with. Whenever the iOS app version changes, rename the branch to match:
# after bumping MARKETING_VERSION in the Xcode project to e.g. 1.1.6
git branch -m <old-version> 1.1.6 # e.g. git branch -m 1.1.5 1.1.6
Right now: MARKETING_VERSION = 2.1.1 → active branch is 2.1.1. Keep them in sync. (The build number bump within the same version does NOT rename the branch — only a version change does.)
MARKETING_VERSION in the Xcode project does NOT drive the shipped version. Both Info.plists hardcode their values, so bumping the project setting alone builds an IPA with the old version and App Store Connect rejects it (90062: must contain a higher version than that of the previously approved version). A version bump means editing three places, and fastlane's increment_build_number only handles the app's build number:
| What | Where | Bumped by fastlane? |
|---|---|---|
| App version | MealMood/Resources/Info.plist → CFBundleShortVersionString |
❌ by hand |
| App build | MealMood/Resources/Info.plist + pbxproj |
✅ |
| Widget version + build | MealMoodWidget/Info.plist |
❌ by hand |
The widget must match the app or the upload is rejected for a version mismatch. Since fastlane beta increments the build as its first step, set the widget to the next build number before running it:
plutil -replace CFBundleShortVersionString -string "<version>" MealMood/Resources/Info.plist
plutil -replace CFBundleShortVersionString -string "<version>" MealMoodWidget/Info.plist
plutil -replace CFBundleVersion -string "<next build>" MealMoodWidget/Info.plist
Credentials — all managed via pass (GPG-encrypted, syncs to Gitea)
Never hardcode credentials. Always read from pass:
# Apple
pass show apple/mealmood/apple-id # alexandre.vazquez@gmail.com
pass show apple/mealmood/developer-team-id # 2825Q76T7H
pass show apple/mealmood/appstore-connect-team-id # 128443966
pass show apple/mealmood/app-store-app-id # 6759255553
pass show apple/mealmood/app-specific-password-fastlane # for fastlane uploads
# Firebase
pass show firebase/mealmood/api-key
pass show firebase/mealmood/project-id
pass show firebase/mealmood/google-app-id
# AdMob
pass show admob/mealmood/app-id
pass show admob/mealmood/banner-home-unit-id
Antes de mandar una versión a la App Store: comprobar el esquema de CloudKit
Un campo que está en el @Model pero no en el esquema de CloudKit no
sincroniza, y no falla nada: los datos se quedan en el dispositivo que los
escribió. Así se colaron nueve campos fuera de Production entre la 2.0 y la
2.1.2 (issue #38) — desayuno/merienda activados, sus horas, el recordatorio,
las fotos del planificador y las reglas de día de las etiquetas no viajaban
entre dispositivos.
fastlane check_schema # o directamente:
python3 scripts/check_cloudkit_schema.py --environment production
fastlane submit y fastlane release ya lo ejecutan y abortan si falta
algo; fastlane beta solo avisa (en TestFlight aún puede faltar el deploy).
Si faltan campos: el esquema de development solo se actualiza al ejecutar la app contra ese entorno con iCloud activo, o importándolo a mano:
TOKEN=$(pass show apple/mealmood/cloudkit-token)
TEAM=$(pass show apple/mealmood/developer-team-id)
xcrun cktool export-schema --token "$TOKEN" --team-id "$TEAM" \
--container-id iCloud.com.alexandrev.mealmood --environment development \
--output-file /tmp/schema.ckdb
# añadir los CD_<campo> que falten (Bool/Int → INT64, String → STRING,
# Date → TIMESTAMP, Data → CD_<campo>_ckAsset ASSET) y:
xcrun cktool import-schema --token "$TOKEN" --team-id "$TEAM" \
--container-id iCloud.com.alexandrev.mealmood --environment development \
--file /tmp/schema.ckdb
El paso a Production no se puede automatizar: cktool no tiene deploy y
Apple bloquea el endpoint de esquema en ese entorno. Lo hace Alexandre en
CloudKit Console → Schema → Deploy Schema Changes → Deploy to Production.
TestFlight upload
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=$(pass show apple/mealmood/app-specific-password-fastlane)
fastlane beta
Stack
- SwiftUI + SwiftData (iOS 17+)
- Firebase Analytics (
AnalyticsService.swift) - Google AdMob (banner, free tier only, hidden on Mac)
- StoreKit 2 (subscription
com.mealmood.premium.monthly) - iCloud KV sync (
ICloudSyncService.swift) — does NOT syncisPremium - Widget extension (
MealMoodWidget/) — requires App Groupgroup.com.alexandrevazquez.mealmood - Fastlane for CI/CD
Premium logic
PremiumSyncServiceis the source of truth for premium state transitionsisPremiumis determined only by StoreKit — never by iCloud syncTransaction.updateslistener inContentViewcatches purchases in real-time- Tests:
MealMoodTests/PremiumSyncServiceTests.swift
Key files
| File | Purpose |
|---|---|
MealMood/ContentView.swift |
App root, premium sync, Transaction.updates listener |
MealMood/Services/PremiumSyncService.swift |
Premium state machine (testable) |
MealMood/Services/StoreManager.swift |
StoreKit 2 purchase/restore |
MealMood/Services/ICloudSyncService.swift |
Cross-device sync (dishes, tags, plans — NOT isPremium) |
MealMood/Views/Home/WeekPlanShareView.swift |
Export styles (default, school, vertical A4) |
MealMoodWidget/MealMoodWidget.swift |
Home screen widget |
fastlane/Fastfile |
beta and release lanes |
fastlane/metadata/ |
App Store metadata in 6 languages |