Files
FamilyMealPlanner/MealMood_PRD.md
T
alexandrev-tibco e593453abd Version casi lista
2026-02-17 13:34:02 +01:00

2637 lines
96 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MealMood - Product Requirements Document (PRD)
## 📋 Índice
1. [Visión General](#visión-general)
2. [Problema y Solución](#problema-y-solución)
3. [Usuario Objetivo](#usuario-objetivo)
4. [Propuesta de Valor](#propuesta-de-valor)
5. [Arquitectura de Información](#arquitectura-de-información)
6. [Modelo de Datos](#modelo-de-datos)
7. [Funcionalidades MVP](#funcionalidades-mvp)
8. [Funcionalidades Freemium (Futuro)](#funcionalidades-freemium-futuro)
9. [Identidad Visual y Branding](#identidad-visual-y-branding)
10. [Especificaciones de Pantallas](#especificaciones-de-pantallas)
11. [Interacciones y Micro-animaciones](#interacciones-y-micro-animaciones)
12. [Integración con Calendario Nativo](#integración-con-calendario-nativo)
13. [Algoritmo de Autocompletado](#algoritmo-de-autocompletado)
14. [Sincronización y Persistencia](#sincronización-y-persistencia)
15. [Monetización](#monetización)
16. [Stack Técnico](#stack-técnico)
17. [Plan de Implementación (Sprints)](#plan-de-implementación-sprints)
18. [Anexos](#anexos)
---
## Visión General
### Nombre
**MealMood** 🍽️✨
### Tagline
- **Español**: "Planifica tu semana, disfruta tus comidas"
- **English**: "Plan your week, enjoy your meals"
### Descripción del Producto
MealMood es una aplicación iOS para planificar comidas y cenas semanales de manera rápida, visual y sin estrés. Orientada principalmente a familias (target: madres), permite organizar el menú semanal en 5 minutos usando drag & drop, respetando reglas nutricionales configurables y sincronizando opcionalmente con el calendario nativo de iOS.
**NO es un tracker de calorías ni macros**. El enfoque está en la simplicidad, variedad y disfrute de las comidas familiares.
---
## Problema y Solución
### Problema
- Planificar comidas semanales es una tarea recurrente que genera **estrés y fatiga decisional**.
- Las familias necesitan **variedad** sin repetir platos constantemente.
- Restricciones dietéticas (gluten, frecuencia de proteínas) complican la planificación.
- Apps existentes son **complejas** (trackers nutricionales) o requieren mucha configuración manual.
### Solución
MealMood ofrece:
1. **Planificación en 5 minutos**: interfaz visual con drag & drop.
2. **Reglas configurables**: limita frecuencia de ingredientes (carne, pescado, etc.) sin ser obsesivo.
3. **Autocompletado inteligente**: algoritmo rellena huecos respetando reglas.
4. **Sincronización con calendario**: eventos automáticos en el calendario nativo de iOS.
5. **Diseño acogedor**: tonos pastel, ilustraciones suaves, experiencia sin fricción.
---
## Usuario Objetivo
### Perfil Primario
- **Madres de familia** (25-45 años).
- Con hijos pequeños/adolescentes.
- Responsables de la planificación semanal de comidas.
- Buscan **eficiencia** sin perder variedad.
- No obsesionadas con calorías/macros, pero conscientes de dieta equilibrada.
### Perfil Secundario
- Padres que comparten la responsabilidad de cocinar.
- Personas organizadas que planifican con antelación.
- Familias con restricciones dietéticas leves (gluten, vegetarianos).
### Jobs to Be Done (JTBD)
*"Cuando llega el domingo por la tarde y necesito planificar la semana, quiero una forma rápida de decidir qué cocinar cada día sin repetir los mismos platos, para no estresarme y que mi familia tenga variedad."*
---
## Propuesta de Valor
### Diferenciadores clave
1. **Rapidez**: planificación en 5 minutos vs. 30+ en competencia.
2. **Visual**: interfaz calendario + drag & drop vs. listas aburridas.
3. **Reglas automáticas**: evita repeticiones sin pensar manualmente.
4. **No es un tracker nutricional**: enfoque en disfrute, no en restricción.
5. **Sincronización calendario**: eventos listos para compartir con la familia.
### Competencia
| App | Enfoque | Diferencia con MealMood |
|-----|---------|------------------------|
| **Paprika** | Recetas + listas de compra | Más complejo, orientado a recetas detalladas |
| **Mealime** | Planes nutricionales | Muy centrado en macros/calorías |
| **Plan to Eat** | Recetas + calendario | Requiere importar recetas, setup largo |
| **AnyList** | Listas de compra | No tiene planificación visual con reglas |
---
## Arquitectura de Información
### Flujo principal
```
Onboarding (primera vez)
Configuración inicial
├── Ventanas de comida (cenas / comidas / ambas)
├── ¿Incluir fines de semana?
└── Sincronización calendario (opcional)
Home (pantalla principal)
├── Calendario semanal (vista principal)
├── Drawer de platos disponibles
└── Botones: Completar / Resetear
Gestión de platos
├── Lista de platos
├── Crear nuevo plato
└── Editar/Eliminar platos
Gestión de etiquetas (Tags)
├── Lista de tags
├── Editar reglas de tags
└── Crear tags custom (Freemium)
Settings
├── Ventanas de comida
├── Fines de semana
├── Calendario (sincronización)
├── Idioma
└── Premium
```
---
## Modelo de Datos
### Entidades principales
#### 1. Settings
```swift
struct Settings {
// General
var mealWindows: MealWindows // .dinnerOnly, .lunchOnly, .both
var includeWeekends: Bool
var language: Language // .spanish, .english
// Calendario
var calendarId: String? // ID del calendario nativo, nil si no sincroniza
var syncEnabled: Bool
var lunchTime: Date // hora del día (default: 14:00)
var dinnerTime: Date // hora del día (default: 21:00)
var eventDuration: Int // minutos (default: 60)
var eventPrefix: String // default: "🍽"
var reminderMinutesBefore: Int? // nil = sin alerta, valores: 30, 60, 120
// Premium
var isPremium: Bool
}
enum MealWindows {
case dinnerOnly
case lunchOnly
case both
}
enum Language {
case spanish
case english
}
```
#### 2. Tag (Etiqueta)
```swift
struct Tag: Identifiable, Codable {
var id: UUID
var name: String // localizado según idioma
var color: String // hex (ej: "#E74C3C")
var rules: TagRules
}
struct TagRules: Codable {
var maxPerWeek: Int? // nil = sin límite
var noConsecutive: Bool // no puede estar en días consecutivos
var noDuplicateInDay: Bool // no puede estar en comida y cena el mismo día
var mealTypeRestriction: MealType? // .lunch, .dinner, nil
}
enum MealType: String, Codable {
case lunch
case dinner
}
```
#### 3. Dish (Plato)
```swift
struct Dish: Identifiable, Codable {
var id: UUID
var name: String
var description: String? // opcional
var tags: [UUID] // referencias a Tag IDs
var createdAt: Date
}
```
#### 4. MealSlot (Slot de comida)
```swift
struct MealSlot: Identifiable, Codable {
var id: UUID
var dayOfWeek: Int // 0=lunes, 6=domingo
var mealType: MealType // .lunch o .dinner
var dish: UUID? // nil si vacío
var calendarEventId: String? // ID del evento en EventKit, nil si no sincroniza
}
```
#### 5. WeekPlan (Planificación semanal)
```swift
struct WeekPlan: Identifiable, Codable {
var id: UUID
var weekStartDate: Date // lunes de esa semana
var slots: [MealSlot]
var createdAt: Date
var updatedAt: Date
}
```
---
## Funcionalidades MVP
### F1: Configuración Inicial (Onboarding)
**Descripción**: Primera vez que el usuario abre la app.
**Pasos**:
1. **Bienvenida**: logo + tagline + ilustración.
2. **Ventanas de comida**: selector (solo cenas / solo comidas / ambas).
3. **Fines de semana**: toggle (incluir o no).
4. **Calendario** (opcional): selector de calendario nativo + configuración de horas.
5. **Crear primeros platos**: formulario rápido para añadir 2-3 platos iniciales.
**Criterios de Aceptación**:
- Usuario puede completar onboarding en <2 minutos.
- Settings guardados en persistencia local (SwiftData/Core Data).
- Navegación fluida sin saltos bruscos.
---
### F2: Gestión de Platos
**Descripción**: Crear, editar y eliminar platos personales.
**Funcionalidad**:
- **Lista de platos**: muestra todos los platos del usuario.
- **Crear plato**:
- Campo: Nombre (obligatorio).
- Campo: Descripción (opcional).
- Selector: Etiquetas (multi-selección de tags disponibles).
- **Editar plato**: mismo formulario, pre-rellenado.
- **Eliminar plato**: swipe-to-delete + confirmación.
**Validaciones**:
- Nombre no puede estar vacío.
- No permitir eliminar plato si está asignado a un slot de la semana actual (mostrar alert).
**Criterios de Aceptación**:
- Usuario puede crear plato en <30 segundos.
- Cambios se reflejan inmediatamente en la lista de platos.
- Eliminación con confirmación para evitar errores.
---
### F3: Gestión de Etiquetas (Tags)
**Descripción**: Ver y editar reglas de etiquetas predefinidas.
**Funcionalidad**:
- **Lista de tags**: muestra tags por defecto (10 en MVP).
- **Editar reglas de tag**:
- Max por semana (slider 1-7 o "sin límite").
- Toggle: No consecutivo.
- Toggle: No duplicar en el mismo día.
- Selector: Restricción de tipo de comida (ninguna / solo comida / solo cena).
- **Ver color del tag**: visualización en lista con pill de color.
**Tags por defecto (MVP)**:
| Tag (ES) | Tag (EN) | Color | Reglas por defecto |
|----------|----------|-------|-------------------|
| Carne | Meat | #E74C3C | Max 3/semana, no consecutivo, no mismo día |
| Pescado | Fish | #3498DB | Max 2/semana, no consecutivo, no mismo día |
| Legumbres | Legumes | #95A5A6 | Max 2/semana, no mismo día |
| Verduras | Vegetables | #2ECC71 | Sin límite |
| Huevos | Eggs | #F1C40F | Max 2/semana, no mismo día |
| Pasta/Arroz | Pasta/Rice | #D4AC6E | Max 3/semana, no consecutivo |
| Solo Cena | Dinner Only | #9B59B6 | Restricción: solo cena |
| Solo Comida | Lunch Only | #E67E22 | Restricción: solo comida |
| Con Gluten | Contains Gluten | #8B4513 | Informativo (sin reglas) |
| Sin Gluten | Gluten Free | #1ABC9C | Informativo (sin reglas) |
**Criterios de Aceptación**:
- Usuario puede modificar reglas en <1 minuto.
- Cambios se aplican inmediatamente al algoritmo de validación.
- No permitir crear tags custom en MVP (solo Freemium).
---
### F4: Planificación Semanal (Pantalla Principal)
**Descripción**: Vista calendario con drag & drop para asignar platos a slots.
**Layout**:
```
┌─────────────────────────────────────────────┐
│ MealMood Semana 3-9 Feb [⚙️] │
│ │
│ ← Anterior Siguiente → │
├─────────────────────────────────────────────┤
│ │
│ Lun Mar Mié Jue Vie │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │🌙 │ │🌙 │ │🌙 │ │🌙 │ │🌙 │ │
│ │Pollo│ │Sal- │ │Torti│ │ │ │ │ │
│ │asado│ │món │ │lla │ │ │ │ │ │
│ │🔴 │ │🔵 │ │🟡 │ │ - - │ │ - - │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
├─────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Completar │ │ Resetear │ │
│ │ ✨ │ │ 🔄 │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────┘
▲ Arrastra platos aquí ▲
┌─────────────────────────────────────────────┐
│ 📋 Mis platos [+] │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ Pollo asado 🔴 │ │
│ │ Jugoso y al horno │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Salmón a la plancha 🔵 │ │
│ │ Con limón y eneldo │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
**Funcionalidad**:
- **Calendario semanal**:
- Muestra días según configuración (lun-dom si incluye fines de semana, lun-vie si no).
- Muestra slots según configuración (solo cena, solo comida, o ambas).
- **Slots vacíos**: placeholder visual con icono (☀️ para comida, 🌙 para cena) + texto "Arrastra un plato".
- **Slots llenos**: muestra nombre del plato + descripción corta + pills de colores (tags).
- **Drawer de platos**:
- Lista scrolleable de todos los platos creados.
- Card por plato: nombre + descripción + pills de tags.
- Botón [+] para crear nuevo plato (abre modal/sheet).
- **Drag & Drop**:
- Long press (200ms) en plato → se "levanta" con sombra.
- Arrastrar sobre slot:
- **Válido** (respeta reglas): borde verde, haptic feedback suave.
- **Inválido** (viola reglas): borde rojo, haptic feedback "error".
- Drop:
- **Válido**: plato se asigna al slot, desaparece temporalmente del drawer, animación "plop".
- **Inválido**: plato vuelve al drawer con bounce animation.
- **Navegación semanal**:
- Flechas ← → para cambiar de semana.
- Muestra rango de fechas (ej: "Semana 3-9 Feb").
- **Botones de acción**:
- **Completar ✨**: ejecuta algoritmo de autocompletado (ver F5).
- **Resetear 🔄**: limpia todos los slots de la semana (con confirmación).
**Criterios de Aceptación**:
- Usuario puede planificar semana completa en <5 minutos.
- Drag & drop fluido (60fps), sin lag.
- Validación de reglas en tiempo real (antes de drop).
- Navegación entre semanas sin perder datos.
---
### F5: Autocompletado Inteligente
**Descripción**: Algoritmo que rellena slots vacíos respetando reglas de tags.
**Funcionamiento**:
1. Usuario presiona botón "Completar ✨".
2. Algoritmo analiza:
- Slots vacíos en la semana actual.
- Platos disponibles (todos los creados por el usuario).
- Reglas de cada tag asociado a cada plato.
3. Para cada slot vacío:
- Filtra platos válidos (que no violen reglas).
- Prioriza platos menos usados en la semana actual (para variedad).
- Asigna plato aleatoriamente entre válidos.
4. Si no encuentra plato válido para un slot → lo deja vacío.
5. Muestra resultado con animación secuencial (slots se rellenan uno por uno).
**Lógica de validación**:
```python
def violates_rules(dish, slot, current_plan, rules):
for tag_id in dish.tags:
tag_rule = rules[tag_id]
# Check max per week
if tag_rule.maxPerWeek:
count = current_plan.count_tag_usage(tag_id)
if count >= tag_rule.maxPerWeek:
return True
# Check no consecutive
if tag_rule.noConsecutive:
adjacent_slots = current_plan.get_adjacent_slots(slot)
for adj_slot in adjacent_slots:
if adj_slot.dish and tag_id in adj_slot.dish.tags:
return True
# Check no duplicate in same day
if tag_rule.noDuplicateInDay:
same_day_slots = current_plan.get_same_day_slots(slot)
for sd_slot in same_day_slots:
if sd_slot.dish and tag_id in sd_slot.dish.tags:
return True
# Check meal type restriction
if tag_rule.mealTypeRestriction:
if slot.mealType != tag_rule.mealTypeRestriction:
return True
return False
```
**Criterios de Aceptación**:
- Algoritmo completa semana en <2 segundos.
- Respeta todas las reglas configuradas.
- Si no puede completar todos los slots, informa al usuario ("No puedo completar con las reglas actuales. ¿Deseas relajar restricciones?").
- Usuario puede manualmente sobrescribir asignaciones del algoritmo.
---
### F6: Sincronización con Calendario Nativo
**Descripción**: Crear eventos en el calendario nativo de iOS para cada comida planificada.
**Configuración** (Settings → Calendario):
- Toggle: Sincronizar (ON/OFF).
- Selector: Calendario nativo (lista de calendarios del dispositivo).
- Time picker: Hora de comidas (default: 14:00).
- Time picker: Hora de cenas (default: 21:00).
- Slider: Duración del evento (30min, 1h, 1.5h, 2h) - default: 1h.
- Text field: Prefijo del título (default: "🍽️").
- Selector: Recordatorio (sin recordatorio / 30min antes / 1h antes / 2h antes).
**Funcionamiento**:
1. **Al asignar plato a slot**:
- Si `syncEnabled == true` y `calendarId != nil`:
- Crear evento en EventKit:
- **Título**: `{eventPrefix} {mealType}: {dish.name}` (ej: "🍽️ Cena: Pollo asado").
- **Fecha**: `weekStartDate + dayOfWeek`.
- **Hora inicio**: `lunchTime` (si es comida) o `dinnerTime` (si es cena).
- **Hora fin**: `startDate + eventDuration`.
- **Notas**: `dish.description` (si existe).
- **Alerta**: según `reminderMinutesBefore`.
- Guardar `eventId` en `slot.calendarEventId`.
2. **Al eliminar plato de slot**:
- Si `slot.calendarEventId != nil`:
- Eliminar evento de EventKit usando `eventId`.
- Limpiar `slot.calendarEventId`.
- Mostrar toast: "Evento eliminado del calendario".
3. **Al cambiar hora en Settings**:
- Preguntar: "¿Actualizar eventos existentes?".
- Si Sí:
- Para cada `slot` con `calendarEventId != nil` y fecha >= hoy:
- Actualizar evento en EventKit con nueva hora.
**Permisos**:
- Solicitar permiso de EventKit al activar sincronización.
- Si usuario deniega permiso → mostrar alert explicando que necesita acceso para sincronizar (con link a Settings del sistema).
**Casos edge**:
- Si calendario sincronizado es eliminado del dispositivo → detectar y mostrar warning: "El calendario sincronizado ya no existe. Selecciona otro."
- Si el usuario edita el evento directamente en la app Calendario nativa → **ignorar cambios** (no sincronización bidireccional en MVP).
**Criterios de Aceptación**:
- Eventos creados correctamente con todos los campos (título, fecha, hora, notas, alerta).
- Eliminación de eventos al quitar plato del slot.
- Usuario puede desactivar sincronización en cualquier momento sin perder datos en la app.
---
### F7: Settings (Ajustes)
**Descripción**: Pantalla de configuración general.
**Secciones**:
#### 🍽️ Planificación
- **Ventanas de comida**: selector (Solo cenas / Solo comidas / Comidas y cenas).
- **Incluir fines de semana**: toggle (ON/OFF).
#### 📅 Calendario
- **Sincronizar**: toggle (ON/OFF).
- **Calendario**: selector de calendarios nativos (solo visible si toggle ON).
- **Hora de comidas**: time picker (default: 14:00).
- **Hora de cenas**: time picker (default: 21:00).
- **Duración del evento**: selector (30min, 1h, 1.5h, 2h).
- **Prefijo del título**: text field (default: "🍽️").
- **Recordatorio**: selector (Sin recordatorio / 30min antes / 1h antes / 2h antes).
#### 🏷️ Etiquetas
- **Gestionar etiquetas**: navegar a pantalla de gestión de tags.
#### 🌍 Idioma
- **Idioma**: selector (Español / English).
#### ⭐ Premium
- **Quitar publicidad**: navegar a pantalla de compra (IAP).
- **Estado**: mostrar si el usuario es Premium o Free.
**Criterios de Aceptación**:
- Cambios se guardan inmediatamente (auto-save).
- Cambio de idioma actualiza toda la interfaz (incluyendo tags predefinidos).
- Navegación fluida entre Settings y otras pantallas.
---
### F8: Persistencia Local
**Descripción**: Guardar datos del usuario en el dispositivo.
**Tecnología**: SwiftData (iOS 17+) o Core Data (si necesitas compatibilidad con iOS 16).
**Entidades persistentes**:
- Settings
- Tag (10 tags por defecto)
- Dish (platos creados por el usuario)
- WeekPlan (planificaciones semanales, al menos la actual y las próximas 2-3)
**Comportamiento**:
- Datos se guardan automáticamente en cada cambio (no requiere botón "Guardar").
- Al abrir la app, carga la planificación de la semana actual.
- Si no existe planificación para la semana actual, crea una nueva vacía.
**Criterios de Aceptación**:
- Datos persisten entre sesiones de la app.
- No hay pérdida de datos al cerrar/abrir la app.
- Performance: carga inicial <1 segundo.
---
### F9: Multiidioma (Español/Inglés)
**Descripción**: Soporte para español e inglés desde día 1.
**Implementación**:
- Usar `Localizable.strings` para textos de UI.
- Tags predefinidos tienen nombre localizado (ej: "Carne" en ES, "Meat" en EN).
- Cambio de idioma en Settings → actualiza toda la interfaz sin reiniciar la app.
**Textos a traducir**:
- Onboarding (bienvenida, pasos).
- Pantalla principal (botones, placeholders).
- Gestión de platos (formularios, validaciones).
- Gestión de tags (nombres, reglas).
- Settings (todas las opciones).
- Alerts y toasts.
**Criterios de Aceptación**:
- Usuario puede cambiar idioma en Settings.
- Cambio se refleja inmediatamente en toda la app.
- Tags predefinidos cambian de nombre según idioma.
---
### F10: Navegación entre Semanas
**Descripción**: Usuario puede planificar múltiples semanas (pasadas y futuras).
**Funcionalidad**:
- Flechas ← → en la pantalla principal.
- Mostrar rango de fechas de la semana actual (ej: "Semana 3-9 Feb 2025").
- Al cambiar de semana:
- Cargar planificación de esa semana (si existe).
- Si no existe, crear planificación vacía.
- **Limitación MVP**: solo se pueden editar semana actual y futuras (no pasadas).
**Criterios de Aceptación**:
- Navegación fluida sin lag.
- Cada semana tiene su propia planificación independiente.
- Usuario puede ver hasta 4 semanas futuras.
---
## Funcionalidades Freemium (Futuro)
### Freemium 1: Tags Custom Ilimitados
- Usuario puede crear tags personalizados con nombre, color y reglas.
- En Free: solo 10 tags predefinidos, no se pueden crear nuevos.
### Freemium 2: Platos Ilimitados
- Free: máximo 20 platos.
- Premium: ilimitados.
### Freemium 3: Reglas Avanzadas
- **Separación mínima**: "Este plato no puede repetirse hasta X días después".
- **Día fijo**: "Este plato siempre los viernes" (tradiciones familiares).
- En Free: solo reglas básicas (max/semana, no consecutivo, no mismo día).
### Freemium 4: Múltiples Planificaciones Guardadas
- Guardar "plantillas" de semanas (ej: "Semana típica", "Semana de invitados").
- Aplicar plantilla a cualquier semana futura.
### Freemium 5: Exportar a Lista de Compra
- Generar lista de ingredientes basada en la planificación semanal.
- Exportar a Notes, Reminders o apps de terceros (AnyList).
### Freemium 6: Analytics/Histórico
- Dashboard con estadísticas:
- "¿Qué platos repito más?"
- "¿Cuántas veces comí carne esta semana?"
- Gráficos de variedad semanal.
### Freemium 7: Colaboración Familiar
- Múltiples usuarios pueden editar la misma planificación.
- Notificaciones push: "Papá cambió la cena del jueves".
---
## Identidad Visual y Branding
### Logo
**Descripción conceptual**:
- **Forma**: circular o redondeada (amigable).
- **Icono**: plato con tenedor/cuchara cruzados, estilo line art minimalista.
- **Colores**: gradiente suave coral → menta (#FFB4A2#B8E6D5).
- **Líneas**: grosor 2-3px, esquinas redondeadas.
- **Opcional**: pequeño "sparkle" (✨) en la esquina superior derecha.
**Placeholder en código**:
```swift
struct AppIconPlaceholder: View {
var body: some View {
ZStack {
Circle()
.fill(
LinearGradient(
colors: [Color(hex: "#FFB4A2"), Color(hex: "#B8E6D5")],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
Image(systemName: "fork.knife.circle")
.font(.system(size: 60, weight: .light))
.foregroundColor(.white)
}
.frame(width: 120, height: 120)
.shadow(color: .black.opacity(0.1), radius: 10, y: 5)
}
}
```
---
### Paleta de Colores
```swift
// Colores principales
extension Color {
// Primary
static let mealMoodCoral = Color(hex: "#FFB4A2") // Coral pastel (acento cálido)
static let mealMoodMint = Color(hex: "#B8E6D5") // Menta claro (secundario)
// Background
static let mealMoodBackground = Color(hex: "#FFF9F5") // Crema muy claro
static let mealMoodSurface = Color.white // Blanco puro (cards)
// Text
static let mealMoodTextPrimary = Color(hex: "#2D2D2D") // Gris oscuro
static let mealMoodTextSecondary = Color(hex: "#6B6B6B") // Gris medio
// Feedback
static let mealMoodSuccess = Color(hex: "#A8D5BA") // Verde sage
static let mealMoodWarning = Color(hex: "#FFD6A5") // Melocotón
static let mealMoodError = Color(hex: "#FF9999") // Rojo suave
// Helpers
init(hex: String) {
let scanner = Scanner(string: hex)
scanner.currentIndex = hex.hasPrefix("#") ? hex.index(after: hex.startIndex) : hex.startIndex
var rgb: UInt64 = 0
scanner.scanHexInt64(&rgb)
let r = Double((rgb & 0xFF0000) >> 16) / 255.0
let g = Double((rgb & 0x00FF00) >> 8) / 255.0
let b = Double(rgb & 0x0000FF) / 255.0
self.init(red: r, green: g, blue: b)
}
}
```
---
### Tipografía
```swift
// Font system
extension Font {
// Headers
static let mealMoodH1 = Font.system(size: 28, weight: .bold, design: .rounded)
static let mealMoodH2 = Font.system(size: 20, weight: .semibold, design: .rounded)
static let mealMoodH3 = Font.system(size: 18, weight: .semibold, design: .rounded)
// Body
static let mealMoodBody = Font.system(size: 16, weight: .regular, design: .default)
static let mealMoodBodyBold = Font.system(size: 16, weight: .semibold, design: .default)
// Small
static let mealMoodCaption = Font.system(size: 12, weight: .medium, design: .default)
static let mealMoodSmall = Font.system(size: 14, weight: .regular, design: .default)
}
```
---
### Componentes UI Reutilizables
#### 1. Card Style (para slots y platos)
```swift
struct MealCardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.background(Color.mealMoodSurface)
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.05), radius: 8, y: 4)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
)
}
}
extension View {
func mealCardStyle() -> some View {
modifier(MealCardStyle())
}
}
```
#### 2. Tag Pill (visual de etiquetas)
```swift
struct TagPill: View {
let tag: Tag
var body: some View {
Text(tag.name)
.font(.mealMoodCaption)
.foregroundColor(.white)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(
Capsule()
.fill(Color(hex: tag.color))
)
}
}
```
#### 3. Empty Slot (placeholder visual)
```swift
struct EmptySlot: View {
let mealType: MealType
var body: some View {
VStack(spacing: 8) {
Image(systemName: mealType == .lunch ? "sun.max.fill" : "moon.stars.fill")
.font(.system(size: 24))
.foregroundColor(Color(hex: "#C4C4C4"))
Text(mealType == .lunch ? "Comida" : "Cena")
.font(.mealMoodSmall)
.foregroundColor(.mealMoodTextSecondary)
Text("Arrastra un plato")
.font(.mealMoodCaption)
.foregroundColor(Color(hex: "#A0A0A0"))
}
.frame(maxWidth: .infinity, minHeight: 100)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(hex: "#F9F9F9"))
.overlay(
RoundedRectangle(cornerRadius: 12)
.strokeBorder(
style: StrokeStyle(lineWidth: 2, dash: [5])
)
.foregroundColor(Color(hex: "#E0E0E0"))
)
)
}
}
```
#### 4. Filled Slot (con plato asignado)
```swift
struct FilledSlot: View {
let dish: Dish
let tags: [Tag]
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(dish.name)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.mealMoodTextPrimary)
.lineLimit(2)
if let description = dish.description {
Text(description)
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
.lineLimit(1)
}
HStack(spacing: 4) {
ForEach(tags.prefix(3)) { tag in
TagPill(tag: tag)
}
if tags.count > 3 {
Text("+\(tags.count - 3)")
.font(.system(size: 10, weight: .medium))
.foregroundColor(.mealMoodTextSecondary)
}
}
}
.padding(12)
.frame(maxWidth: .infinity, minHeight: 100, alignment: .topLeading)
.mealCardStyle()
}
}
```
#### 5. Primary Button
```swift
struct PrimaryButton: View {
let title: String
let icon: String?
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 8) {
Text(title)
.font(.mealMoodBodyBold)
if let icon = icon {
Text(icon)
}
}
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(
LinearGradient(
colors: [Color.mealMoodCoral, Color.mealMoodMint],
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(12)
.shadow(color: Color.mealMoodCoral.opacity(0.3), radius: 8, y: 4)
}
}
}
```
#### 6. Secondary Button
```swift
struct SecondaryButton: View {
let title: String
let icon: String?
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 8) {
Text(title)
.font(.mealMoodBodyBold)
if let icon = icon {
Text(icon)
}
}
.foregroundColor(.mealMoodTextPrimary)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(Color.mealMoodSurface)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.mealMoodTextSecondary.opacity(0.3), lineWidth: 1.5)
)
.cornerRadius(12)
}
}
}
```
---
## Especificaciones de Pantallas
### Pantalla 1: Onboarding
#### 1.1: Bienvenida
```
┌─────────────────────────────────────────────┐
│ │
│ [Logo MealMood placeholder] │
│ │
│ ✨ Planifica tu semana en 5 minutos │
│ │
│ [Ilustración: familia cenando feliz] │
│ │
│ • Sin estrés │
│ • Sin contar calorías │
│ • Sincroniza con tu calendario │
│ │
│ │
│ [Comenzar] ──────────► │
│ │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Logo placeholder (usar componente AppIconPlaceholder).
- Título: H1, centrado, coral.
- Ilustración: placeholder con SF Symbols (ej: "figure.2.and.child.holdinghands").
- Lista de beneficios: bullets con iconos.
- Botón primario: "Comenzar".
**Navegación**: tap en "Comenzar" → Onboarding paso 1.
---
#### 1.2: Paso 1 - Ventanas de Comida
```
┌─────────────────────────────────────────────┐
│ ¿Qué quieres planificar? │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 🌙 Solo cenas │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ ☀️ Solo comidas │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 🌞🌙 Comidas y cenas │ │
│ └─────────────────────────────────────┘ │
│ │
│ [Continuar] ────────► │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Título: H2, alineado a la izquierda.
- 3 cards seleccionables (radio button style).
- Botón primario: "Continuar" (solo habilitado si hay selección).
**Navegación**: tap en "Continuar" → Onboarding paso 2.
---
#### 1.3: Paso 2 - Fines de Semana
```
┌─────────────────────────────────────────────┐
│ ¿Incluir fines de semana? │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Planificar también sábados │ │
│ │ y domingos [ON] │ │
│ └─────────────────────────────────────┘ │
│ │
│ (Puedes cambiar esto después en ajustes) │
│ │
│ [Continuar] ────────► │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Título: H2.
- Toggle prominente.
- Nota aclaratoria (small text, gris).
- Botón primario: "Continuar".
**Navegación**: tap en "Continuar" → Onboarding paso 3.
---
#### 1.4: Paso 3 - Calendario (Opcional)
```
┌─────────────────────────────────────────────┐
│ ¿Sincronizar con tu calendario? │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Sincronizar [OFF] │ │
│ └─────────────────────────────────────┘ │
│ │
│ [Si activas sincronización:] │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Calendario │ │
│ │ Personal ▼ │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Hora de comidas 14:00 ⏰ │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Hora de cenas 21:00 ⏰ │ │
│ └─────────────────────────────────────┘ │
│ │
│ (Opcional, puedes activarlo después) │
│ │
│ [Omitir] [Continuar] ──────► │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Toggle: "Sincronizar".
- Si toggle ON → mostrar selector de calendario + time pickers.
- Botones: "Omitir" (secondary) y "Continuar" (primary).
**Navegación**:
- "Omitir" o "Continuar" → Onboarding paso 4.
- Si toggle ON, solicitar permiso de EventKit antes de continuar.
---
#### 1.5: Paso 4 - Crear Primeros Platos
```
┌─────────────────────────────────────────────┐
│ Añade tus primeros platos │
│ │
│ Añade al menos 2-3 platos para empezar │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Nombre del plato │ │
│ │ [Pollo asado ] │ │
│ │ │ │
│ │ Etiquetas │ │
│ │ 🔴 Carne [×] │ │
│ │ │ │
│ │ [+ Añadir plato] │ │
│ └─────────────────────────────────────┘ │
│ │
│ Platos añadidos: │
│ ✓ Pollo asado (🔴 Carne) │
│ │
│ [Finalizar] ────────► │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Formulario simple: nombre + selector de tags.
- Botón "Añadir plato" (añade a lista temporal).
- Lista de platos añadidos (con opción de eliminar).
- Botón "Finalizar" (habilitado cuando hay >= 2 platos).
**Navegación**: tap en "Finalizar" → Home (pantalla principal).
---
### Pantalla 2: Home (Pantalla Principal)
```
┌─────────────────────────────────────────────┐
│ MealMood Semana 3-9 Feb [⚙️] │
│ │
│ ← Anterior Siguiente → │
├─────────────────────────────────────────────┤
│ │
│ Lun Mar Mié Jue Vie │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │🌙 │ │🌙 │ │🌙 │ │🌙 │ │🌙 │ │
│ │Pollo│ │Sal- │ │Torti│ │ │ │ │ │
│ │asado│ │món │ │lla │ │ │ │ │ │
│ │🔴 │ │🔵 │ │🟡 │ │ - - │ │ - - │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
│ [Si incluye comida y cena:] │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │☀️ │ │☀️ │ │☀️ │ │☀️ │ │☀️ │ │
│ │Lente│ │ │ │Pasta│ │ │ │ │ │
│ │jas │ │ │ │🟡 │ │ │ │ │ │
│ │🟢🟤 │ │ - - │ │ │ │ - - │ │ - - │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
├─────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Completar │ │ Resetear │ │
│ │ ✨ │ │ 🔄 │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────┘
▲ Arrastra platos aquí ▲
┌─────────────────────────────────────────────┐
│ 📋 Mis platos [+] │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ Pollo asado 🔴 │ │
│ │ Jugoso y al horno │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Salmón a la plancha 🔵 │ │
│ │ Con limón y eneldo │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Lentejas 🟢🟤 │ │
│ │ Estofadas con verduras │ │
│ └─────────────────────────────────────┘ │
│ │
│ ... (scroll si hay más platos) │
│ │
│ [Banner AdMob - 50px altura] │
└─────────────────────────────────────────────┘
```
**Elementos**:
#### Header
- Título: "MealMood" (H2, bold).
- Rango de semana: "Semana 3-9 Feb" (body, gris).
- Botón Settings: icono ⚙️ (top-right).
#### Navegación Semanal
- Flechas: ← Anterior / Siguiente →.
- Centrado en el rango de fechas.
#### Calendario Semanal
- **Grid de slots**:
- Orientación: horizontal scroll si incluye fines de semana (7 columnas).
- Orientación: vertical scroll si incluye comida+cena (2 filas por día).
- **Slot vacío**: usar componente `EmptySlot`.
- **Slot lleno**: usar componente `FilledSlot`.
#### Botones de Acción
- **Completar ✨**: PrimaryButton.
- **Resetear 🔄**: SecondaryButton.
#### Drawer de Platos
- Lista scrolleable vertical.
- Card por plato (usar `mealCardStyle`).
- Botón [+]: circular, floating, color coral.
#### AdMob Banner (si Free)
- Altura: 50px.
- Background: mismo color que la app (#FFF9F5).
- Sticky al fondo del drawer.
**Interacciones**:
- **Long press en plato** → drag & drop.
- **Tap en slot lleno** → opción de eliminar plato.
- **Tap en [+]** → abrir sheet "Crear Plato".
- **Tap en ⚙️** → navegar a Settings.
- **Tap en "Completar"** → ejecutar algoritmo de autocompletado.
- **Tap en "Resetear"** → mostrar alert de confirmación.
---
### Pantalla 3: Crear/Editar Plato
```
┌─────────────────────────────────────────────┐
│ ← Nuevo plato [✓] │
├─────────────────────────────────────────────┤
│ │
│ Nombre del plato * │
│ ┌─────────────────────────────────────┐ │
│ │ Pollo asado │ │
│ └─────────────────────────────────────┘ │
│ │
│ Descripción (opcional) │
│ ┌─────────────────────────────────────┐ │
│ │ Jugoso y al horno con hierbas │ │
│ └─────────────────────────────────────┘ │
│ │
│ Etiquetas * │
│ ┌──────────────────────────────────┐ │
│ │ 🔴 Carne [×] │ │
│ │ 🟢 Verduras [×] │ │
│ └──────────────────────────────────┘ │
│ │
│ [+ Añadir etiqueta] │
│ │
│ │
│ │
│ [Eliminar plato] (solo en modo edición) │
└─────────────────────────────────────────────┘
```
**Elementos**:
- **Header**:
- Botón back: ← (top-left).
- Título: "Nuevo plato" (crear) o "Editar plato" (editar).
- Botón guardar: ✓ (top-right, habilitado solo si campos válidos).
- **Formulario**:
- Campo: Nombre (obligatorio, TextField).
- Campo: Descripción (opcional, TextEditor, max 100 caracteres).
- Lista de tags seleccionados (pills con botón [×] para eliminar).
- Botón "Añadir etiqueta" → abre sheet selector de tags.
- **Botón Eliminar** (solo en modo edición):
- SecondaryButton, color rojo.
- Mostrar alert de confirmación.
**Validaciones**:
- Nombre no puede estar vacío.
- Debe tener al menos 1 etiqueta.
**Navegación**:
- Tap en [✓] → guardar y cerrar sheet.
- Tap en ← → descartar cambios (mostrar alert si hay cambios sin guardar).
---
#### 3.1: Sheet Selector de Tags
```
┌─────────────────────────────────────────────┐
│ Selecciona etiquetas [×] │
├─────────────────────────────────────────────┤
│ │
│ 🔴 Carne [✓] │
│ 🔵 Pescado [ ] │
│ 🟢 Verduras [✓] │
│ 🟡 Huevos [ ] │
│ 🟤 Legumbres [ ] │
│ 🟠 Pasta/Arroz [ ] │
│ 🟣 Solo Cena [ ] │
│ 🟠 Solo Comida [ ] │
│ 🟤 Con Gluten [ ] │
│ 🟢 Sin Gluten [ ] │
│ │
│ [Listo] │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Lista de todos los tags disponibles.
- Checkbox por tag (multi-selección).
- Color pill al lado del nombre.
- Botón "Listo" (primary).
**Navegación**: tap en "Listo" → cerrar sheet, actualizar lista de tags seleccionados.
---
### Pantalla 4: Gestión de Platos
```
┌─────────────────────────────────────────────┐
│ ← Mis platos [+] │
├─────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Pollo asado 🔴 [›] │ │
│ │ Jugoso y al horno │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Salmón a la plancha 🔵 [] │ │
│ │ Con limón y eneldo │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Lentejas 🟢🟤 [›] │ │
│ │ Estofadas con verduras │ │
│ └─────────────────────────────────────┘ │
│ │
│ ... (scroll si hay más platos) │
│ │
└─────────────────────────────────────────────┘
```
**Elementos**:
- **Header**:
- Botón back: ← (top-left).
- Título: "Mis platos".
- Botón [+]: crear nuevo plato (top-right).
- **Lista de platos**:
- Card por plato (mealCardStyle).
- Tap en card → abrir sheet "Editar Plato".
- Swipe left → botón "Eliminar" (rojo).
**Interacciones**:
- **Tap en plato** → abrir sheet edición.
- **Swipe left** → mostrar botón "Eliminar".
- **Tap en "Eliminar"** → mostrar alert de confirmación.
- **Tap en [+]** → abrir sheet "Crear Plato".
---
### Pantalla 5: Gestión de Etiquetas
```
┌─────────────────────────────────────────────┐
│ ← Etiquetas │
├─────────────────────────────────────────────┤
│ │
│ 🔴 Carne [] │
│ Max 3/semana, no consecutivo │
│ │
│ 🔵 Pescado [] │
│ Max 2/semana, no consecutivo │
│ │
│ 🟢 Verduras [] │
│ Sin límite │
│ │
│ 🟡 Huevos [] │
│ Max 2/semana, no mismo día │
│ │
│ ... (resto de tags) │
│ │
│ [+ Crear etiqueta] (solo Premium) │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Lista de tags con resumen de reglas.
- Tap en tag → abrir pantalla "Editar Reglas de Tag".
- Botón [+ Crear etiqueta] (disabled en Free, enabled en Premium).
**Navegación**: tap en tag → Pantalla 5.1.
---
#### 5.1: Editar Reglas de Tag
```
┌─────────────────────────────────────────────┐
│ ← Carne [✓] │
├─────────────────────────────────────────────┤
│ │
│ 🔴 Carne │
│ │
│ Máximo por semana │
│ ┌──────────────────────────────────┐ │
│ │ [1] [2] [3] [4] [5] [6] [7] [∞] │ │
│ └──────────────────────────────────┘ │
│ Seleccionado: 3 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ No consecutivo [ON] │ │
│ └──────────────────────────────────┘ │
│ Evita que aparezca días seguidos │
│ │
│ ┌──────────────────────────────────┐ │
│ │ No repetir en el mismo día [ON] │ │
│ └──────────────────────────────────┘ │
│ No puede estar en comida y cena el mismo │
│ día │
│ │
│ Restricción de horario │
│ ┌──────────────────────────────────┐ │
│ │ Sin restricción ▼ │ │
│ └──────────────────────────────────┘ │
│ Opciones: Sin restricción / Solo comida / │
│ Solo cena │
│ │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Pill de color del tag (centrado).
- **Max por semana**: segmented control (1-7, ∞).
- **Toggles**: no consecutivo, no mismo día.
- **Restricción horario**: picker (sin restricción / solo comida / solo cena).
- Botón [✓] (top-right): guardar y volver.
**Navegación**: tap en [✓] → guardar y volver a lista de tags.
---
### Pantalla 6: Settings
```
┌─────────────────────────────────────────────┐
│ ← Ajustes │
├─────────────────────────────────────────────┤
│ │
│ 🍽️ PLANIFICACIÓN │
│ │
│ Ventanas de comida │
│ ┌──────────────────────────────────┐ │
│ │ Solo cenas ▼ │ │
│ └──────────────────────────────────┘ │
│ │
│ Incluir fines de semana │
│ ┌──────────────────────────────────┐ │
│ │ [ON] │ │
│ └──────────────────────────────────┘ │
│ │
│ ───────────────────────────────────── │
│ │
│ 📅 CALENDARIO │
│ │
│ Sincronizar │
│ ┌──────────────────────────────────┐ │
│ │ [OFF] │ │
│ └──────────────────────────────────┘ │
│ │
│ [Si ON, mostrar:] │
│ Calendario │
│ ┌──────────────────────────────────┐ │
│ │ Personal ▼ │ │
│ └──────────────────────────────────┘ │
│ │
│ Hora de comidas │
│ ┌──────────────────────────────────┐ │
│ │ 14:00 ⏰ │ │
│ └──────────────────────────────────┘ │
│ │
│ Hora de cenas │
│ ┌──────────────────────────────────┐ │
│ │ 21:00 ⏰ │ │
│ └──────────────────────────────────┘ │
│ │
│ Duración del evento │
│ ┌──────────────────────────────────┐ │
│ │ 1 hora ▼ │ │
│ └──────────────────────────────────┘ │
│ │
│ Prefijo del título │
│ ┌──────────────────────────────────┐ │
│ │ 🍽️ │ │
│ └──────────────────────────────────┘ │
│ │
│ Recordatorio │
│ ┌──────────────────────────────────┐ │
│ │ Sin recordatorio ▼ │ │
│ └──────────────────────────────────┘ │
│ │
│ ───────────────────────────────────── │
│ │
│ 🏷️ ETIQUETAS │
│ │
│ Gestionar etiquetas [] │
│ │
│ ───────────────────────────────────── │
│ │
│ 🌍 IDIOMA │
│ │
│ Idioma │
│ ┌──────────────────────────────────┐ │
│ │ Español ▼ │ │
│ └──────────────────────────────────┘ │
│ │
│ ───────────────────────────────────── │
│ │
│ ⭐ PREMIUM │
│ │
│ Estado: Free │
│ Quitar publicidad [] │
│ │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Secciones separadas por headers (emoji + título).
- Separadores visuales (línea gris claro).
- Controles nativos de iOS (Toggle, Picker, TextField).
- Navegación a sub-pantallas con [].
**Navegación**:
- **Gestionar etiquetas** → Pantalla 5.
- **Quitar publicidad** → Pantalla de IAP (In-App Purchase).
---
### Pantalla 7: Premium / IAP
```
┌─────────────────────────────────────────────┐
│ ← Premium [×] │
├─────────────────────────────────────────────┤
│ │
│ ✨ MealMood Premium ✨ │
│ │
│ Disfruta de todas las funciones │
│ │
│ ✓ Sin publicidad │
│ ✓ Platos ilimitados │
│ ✓ Etiquetas personalizadas │
│ ✓ Reglas avanzadas (próximamente) │
│ │
│ ───────────────────────────────────── │
│ │
│ Planes disponibles: │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Mensual │ │
│ │ $2.99 / mes │ │
│ │ [Suscribirse] │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Anual │ │
│ │ $19.99 / año (ahorra 44%) │ │
│ │ [Suscribirse] │ │
│ └─────────────────────────────────────┘ │
│ │
│ [Restaurar compras] │
│ │
└─────────────────────────────────────────────┘
```
**Elementos**:
- Header con título y botón cerrar [×].
- Logo premium (placeholder con ✨).
- Lista de beneficios con checkmarks.
- Cards de planes (mensual / anual).
- Botones "Suscribirse" (StoreKit).
- Link "Restaurar compras".
**Navegación**:
- Tap en "Suscribirse" → StoreKit purchase flow.
- Tap en [×] → cerrar sheet.
---
## Interacciones y Micro-animaciones
### 1. Drag & Drop
**Gestos**:
1. **Long press** (200ms) en card de plato del drawer:
- Card se "levanta" con escala 1.05.
- Sombra aumenta (radius: 12, y: 8).
- Haptic feedback: `.light`.
2. **Drag** sobre slot:
- **Válido** (respeta reglas):
- Slot muestra borde verde (#A8D5BA, 2px).
- Haptic feedback: `.light` (suave, continuo cada 100ms).
- **Inválido** (viola reglas):
- Slot muestra borde rojo (#FF9999, 2px).
- Haptic feedback: `.rigid` (fuerte, único).
3. **Drop**:
- **Válido**:
- Animación "plop": slot hace scale 1.0 → 1.1 → 1.0 (duration: 0.3s).
- Plato desaparece del drawer con fade out.
- Haptic feedback: `.success`.
- **Inválido**:
- Plato vuelve al drawer con spring animation (bouncy).
- Haptic feedback: `.error`.
**Código ejemplo (SwiftUI)**:
```swift
.onLongPressGesture(minimumDuration: 0.2) {
// Iniciar drag
withAnimation(.spring()) {
draggedDish = dish
isLifted = true
}
HapticManager.shared.impact(style: .light)
}
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation
validateDrop(at: value.location)
}
.onEnded { value in
performDrop(at: value.location)
}
)
```
---
### 2. Completar Automáticamente
**Flujo**:
1. Usuario tap en botón "Completar ✨".
2. Botón muestra loading indicator (ProgressView, color coral).
3. Algoritmo ejecuta en background (no bloquea UI).
4. Animación secuencial:
- Slots vacíos se rellenan uno por uno (delay: 150ms entre cada uno).
- Cada slot hace fade in + scale animation (0.8 → 1.0).
5. Al terminar:
- Confetti suave (partículas pequeñas, colores pastel, duration: 2s).
- Haptic feedback: `.success`.
- Toast: "¡Listo! Tu semana está completa 🎉" (auto-dismiss en 3s).
**Código ejemplo (SwiftUI)**:
```swift
func autoComplete() {
isLoading = true
Task {
let filledSlots = await MealPlanner.shared.autoComplete(weekPlan: weekPlan)
for (index, slot) in filledSlots.enumerated() {
try? await Task.sleep(nanoseconds: 150_000_000) // 150ms
withAnimation(.spring()) {
weekPlan.slots[slot.id] = slot
}
HapticManager.shared.impact(style: .light)
}
isLoading = false
showConfetti = true
HapticManager.shared.notification(type: .success)
showToast = true
}
}
```
---
### 3. Resetear
**Flujo**:
1. Usuario tap en botón "Resetear 🔄".
2. Alert suave (no modal agresivo):
```
┌─────────────────────────────────────┐
│ ¿Limpiar la semana completa? │
│ │
│ Esto no se puede deshacer │
│ │
│ [Cancelar] [Limpiar] │
└─────────────────────────────────────┘
```
3. Si usuario confirma:
- Todos los slots vacíos hacen fade out (simultáneo, duration: 0.3s).
- Haptic feedback: `.warning`.
- Platos vuelven al drawer con fade in (staggered, delay: 50ms entre cada uno).
**Código ejemplo (SwiftUI)**:
```swift
func resetWeek() {
withAnimation(.easeOut(duration: 0.3)) {
for slot in weekPlan.slots {
slot.dish = nil
}
}
HapticManager.shared.notification(type: .warning)
}
```
---
### 4. Micro-interactions Adicionales
#### 4.1: Empty State (sin platos)
- Ilustración de familia con plato vacío.
- Texto: "Añade tu primer plato para empezar".
- Botón "Añadir plato" (primary, centrado).
#### 4.2: Pull to Refresh (en lista de platos)
- Arrastrar hacia abajo → loading indicator.
- Aunque no haya backend, simular recarga con delay 1s.
#### 4.3: Swipe to Delete (en lista de platos)
- Swipe left → botón "Eliminar" (rojo, icono 🗑️).
- Tap en eliminar → animación de desaparición (slide left + fade out).
#### 4.4: Toast Notifications
- Posición: top-center.
- Background: coral suave con opacity 0.95.
- Sombra suave.
- Auto-dismiss en 3s.
- Ejemplos:
- "Plato guardado ✓"
- "Evento eliminado del calendario"
- "No puedo completar con las reglas actuales"
---
## Integración con Calendario Nativo
### Permisos
**Solicitar permiso** al activar sincronización por primera vez:
```swift
import EventKit
let eventStore = EKEventStore()
func requestCalendarAccess() async -> Bool {
do {
return try await eventStore.requestFullAccessToEvents()
} catch {
print("Error requesting calendar access: \(error)")
return false
}
}
```
**Si usuario deniega**:
- Mostrar alert: "MealMood necesita acceso a tu calendario para sincronizar eventos. Ve a Ajustes > MealMood > Calendario."
- Link directo a Settings del sistema: `UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)`
---
### Crear Evento
**Función**:
```swift
func createEvent(
slot: MealSlot,
dish: Dish,
weekStartDate: Date,
settings: Settings
) -> String? {
guard let calendarId = settings.calendarId,
let calendar = eventStore.calendar(withIdentifier: calendarId) else {
return nil
}
let event = EKEvent(eventStore: eventStore)
// Título
let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) "
let mealName = slot.mealType == .lunch ? "Comida" : "Cena"
event.title = "\(prefix)\(mealName): \(dish.name)"
// Fecha y hora
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
let slotTime = slot.mealType == .lunch ? settings.lunchTime : settings.dinnerTime
event.startDate = combine(date: slotDate, time: slotTime)
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
// Nota
event.notes = dish.description
// Calendario
event.calendar = calendar
// Alerta
if let reminderMinutes = settings.reminderMinutesBefore {
let alarm = EKAlarm(relativeOffset: -TimeInterval(reminderMinutes * 60))
event.addAlarm(alarm)
}
// Guardar
do {
try eventStore.save(event, span: .thisEvent)
return event.eventIdentifier
} catch {
print("Error saving event: \(error)")
return nil
}
}
```
---
### Eliminar Evento
**Función**:
```swift
func deleteEvent(eventId: String) {
guard let event = eventStore.event(withIdentifier: eventId) else {
return
}
do {
try eventStore.remove(event, span: .thisEvent)
} catch {
print("Error deleting event: \(error)")
}
}
```
---
### Actualizar Eventos (cambio de hora)
**Función**:
```swift
func updateEventsTime(weekPlan: WeekPlan, newLunchTime: Date, newDinnerTime: Date) {
for slot in weekPlan.slots {
guard let eventId = slot.calendarEventId,
let event = eventStore.event(withIdentifier: eventId),
event.startDate >= Date() else { // solo futuros
continue
}
// Actualizar hora
let newTime = slot.mealType == .lunch ? newLunchTime : newDinnerTime
let newStartDate = combine(date: event.startDate, time: newTime)
event.startDate = newStartDate
event.endDate = newStartDate.addingMinutes(settings.eventDuration)
// Guardar
do {
try eventStore.save(event, span: .thisEvent)
} catch {
print("Error updating event: \(error)")
}
}
}
```
---
### Helpers
```swift
extension Date {
func addingDays(_ days: Int) -> Date {
Calendar.current.date(byAdding: .day, value: days, to: self)!
}
func addingMinutes(_ minutes: Int) -> Date {
Calendar.current.date(byAdding: .minute, value: minutes, to: self)!
}
}
func combine(date: Date, time: Date) -> Date {
let calendar = Calendar.current
let dateComponents = calendar.dateComponents([.year, .month, .day], from: date)
let timeComponents = calendar.dateComponents([.hour, .minute], from: time)
var combined = DateComponents()
combined.year = dateComponents.year
combined.month = dateComponents.month
combined.day = dateComponents.day
combined.hour = timeComponents.hour
combined.minute = timeComponents.minute
return calendar.date(from: combined)!
}
```
---
## Algoritmo de Autocompletado
### Pseudocódigo
```python
def autocomplete(week_plan, available_dishes, tags_rules):
"""
Rellena slots vacíos respetando reglas de tags.
Args:
week_plan: WeekPlan actual
available_dishes: [Dish] todos los platos del usuario
tags_rules: {tag_id: TagRules} reglas de cada tag
Returns:
WeekPlan actualizado
"""
empty_slots = week_plan.get_empty_slots()
for slot in empty_slots:
# Filtrar platos válidos
candidates = filter_valid_dishes(
dishes=available_dishes,
slot=slot,
current_plan=week_plan,
rules=tags_rules
)
if len(candidates) == 0:
continue # dejar slot vacío
# Priorizar platos menos usados esta semana
dish = weighted_random_pick(candidates, week_plan)
week_plan.assign(slot, dish)
return week_plan
def filter_valid_dishes(dishes, slot, current_plan, rules):
"""Filtra platos que no violen reglas."""
valid = []
for dish in dishes:
if not violates_rules(dish, slot, current_plan, rules):
valid.append(dish)
return valid
def violates_rules(dish, slot, current_plan, rules):
"""
Verifica si asignar este plato al slot viola alguna regla.
Returns:
True si viola alguna regla, False si es válido
"""
for tag_id in dish.tags:
tag_rule = rules[tag_id]
# Regla 1: Max por semana
if tag_rule.maxPerWeek is not None:
count = current_plan.count_tag_usage(tag_id)
if count >= tag_rule.maxPerWeek:
return True
# Regla 2: No consecutivo
if tag_rule.noConsecutive:
adjacent_slots = current_plan.get_adjacent_slots(slot)
for adj_slot in adjacent_slots:
if adj_slot.dish and tag_id in adj_slot.dish.tags:
return True
# Regla 3: No duplicar en el mismo día
if tag_rule.noDuplicateInDay:
same_day_slots = current_plan.get_same_day_slots(slot)
for sd_slot in same_day_slots:
if sd_slot.dish and tag_id in sd_slot.dish.tags:
return True
# Regla 4: Restricción de tipo de comida
if tag_rule.mealTypeRestriction is not None:
if slot.mealType != tag_rule.mealTypeRestriction:
return True
return False
def weighted_random_pick(candidates, current_plan):
"""
Elige un plato aleatoriamente, priorizando los menos usados.
Lógica:
- Platos con 0 usos: peso 3
- Platos con 1 uso: peso 2
- Platos con 2+ usos: peso 1
"""
weights = []
for dish in candidates:
usage_count = current_plan.count_dish_usage(dish.id)
if usage_count == 0:
weight = 3
elif usage_count == 1:
weight = 2
else:
weight = 1
weights.append(weight)
# Random weighted choice
import random
return random.choices(candidates, weights=weights)[0]
```
---
### Helpers
```swift
extension WeekPlan {
func getEmptySlots() -> [MealSlot] {
slots.filter { $0.dish == nil }
}
func countTagUsage(_ tagId: UUID) -> Int {
var count = 0
for slot in slots {
if let dishId = slot.dish,
let dish = DishRepository.shared.getDish(by: dishId),
dish.tags.contains(tagId) {
count += 1
}
}
return count
}
func countDishUsage(_ dishId: UUID) -> Int {
slots.filter { $0.dish == dishId }.count
}
func getAdjacentSlots(_ slot: MealSlot) -> [MealSlot] {
var adjacent: [MealSlot] = []
// Día anterior, mismo tipo de comida
if slot.dayOfWeek > 0 {
if let prevSlot = slots.first(where: {
$0.dayOfWeek == slot.dayOfWeek - 1 && $0.mealType == slot.mealType
}) {
adjacent.append(prevSlot)
}
}
// Día siguiente, mismo tipo de comida
if slot.dayOfWeek < 6 {
if let nextSlot = slots.first(where: {
$0.dayOfWeek == slot.dayOfWeek + 1 && $0.mealType == slot.mealType
}) {
adjacent.append(nextSlot)
}
}
return adjacent
}
func getSameDaySlots(_ slot: MealSlot) -> [MealSlot] {
slots.filter {
$0.dayOfWeek == slot.dayOfWeek && $0.id != slot.id
}
}
}
```
---
## Sincronización y Persistencia
### SwiftData (iOS 17+)
#### Configuración
```swift
import SwiftData
@main
struct MealMoodApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [
Settings.self,
Tag.self,
Dish.self,
WeekPlan.self,
MealSlot.self
])
}
}
```
#### Modelo de Datos
```swift
@Model
final class Settings {
var mealWindows: String // "dinnerOnly", "lunchOnly", "both"
var includeWeekends: Bool
var language: String // "spanish", "english"
var calendarId: String?
var syncEnabled: Bool
var lunchTime: Date
var dinnerTime: Date
var eventDuration: Int
var eventPrefix: String
var reminderMinutesBefore: Int?
var isPremium: Bool
init() {
self.mealWindows = "dinnerOnly"
self.includeWeekends = true
self.language = "spanish"
self.calendarId = nil
self.syncEnabled = false
self.lunchTime = Date() // set to 14:00
self.dinnerTime = Date() // set to 21:00
self.eventDuration = 60
self.eventPrefix = "🍽️"
self.reminderMinutesBefore = nil
self.isPremium = false
}
}
@Model
final class Tag {
var id: UUID
var name: String
var color: String
var maxPerWeek: Int?
var noConsecutive: Bool
var noDuplicateInDay: Bool
var mealTypeRestriction: String? // "lunch", "dinner", nil
init(id: UUID = UUID(), name: String, color: String, maxPerWeek: Int? = nil, noConsecutive: Bool = false, noDuplicateInDay: Bool = false, mealTypeRestriction: String? = nil) {
self.id = id
self.name = name
self.color = color
self.maxPerWeek = maxPerWeek
self.noConsecutive = noConsecutive
self.noDuplicateInDay = noDuplicateInDay
self.mealTypeRestriction = mealTypeRestriction
}
}
@Model
final class Dish {
var id: UUID
var name: String
var descriptionText: String?
var tagIds: [UUID]
var createdAt: Date
init(id: UUID = UUID(), name: String, descriptionText: String? = nil, tagIds: [UUID] = [], createdAt: Date = Date()) {
self.id = id
self.name = name
self.descriptionText = descriptionText
self.tagIds = tagIds
self.createdAt = createdAt
}
}
@Model
final class WeekPlan {
var id: UUID
var weekStartDate: Date
var createdAt: Date
var updatedAt: Date
@Relationship(deleteRule: .cascade)
var slots: [MealSlot]
init(id: UUID = UUID(), weekStartDate: Date, slots: [MealSlot] = [], createdAt: Date = Date(), updatedAt: Date = Date()) {
self.id = id
self.weekStartDate = weekStartDate
self.slots = slots
self.createdAt = createdAt
self.updatedAt = updatedAt
}
}
@Model
final class MealSlot {
var id: UUID
var dayOfWeek: Int
var mealType: String // "lunch", "dinner"
var dishId: UUID?
var calendarEventId: String?
init(id: UUID = UUID(), dayOfWeek: Int, mealType: String, dishId: UUID? = nil, calendarEventId: String? = nil) {
self.id = id
self.dayOfWeek = dayOfWeek
self.mealType = mealType
self.dishId = dishId
self.calendarEventId = calendarEventId
}
}
```
---
### CloudKit (Sincronización iCloud)
#### Configuración
1. Habilitar CloudKit en Xcode:
- Target → Signing & Capabilities → + Capability → iCloud.
- Activar "CloudKit".
- Crear container (ej: `iCloud.com.mealMood.app`).
2. Configurar SwiftData para usar CloudKit:
```swift
@main
struct MealMoodApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(
for: [Settings.self, Tag.self, Dish.self, WeekPlan.self, MealSlot.self],
isStoredInMemoryOnly: false,
isAutosaveEnabled: true,
isUndoEnabled: false,
cloudKitContainerIdentifier: "iCloud.com.mealMood.app"
)
}
}
```
**Comportamiento**:
- Datos se sincronizan automáticamente entre dispositivos del mismo usuario.
- Sincronización en background (no requiere acción del usuario).
- Conflictos resueltos por "last write wins" (por defecto).
---
## Monetización
### Free Tier
**Limitaciones**:
- Máximo 20 platos.
- Solo 10 tags predefinidos (no se pueden crear custom).
- Solo reglas básicas (max/semana, no consecutivo, no mismo día).
- Banner AdMob en parte inferior de Home.
**AdMob Configuración**:
```swift
import GoogleMobileAds
struct AdBannerView: UIViewRepresentable {
func makeUIView(context: Context) -> GADBannerView {
let banner = GADBannerView(adSize: GADAdSizeBanner)
banner.adUnitID = "ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX" // reemplazar con tu ID
banner.rootViewController = UIApplication.shared.windows.first?.rootViewController
banner.load(GADRequest())
return banner
}
func updateUIView(_ uiView: GADBannerView, context: Context) {}
}
```
**Ubicación**:
- Home → parte inferior, sticky.
- Background: #FFF9F5 (mismo color que la app).
- Altura: 50px.
- NO en otras pantallas (Settings, gestión de platos, etc.).
---
### Premium Tier
**Precio**:
- Mensual: $2.99/mes.
- Anual: $19.99/año (ahorro 44%).
**Beneficios**:
- Sin publicidad.
- Platos ilimitados.
- Tags custom ilimitados.
- (Futuro) Reglas avanzadas.
- (Futuro) Múltiples planificaciones guardadas.
- (Futuro) Exportar a lista de compra.
- (Futuro) Analytics/histórico.
**IAP Configuración**:
```swift
import StoreKit
class StoreManager: ObservableObject {
@Published var products: [Product] = []
@Published var isPremium: Bool = false
private let productIds = [
"com.mealmood.premium.monthly",
"com.mealmood.premium.yearly"
]
init() {
Task {
await loadProducts()
await checkPremiumStatus()
}
}
func loadProducts() async {
do {
products = try await Product.products(for: productIds)
} catch {
print("Failed to load products: \(error)")
}
}
func purchase(_ product: Product) async {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
isPremium = true
case .unverified:
break
}
case .userCancelled, .pending:
break
@unknown default:
break
}
} catch {
print("Purchase failed: \(error)")
}
}
func restorePurchases() async {
do {
try await AppStore.sync()
await checkPremiumStatus()
} catch {
print("Restore failed: \(error)")
}
}
private func checkPremiumStatus() async {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result,
productIds.contains(transaction.productID) {
isPremium = true
return
}
}
isPremium = false
}
}
```
---
## Stack Técnico
### Plataforma
- **iOS**: mínimo iOS 17.0 (para SwiftData).
- **Dispositivos**: iPhone (optimizado para todos los tamaños).
- **Orientación**: Portrait only (landscape no necesario para MVP).
### Lenguajes y Frameworks
- **Swift**: 5.9+.
- **SwiftUI**: para toda la UI.
- **SwiftData**: persistencia local.
- **EventKit**: integración con calendario nativo.
- **CloudKit**: sincronización iCloud.
- **StoreKit 2**: IAP (In-App Purchases).
### Dependencias Externas
- **Google Mobile Ads SDK**: para AdMob.
- Instalación: Swift Package Manager (SPM) o CocoaPods.
- URL: `https://github.com/googleads/swift-package-manager-google-mobile-ads`
### Arquitectura
- **MVVM** (Model-View-ViewModel):
- **Models**: Settings, Tag, Dish, WeekPlan, MealSlot (SwiftData models).
- **ViewModels**: HomeViewModel, DishViewModel, SettingsViewModel, etc.
- **Views**: SwiftUI views.
### Testing
- **Unit tests**: lógica de algoritmo de autocompletado, validación de reglas.
- **UI tests**: flujos críticos (onboarding, drag & drop, crear plato).
---
## Plan de Implementación (Sprints)
### Sprint 1: Setup e Infraestructura (1 semana)
- Configurar proyecto Xcode.
- Configurar SwiftData models.
- Configurar CloudKit.
- Crear paleta de colores y componentes UI reutilizables.
- Setup Google AdMob.
**Entregables**:
- Proyecto compilable con estructura de carpetas.
- Modelos de datos persistentes.
- Componentes UI básicos (buttons, cards, pills).
---
### Sprint 2: Onboarding (1 semana)
- Pantalla de bienvenida.
- 4 pasos de configuración inicial.
- Navegación fluida entre pasos.
- Crear tags predefinidos en primera carga.
**Entregables**:
- Onboarding completo y funcional.
- Usuario puede completar setup inicial.
---
### Sprint 3: Gestión de Platos (1 semana)
- Pantalla de lista de platos.
- Crear/editar/eliminar platos.
- Selector de tags (modal).
- Validaciones de formulario.
**Entregables**:
- Usuario puede crear y gestionar platos.
- Lista de platos persistente.
---
### Sprint 4: Pantalla Principal (Calendario) (2 semanas)
- Layout de calendario semanal.
- Slots vacíos y llenos.
- Drawer de platos.
- Drag & drop básico (sin validaciones).
- Navegación entre semanas.
**Entregables**:
- Pantalla Home funcional.
- Usuario puede asignar platos manualmente.
---
### Sprint 5: Validación de Reglas y Drag & Drop Avanzado (1 semana)
- Implementar lógica de validación de reglas.
- Feedback visual en drag & drop (bordes verdes/rojos).
- Haptic feedback.
- Animaciones.
**Entregables**:
- Drag & drop valida reglas en tiempo real.
- Feedback visual y táctil funcional.
---
### Sprint 6: Autocompletado (1 semana)
- Implementar algoritmo de autocompletado.
- Animación secuencial al rellenar slots.
- Confetti y toast.
- Botón "Resetear" con confirmación.
**Entregables**:
- Botón "Completar" funcional.
- Usuario puede autocompletar semana respetando reglas.
---
### Sprint 7: Gestión de Tags (1 semana)
- Pantalla de lista de tags.
- Editar reglas de tags.
- Localización de tags predefinidos.
**Entregables**:
- Usuario puede modificar reglas de tags.
- Cambios se aplican al algoritmo.
---
### Sprint 8: Sincronización con Calendario (1 semana)
- Solicitar permiso de EventKit.
- Crear/eliminar eventos al asignar/quitar platos.
- Configuración en Settings (horarios, prefijo, etc.).
- Actualizar eventos al cambiar hora.
**Entregables**:
- Sincronización con calendario funcional.
- Eventos visibles en app Calendario nativa.
---
### Sprint 9: Settings y Multiidioma (1 semana)
- Pantalla de Settings completa.
- Cambio de idioma (español/inglés).
- Localización de toda la app.
- Persistencia de configuración.
**Entregables**:
- Settings funcional.
- App totalmente bilingüe.
---
### Sprint 10: IAP y Premium (1 semana)
- Configurar StoreKit.
- Pantalla de Premium con planes.
- Compra y restauración.
- Limitar features en Free.
**Entregables**:
- Usuario puede comprar Premium.
- Features Free/Premium funcionan correctamente.
---
### Sprint 11: Testing y QA (1 semana)
- Unit tests para algoritmo.
- UI tests para flujos críticos.
- Bug fixing.
- Performance optimization.
**Entregables**:
- App estable, sin bugs críticos.
- Cobertura de tests >70%.
---
### Sprint 12: Polish y Lanzamiento (1 semana)
- Ajustes finales de UI/UX.
- App Store assets (screenshots, descripción, keywords).
- Beta testing con TestFlight.
- Envío a App Store.
**Entregables**:
- App publicada en App Store.
---
## Anexos
### A. Glosario
- **Slot**: espacio en el calendario donde se asigna una comida (comida o cena de un día específico).
- **Tag**: etiqueta asociada a un plato (ej: Carne, Pescado, Sin Gluten).
- **Tag Rule**: regla asociada a un tag (ej: máximo 3 veces por semana).
- **Autocompletado**: algoritmo que rellena slots vacíos respetando reglas.
- **Drag & Drop**: interacción de arrastrar un plato y soltarlo en un slot.
- **EventKit**: framework de iOS para interactuar con el calendario nativo.
- **SwiftData**: framework de Apple para persistencia local (iOS 17+).
- **CloudKit**: servicio de Apple para sincronización en la nube.
- **IAP**: In-App Purchase (compra dentro de la app).
---
### B. Referencias Técnicas
#### SwiftUI Drag & Drop
- Documentación: https://developer.apple.com/documentation/swiftui/view/ondrag(_:)
- Tutorial: https://www.hackingwithswift.com/quick-start/swiftui/how-to-support-drag-and-drop-in-swiftui
#### EventKit
- Documentación: https://developer.apple.com/documentation/eventkit
- Tutorial: https://www.hackingwithswift.com/example-code/system/how-to-add-events-to-the-users-calendar-using-eventkit
#### SwiftData
- Documentación: https://developer.apple.com/documentation/swiftdata
- Tutorial: https://www.hackingwithswift.com/quick-start/swiftdata
#### StoreKit 2
- Documentación: https://developer.apple.com/documentation/storekit
- Tutorial: https://www.hackingwithswift.com/quick-start/storekit
#### Google Mobile Ads
- Documentación: https://developers.google.com/admob/ios/quick-start
- Setup: https://developers.google.com/admob/ios/quick-start
---
### C. Notas de Implementación
#### Consideraciones de Performance
- **SwiftData queries**: usar `@Query` con predicates para filtrar eficientemente.
- **Drag & Drop**: mantener 60fps, usar `LazyVStack` en lista de platos.
- **Algoritmo de autocompletado**: ejecutar en background thread (`Task`), no bloquear UI.
- **Sincronización CloudKit**: usar queue de operaciones para batch updates.
#### Consideraciones de UX
- **Feedback inmediato**: haptic feedback en cada interacción importante.
- **Animaciones suaves**: spring animations (0.3-0.5s duration).
- **Loading states**: mostrar ProgressView durante operaciones lentas.
- **Empty states**: ilustraciones amigables, no solo texto.
- **Error handling**: mostrar alerts/toasts amigables, no errores técnicos.
#### Consideraciones de Accesibilidad
- **VoiceOver**: etiquetar todos los elementos interactivos.
- **Dynamic Type**: usar `@ScaledMetric` para tamaños de fuente.
- **Color Contrast**: asegurar ratio mínimo 4.5:1 (WCAG AA).
- **Haptic Feedback**: opcional, respetar configuración del sistema.
---
### D. Checklist Pre-Lanzamiento
#### Funcionalidad
- [ ] Onboarding completo y sin bugs.
- [ ] Crear/editar/eliminar platos funcional.
- [ ] Drag & drop fluido (60fps).
- [ ] Validación de reglas correcta.
- [ ] Autocompletado respeta reglas.
- [ ] Sincronización con calendario funcional.
- [ ] Settings guarda cambios correctamente.
- [ ] Multiidioma (ES/EN) sin strings faltantes.
- [ ] IAP (compra y restauración) funcional.
- [ ] AdMob muestra ads correctamente.
#### Testing
- [ ] Unit tests para algoritmo (>80% cobertura).
- [ ] UI tests para flujos críticos.
- [ ] Testeo manual en iPhone SE, iPhone 15 Pro, iPhone 15 Pro Max.
- [ ] Beta testing con 10+ usuarios externos (TestFlight).
- [ ] Performance: app carga en <2s, drag & drop sin lag.
- [ ] Memoria: no memory leaks (Instruments).
#### App Store
- [ ] Screenshots en 3 tamaños (6.7", 6.5", 5.5").
- [ ] Video preview (opcional, recomendado).
- [ ] Descripción optimizada (keywords: meal planner, family meals, weekly menu).
- [ ] Privacy policy URL.
- [ ] Support URL.
- [ ] Categoría: Food & Drink.
- [ ] Keywords: meal planner, weekly menu, family meals, meal organizer, recipe planner.
#### Legal
- [ ] Privacy policy publicada (mencionar EventKit, CloudKit, AdMob).
- [ ] Terms of Service (opcional, recomendado).
- [ ] App Store Review Guidelines (revisar compliance).
---
## Fin del PRD
**Versión**: 1.0
**Fecha**: 2025-02-16
**Autor**: Alex + Claude
**Próxima revisión**: Post-Sprint 6 (evaluar ajustes de MVP)
---
## Notas para Claude Code
Este PRD está diseñado para ser consumido por Claude Code. Contiene:
1. **Especificaciones completas** de funcionalidades MVP y Freemium.
2. **Modelo de datos** con Swift structs/classes.
3. **Ejemplos de código** para componentes clave (drag & drop, EventKit, SwiftData).
4. **Wireframes textuales** detallados de cada pantalla.
5. **Paleta de colores** y componentes UI reutilizables.
6. **Plan de implementación** en sprints secuenciales.
**Instrucciones para Claude Code**:
- Implementar MVP siguiendo orden de sprints.
- Usar SwiftUI + SwiftData (iOS 17+).
- Priorizar UX fluida (animaciones, haptic feedback).
- Código limpio, comentado, siguiendo Swift style guide.
- Tests unitarios para lógica crítica (algoritmo, validaciones).
**Contacto**: Si hay dudas o ambigüedades, consultar con el product owner (Alex).
---
¡Buena suerte con la implementación! 🚀✨