Files
alexandrev-tibco e593453abd Version casi lista
2026-02-17 13:34:02 +01:00

96 KiB
Raw Permalink Blame History

MealMood - Product Requirements Document (PRD)

📋 Índice

  1. Visión General
  2. Problema y Solución
  3. Usuario Objetivo
  4. Propuesta de Valor
  5. Arquitectura de Información
  6. Modelo de Datos
  7. Funcionalidades MVP
  8. Funcionalidades Freemium (Futuro)
  9. Identidad Visual y Branding
  10. Especificaciones de Pantallas
  11. Interacciones y Micro-animaciones
  12. Integración con Calendario Nativo
  13. Algoritmo de Autocompletado
  14. Sincronización y Persistencia
  15. Monetización
  16. Stack Técnico
  17. Plan de Implementación (Sprints)
  18. 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

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)

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)

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)

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)

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:

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

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:

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

// 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

// 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)

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)

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)

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)

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

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

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):

.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):

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):

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:

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:

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:

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:

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

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

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

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

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

@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:

@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:

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:

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

EventKit

SwiftData

StoreKit 2

Google Mobile Ads


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.
  • 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! 🚀