Nota: Este proyecto fue desarrollado en Swift + SwiftUI (iOS) como equivalente funcional a la guía de clase basada en Jetpack Compose (Android). A continuación se explica punto por punto cómo se implementó cada requerimiento, junto con los fragmentos de código relevantes y un espacio para capturas de pantalla.
- Repositorio en GitHub
- Estructura de Carpetas del Proyecto
- Pantalla HomeScreen (Descripción + Logo)
- Ícono de la Aplicación (AppIcon)
- Pantalla de Login + ViewModel
- Pantalla de Registro + ViewModel
- Pantallas de Olvido y Recuperación de Contraseña
- Uso de Snackbar (Alertas y Retroalimentación)
- Funcionalidades Adicionales
El proyecto se encuentra alojado en un repositorio público de GitHub:
🔗 Repositorio: github.com/anju2246/DailyMath
- Se utiliza Git para control de versiones.
- El archivo
.gitignoreestá configurado para excluir archivos de Xcode innecesarios (build artifacts, DerivedData, etc.). - Se incluye un
.gitattributespara gestión de archivos binarios.
📸 Captura de pantalla del repositorio en GitHub:
(Insertar captura aquí)
La estructura del proyecto sigue el patrón MVVM (Model-View-ViewModel), equivalente a la arquitectura recomendada en Jetpack Compose:
DailyMath/
├── App/
│ └── AppState.swift ← Estado global de la app (equivalente a Application class)
├── Assets.xcassets/
│ ├── AccentColor.colorset/ ← Color de acento (equivalente a themes.xml)
│ └── AppIcon.appiconset/ ← Ícono de la app (equivalente a mipmap)
├── Config/
│ └── Secrets.swift ← Claves y configuración sensible
├── Models/
│ ├── UserProfile.swift ← Modelo de usuario
│ ├── Exercise.swift ← Modelo de ejercicio
│ ├── Flashcard.swift ← Modelo de flashcard (SM-2)
│ ├── Community.swift ← Modelos de comunidad (comentarios, votos, badges)
│ └── Duel.swift ← Modelos de duelos y torneos
├── Services/
│ ├── AuthService.swift ← Servicio de autenticación (equivalente a ViewModel de Auth)
│ ├── FlashcardStore.swift ← Store de flashcards con persistencia local
│ └── NotificationService.swift ← Servicio de notificaciones push
├── Utilities/
│ ├── Constants.swift ← Constantes de la app (categorías, niveles, badges)
│ ├── Extensions.swift ← Extensiones de UI (botones, colores, validaciones)
│ └── SM2Algorithm.swift ← Implementación del algoritmo SM-2 (repaso espaciado)
├── ViewModels/ ← (Vacío: la lógica de ViewModel vive en Services/)
├── Views/
│ ├── Auth/
│ │ ├── LoginView.swift ← Pantalla de Login
│ │ ├── RegisterView.swift ← Pantalla de Registro
│ │ └── ForgotPasswordView.swift← Pantalla de recuperación de contraseña
│ ├── App/
│ │ └── MainTabView.swift ← Navegación principal por tabs
│ ├── Today/
│ │ ├── TodayView.swift ← Vista "Hoy" (flashcards pendientes)
│ │ ├── FlashcardQuizView.swift ← Quiz interactivo de flashcards
│ │ └── CreateFlashcardView.swift← Crear nueva flashcard
│ ├── Explore/
│ │ └── ExploreView.swift ← Explorar ejercicios
│ ├── Create/
│ │ └── CreateExerciseView.swift← Crear ejercicios
│ ├── Agility/
│ │ ├── AgilityView.swift ← Modo agilidad mental
│ │ └── NumericKeyboardView.swift← Teclado numérico custom
│ └── Profile/
│ └── ProfileView.swift ← Perfil del usuario
└── DailyMathApp.swift ← Punto de entrada de la app (@main)
| Concepto Android (Jetpack Compose) | Equivalente iOS (SwiftUI) |
|---|---|
Activity / @Composable |
struct View: View |
ViewModel (Jetpack) |
ObservableObject (AuthService, AppState) |
LiveData / StateFlow |
@Published + @StateObject / @EnvironmentObject |
Navigation Component |
NavigationStack + .navigationDestination / .sheet |
res/mipmap/ |
Assets.xcassets/AppIcon.appiconset/ |
AndroidManifest.xml |
Info.plist + Xcode project settings |
themes.xml / colors.xml |
AccentColor.colorset + Extensions.swift (colores custom) |
📸 Captura de la estructura de carpetas en Xcode:
(Insertar captura aquí)
La pantalla de HomeScreen se presenta como la pantalla de Login cuando el usuario no está autenticado y como la vista TodayView (vista principal) una vez autenticado.
La app muestra un splash screen con el logo y nombre de la aplicación mientras se verifica el estado de autenticación:
// DailyMathApp.swift
ZStack {
Color(.systemBackground).ignoresSafeArea()
VStack(spacing: 16) {
Image(systemName: "function") // Logo: ícono de función matemática
.font(.system(size: 64))
.foregroundStyle(.tint)
Text("DailyMath") // Nombre de la app
.font(.largeTitle.bold())
ProgressView() // Indicador de carga
.padding(.top, 8)
}
}Una vez autenticado, el usuario ve un saludo personalizado, estadísticas y sus flashcards:
// Views/Today/TodayView.swift
VStack(alignment: .leading, spacing: 8) {
Text("¡Hola, \(appState.currentUser?.displayName ?? "")!")
.font(.title.bold())
Text("Tus tarjetas de repaso del día")
.font(.subheadline)
.foregroundStyle(.secondary)
}Se incluyen estadísticas visuales con badges de color (pendientes, repasadas hoy, total) y un botón destacado para comenzar el quiz con gradiente verde.
La app usa un TabView con 5 secciones, equivalente a un BottomNavigationBar en Jetpack Compose:
| Tab | Vista | Ícono |
|---|---|---|
| Hoy | TodayView |
calendar.badge.clock |
| Explorar | ExploreView |
magnifyingglass |
| Crear | CreateExerciseView |
plus.circle.fill |
| Agilidad | AgilityView |
brain |
| Perfil | ProfileView |
person.circle |
📸 Captura de la pantalla HomeScreen / pantalla principal:
(Insertar captura aquí)
📸 Captura del Splash Screen:
(Insertar captura aquí)
En iOS, el ícono de la aplicación se configura a través de Assets.xcassets/AppIcon.appiconset/. Este directorio contiene un archivo Contents.json que define las dimensiones del ícono para cada tipo de dispositivo y contexto.
| Android | iOS |
|---|---|
Ícono en res/mipmap-*/ |
Ícono en AppIcon.appiconset/ |
Configurar en AndroidManifest.xml (android:icon) |
Configurar en Xcode → Target → General → App Icons |
Formatos: mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi |
Formatos: 1x, 2x, 3x (1024×1024 para App Store) |
- Se genera un ícono representativo del proyecto (temática matemática).
- Se coloca el ícono dentro de
Assets.xcassets/AppIcon.appiconset/. - Se edita el archivo
Contents.jsonpara apuntar a la imagen del ícono con las resoluciones adecuadas. - Xcode automáticamente usa este ícono como predeterminado — no se necesita editar un "manifiesto" como en Android.
📸 Captura del ícono de la app (AppIcon en Xcode):
(Insertar captura aquí)
📸 Captura del ícono visible en el simulador o dispositivo:
(Insertar captura aquí)
La pantalla de Login incluye:
- Logo con gradiente azul-púrpura (ícono
functiondel sistema). - Título "DailyMath" y subtítulo "Tu plataforma de estudio matemático".
- Campo de Email con validación de tipo de teclado (
.emailAddress). - Campo de Contraseña (
SecureField). - Botón "Iniciar Sesión" con estilo primario y estado de carga (
ProgressView). - Enlace "¿Olvidaste tu contraseña?" que abre un modal (
.sheet). - Botón "Crear cuenta" que navega a
RegisterView. - Mensajes de error en rojo cuando la autenticación falla.
// Botón de Login con estado de carga
Button {
Task {
do {
try await authService.signIn(email: email, password: password)
} catch {
await MainActor.run {
authService.errorMessage = error.localizedDescription
}
}
}
} label: {
if authService.isLoading {
ProgressView().tint(.white).primaryButton()
} else {
Text("Iniciar Sesión").primaryButton()
}
}
.disabled(email.isEmpty || password.isEmpty || authService.isLoading)En SwiftUI, el patrón ViewModel se implementa con clases que conforman ObservableObject. El AuthService actúa como ViewModel para Login, Registro y Recuperación de contraseña:
class AuthService: ObservableObject {
@Published var currentUser: UserProfile? // Usuario actual
@Published var isAuthenticated = false // Estado de autenticación
@Published var isLoading = false // Estado de carga
@Published var errorMessage: String? // Mensajes de error/feedback
func signIn(email: String, password: String) async throws { ... }
func signUp(email: String, password: String, ...) async throws { ... }
func signOut() async throws { ... }
func resetPassword(email: String) async throws { ... }
func deleteAccount() async throws { ... }
}| Jetpack Compose | SwiftUI |
|---|---|
class LoginViewModel : ViewModel() |
class AuthService: ObservableObject |
val email = mutableStateOf("") |
@State private var email = "" (en View) |
val isLoading: StateFlow<Boolean> |
@Published var isLoading = false |
viewModelScope.launch { } |
Task { } con async/await |
ViewModelProvider / Hilt injection |
@EnvironmentObject / @StateObject |
📸 Captura de la pantalla de Login:
(Insertar captura aquí)
📸 Captura mostrando validación / error en Login:
(Insertar captura aquí)
La pantalla de Registro incluye:
- Ícono de
person.badge.pluscon el título "Crear Cuenta". - Subtítulo "Únete a la comunidad DailyMath".
- Campos del formulario:
- Nombre completo (
displayName) - Email (con validación de formato)
- Universidad
- Contraseña (mínimo 6 caracteres)
- Confirmar contraseña
- Nombre completo (
- Validación en tiempo real del formulario:
private var isFormValid: Bool { !displayName.isEmpty && !email.isEmpty && email.isValidEmail && // Extensión con Regex password.count >= 6 && password == confirmPassword }
- Mensaje de error si las contraseñas no coinciden.
- Botón deshabilitado hasta que el formulario sea válido.
Reutiliza el mismo AuthService con el método signUp():
func signUp(email: String, password: String, username: String, displayName: String) async throws {
try await signIn(email: email, password: password) // Demo mode
}// Utilities/Extensions.swift
extension String {
var isValidEmail: Bool {
let regex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
return self.wholeMatch(of: regex) != nil
}
}📸 Captura de la pantalla de Registro:
(Insertar captura aquí)
📸 Captura mostrando validación de campos en Registro:
(Insertar captura aquí)
Esta pantalla maneja dos estados en una sola vista (equivalente a tener pantalla de olvido + pantalla de recuperación):
- Ícono de
lock.rotation. - Título "Recuperar Contraseña".
- Instrucciones: "Ingresa tu email y te enviaremos un enlace para restablecer tu contraseña."
- Campo de email con validación.
- Botón "Enviar enlace" que llama a
authService.resetPassword(email:).
- Ícono de
envelope.badge.shield.half.filleden verde. - Título "¡Correo enviado!".
- Mensaje: "Revisa tu bandeja de entrada en {email} para restablecer tu contraseña."
- Botón "Volver al login" para cerrar el modal.
// Transición entre estados
if emailSent {
// ✅ Estado de éxito — "pantalla de recuperación"
VStack(spacing: 16) {
Image(systemName: "envelope.badge.shield.half.filled")
.font(.system(size: 64))
.foregroundStyle(.green)
Text("¡Correo enviado!").font(.title2.bold())
Text("Revisa tu bandeja de entrada en **\(email)**...")
Button("Volver al login") { dismiss() }
}
} else {
// 📝 Formulario — "pantalla de olvido de contraseña"
// ... campo de email + botón enviar
}| Requerimiento de la guía | Implementación en SwiftUI |
|---|---|
| Pantalla de olvido de contraseña | ForgotPasswordView — Estado inicial (formulario) |
| Pantalla de recuperación de contraseña | ForgotPasswordView — Estado emailSent = true (confirmación) |
| ViewModel correspondiente | AuthService.resetPassword(email:) vía async/await |
📸 Captura de la pantalla de Olvido de Contraseña (formulario):
(Insertar captura aquí)
📸 Captura de la pantalla de Recuperación (confirmación de envío):
(Insertar captura aquí)
En Android/Jetpack Compose, se usa Snackbar para dar retroalimentación al usuario. En SwiftUI/iOS, el equivalente se implementa mediante:
Todos los formularios muestran mensajes de error en rojo directamente en la interfaz:
// LoginView.swift, RegisterView.swift, ForgotPasswordView.swift
if let error = authService.errorMessage {
Text(error)
.font(.caption)
.foregroundStyle(.red)
.frame(maxWidth: .infinity, alignment: .leading)
}if showPasswordMismatch {
Text("Las contraseñas no coinciden")
.font(.caption)
.foregroundStyle(.red)
}Para acciones destructivas se usa un Alert nativo:
.alert("¿Eliminar cuenta?", isPresented: $showDeleteConfirmation) {
Button("Cancelar", role: .cancel) { }
Button("Eliminar", role: .destructive) {
Task { try? await appState.authService.deleteAccount() }
}
} message: {
Text("Esta acción es permanente. Se eliminarán todos tus datos.")
}Todos los botones de acción muestran un ProgressView animado mientras la operación está en curso:
if authService.isLoading {
ProgressView().tint(.white).primaryButton()
} else {
Text("Iniciar Sesión").primaryButton()
}| Snackbar (Android) | Equivalente iOS |
|---|---|
Snackbar.make(view, "Mensaje", LENGTH_SHORT).show() |
Text(error).foregroundStyle(.red) (inline) |
Snackbar con acción |
.alert() con botones de acción |
| Mensajes temporales (Toast) | .overlay de confirmación / ProgressView animado |
| Validación de formularios | @State variables + condicionales en la vista |
📸 Captura mostrando un mensaje de error en un formulario:
(Insertar captura aquí)
📸 Captura mostrando el alert de confirmación:
(Insertar captura aquí)
📸 Captura mostrando el estado de carga (ProgressView):
(Insertar captura aquí)
Además de los requerimientos del seguimiento, el proyecto DailyMath incluye funcionalidades extra que demuestran el uso avanzado de SwiftUI:
- Repaso espaciado usando el algoritmo SuperMemo SM-2.
- Las flashcards se almacenan localmente con
UserDefaults. - Quiz interactivo con opciones múltiples.
- Puntos y niveles de usuario (Novato → Estudiante → Tutor → Maestro).
- Racha de estudio (streak).
- Sistema de insignias (badges) por logros.
- Sistema de reputación por contribuciones.
- Modelo de duelos 1v1 con preguntas de agilidad.
- Sistema de torneos con participantes y ranking.
- Avatar generado dinámicamente con la inicial del nombre.
- Grid de estadísticas (Puntos, Racha, Nivel).
- Panel de moderador condicional.
- Opciones de cerrar sesión y eliminar cuenta.
- Servicio de notificaciones push (
NotificationService). - Solicitud de permisos al iniciar la app.
- Colores custom (
dmPrimary,dmSecondary,dmAccent, etc.). - Modificadores reutilizables (
.primaryButton(),.secondaryButton(),.cardStyle()). - Localización completa en español colombiano (
es_CO).
📸 Capturas adicionales del proyecto (perfil, flashcards, quiz, etc.):
(Insertar capturas aquí)
| # | Requerimiento | Estado | Archivo(s) Clave |
|---|---|---|---|
| 1 | Repositorio en GitHub | ✅ | Repo anju2246/DailyMath |
| 2 | Estructura de carpetas | ✅ | Models/, Views/, Services/, Utilities/ |
| 3 | HomeScreen (descripción + logo) | ✅ | DailyMathApp.swift, TodayView.swift |
| 4 | Ícono de la app en mipmap/AppIcon | ✅ | Assets.xcassets/AppIcon.appiconset/ |
| 5 | Login + ViewModel | ✅ | LoginView.swift, AuthService.swift |
| 6 | Registro + ViewModel | ✅ | RegisterView.swift, AuthService.swift |
| 7 | Olvido + Recuperación de contraseña | ✅ | ForgotPasswordView.swift |
| 8 | Snackbar (retroalimentación) | ✅ | Mensajes inline + .alert() + ProgressView |
📝 Elaborado por: Grupo de Trabajo — DailyMath
📅 Fecha: Marzo 2026
🍎 Tecnología: Swift 5.9 + SwiftUI (iOS 17+)
🤖 Equivalente Android: Jetpack Compose + Kotlin