96 KiB
MealMood - Product Requirements Document (PRD)
📋 Índice
- Visión General
- Problema y Solución
- Usuario Objetivo
- Propuesta de Valor
- Arquitectura de Información
- Modelo de Datos
- Funcionalidades MVP
- Funcionalidades Freemium (Futuro)
- Identidad Visual y Branding
- Especificaciones de Pantallas
- Interacciones y Micro-animaciones
- Integración con Calendario Nativo
- Algoritmo de Autocompletado
- Sincronización y Persistencia
- Monetización
- Stack Técnico
- Plan de Implementación (Sprints)
- 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:
- Planificación en 5 minutos: interfaz visual con drag & drop.
- Reglas configurables: limita frecuencia de ingredientes (carne, pescado, etc.) sin ser obsesivo.
- Autocompletado inteligente: algoritmo rellena huecos respetando reglas.
- Sincronización con calendario: eventos automáticos en el calendario nativo de iOS.
- 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
- Rapidez: planificación en 5 minutos vs. 30+ en competencia.
- Visual: interfaz calendario + drag & drop vs. listas aburridas.
- Reglas automáticas: evita repeticiones sin pensar manualmente.
- No es un tracker nutricional: enfoque en disfrute, no en restricción.
- 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:
- Bienvenida: logo + tagline + ilustración.
- Ventanas de comida: selector (solo cenas / solo comidas / ambas).
- Fines de semana: toggle (incluir o no).
- Calendario (opcional): selector de calendario nativo + configuración de horas.
- 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:
- Usuario presiona botón "Completar ✨".
- 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.
- 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.
- Si no encuentra plato válido para un slot → lo deja vacío.
- 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:
-
Al asignar plato a slot:
- Si
syncEnabled == trueycalendarId != nil:- Crear evento en EventKit:
- Título:
{eventPrefix} {mealType}: {dish.name}(ej: "🍽️ Cena: Pollo asado"). - Fecha:
weekStartDate + dayOfWeek. - Hora inicio:
lunchTime(si es comida) odinnerTime(si es cena). - Hora fin:
startDate + eventDuration. - Notas:
dish.description(si existe). - Alerta: según
reminderMinutesBefore.
- Título:
- Guardar
eventIdenslot.calendarEventId.
- Crear evento en EventKit:
- Si
-
Al eliminar plato de slot:
- Si
slot.calendarEventId != nil:- Eliminar evento de EventKit usando
eventId. - Limpiar
slot.calendarEventId. - Mostrar toast: "Evento eliminado del calendario".
- Eliminar evento de EventKit usando
- Si
-
Al cambiar hora en Settings:
- Preguntar: "¿Actualizar eventos existentes?".
- Si Sí:
- Para cada
slotconcalendarEventId != nily fecha >= hoy:- Actualizar evento en EventKit con nueva hora.
- Para cada
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.stringspara 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:
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:
-
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.
-
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).
- Válido (respeta reglas):
-
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.
- Válido:
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:
- Usuario tap en botón "Completar ✨".
- Botón muestra loading indicator (ProgressView, color coral).
- Algoritmo ejecuta en background (no bloquea UI).
- 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).
- 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:
- Usuario tap en botón "Resetear 🔄".
- Alert suave (no modal agresivo):
┌─────────────────────────────────────┐ │ ¿Limpiar la semana completa? │ │ │ │ Esto no se puede deshacer │ │ │ │ [Cancelar] [Limpiar] │ └─────────────────────────────────────┘ - 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
-
Habilitar CloudKit en Xcode:
- Target → Signing & Capabilities → + Capability → iCloud.
- Activar "CloudKit".
- Crear container (ej:
iCloud.com.mealMood.app).
-
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
- 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
@Querycon predicates para filtrar eficientemente. - Drag & Drop: mantener 60fps, usar
LazyVStacken 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
@ScaledMetricpara 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:
- Especificaciones completas de funcionalidades MVP y Freemium.
- Modelo de datos con Swift structs/classes.
- Ejemplos de código para componentes clave (drag & drop, EventKit, SwiftData).
- Wireframes textuales detallados de cada pantalla.
- Paleta de colores y componentes UI reutilizables.
- 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! 🚀✨