Initial commit: MangaReader iOS App
✨ Features: - App iOS completa para leer manga sin publicidad - Scraper con WKWebView para manhwaweb.com - Sistema de descargas offline - Lector con zoom y navegación - Favoritos y progreso de lectura - Compatible con iOS 15+ y Sideloadly/3uTools 📦 Contenido: - Backend Node.js con Puppeteer (opcional) - App iOS con SwiftUI - Scraper de capítulos e imágenes - Sistema de almacenamiento local - Testing completo - Documentación exhaustiva 🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente - 21 páginas descargadas - 4.68 MB total - URLs verificadas y funcionales 🎉 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
642
docs/ARCHITECTURE.md
Normal file
642
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,642 @@
|
||||
# Arquitectura de MangaReader
|
||||
|
||||
Este documento describe la arquitectura general del proyecto MangaReader, explicando cómo funcionan los componentes Backend y iOS App, y cómo fluyen los datos desde el scraping hasta el display en pantalla.
|
||||
|
||||
## Tabla de Contenidos
|
||||
|
||||
- [Visión General](#visión-general)
|
||||
- [Arquitectura del Sistema](#arquitectura-del-sistema)
|
||||
- [Arquitectura de la App iOS](#arquitectura-de-la-app-ios)
|
||||
- [Flujo de Datos](#flujo-de-datos)
|
||||
- [Patrones de Diseño](#patrones-de-diseño)
|
||||
- [Diagramas de Secuencia](#diagramas-de-secuencia)
|
||||
|
||||
## Visión General
|
||||
|
||||
MangaReader es una aplicación nativa de iOS para leer manga sin publicidad. El proyecto consta de dos componentes opcionales:
|
||||
|
||||
1. **Backend (Opcional)**: Servidor Node.js con Express que realiza scraping usando Puppeteer
|
||||
2. **iOS App**: Aplicación nativa SwiftUI que puede hacer scraping localmente usando WKWebView
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MangaReader │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Backend │ │ iOS App │ │
|
||||
│ │ (Opcional) │ │ (Principal) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Node.js │ │ • SwiftUI │ │
|
||||
│ │ • Express │ │ • WKWebView │ │
|
||||
│ │ • Puppeteer │ │ • Core Data │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
│ ▲ │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ Scraping Independiente │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Arquitectura del Sistema
|
||||
|
||||
### Componentes Principales
|
||||
|
||||
#### 1. Backend (Opcional)
|
||||
|
||||
El backend es una API REST opcional que puede actuar como intermediario:
|
||||
|
||||
```
|
||||
backend/
|
||||
├── scraper.js # Scraper con Puppeteer
|
||||
├── server.js # API REST con Express
|
||||
└── package.json
|
||||
```
|
||||
|
||||
**Responsabilidades:**
|
||||
- Realizar scraping de manhwaweb.com
|
||||
- Servir datos vía API REST
|
||||
- Cachear respuestas para mejorar rendimiento
|
||||
|
||||
**API Endpoints:**
|
||||
- `GET /api/health` - Health check
|
||||
- `GET /api/manga/:slug` - Información de un manga
|
||||
- `GET /api/manga/:slug/chapters` - Lista de capítulos
|
||||
- `GET /api/chapter/:slug/images` - Imágenes de un capítulo
|
||||
|
||||
**Nota Importante**: El backend es completamente opcional. La app iOS está diseñada para funcionar de manera autónoma sin necesidad del backend.
|
||||
|
||||
#### 2. iOS App (Principal)
|
||||
|
||||
La aplicación iOS es el componente principal y puede operar independientemente:
|
||||
|
||||
```
|
||||
ios-app/
|
||||
├── MangaReaderApp.swift # Entry point
|
||||
├── Info.plist
|
||||
└── Sources/
|
||||
├── Models/ # Modelos de datos
|
||||
│ └── Manga.swift
|
||||
├── Services/ # Lógica de negocio
|
||||
│ ├── ManhwaWebScraper.swift
|
||||
│ └── StorageService.swift
|
||||
└── Views/ # UI SwiftUI
|
||||
├── ContentView.swift
|
||||
├── MangaDetailView.swift
|
||||
└── ReaderView.swift
|
||||
```
|
||||
|
||||
## Arquitectura de la App iOS
|
||||
|
||||
### MVVM Pattern (Model-View-ViewModel)
|
||||
|
||||
La app iOS sigue el patrón MVVM para separar la UI de la lógica de negocio:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MVVM Architecture │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────────────┐ ┌─────────┐ │
|
||||
│ │ View │◄────────│ ViewModel │◄────────│ Model │ │
|
||||
│ │(SwiftUI)│ │ (Observable) │ │(Struct) │ │
|
||||
│ └─────────┘ └─────────────────┘ └─────────┘ │
|
||||
│ ▲ │ │ │
|
||||
│ │ │ │ │
|
||||
│ └───────────────────────┴───────────────────────────┘ │
|
||||
│ Data Binding & Commands │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Estructura de Componentes
|
||||
|
||||
#### 1. Models (Datos)
|
||||
|
||||
**Ubicación**: `ios-app/Sources/Models/Manga.swift`
|
||||
|
||||
Los modelos son estructuras inmutables que representan los datos:
|
||||
|
||||
```swift
|
||||
- Manga: Información del manga (título, descripción, géneros, estado)
|
||||
- Chapter: Capítulo individual (número, título, URL, estado de lectura)
|
||||
- MangaPage: Página individual del capítulo (URL de imagen, índice)
|
||||
- ReadingProgress: Progreso de lectura del usuario
|
||||
- DownloadedChapter: Capítulo descargado localmente
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Inmutables (`struct`)
|
||||
- Conformes a `Codable` para serialización
|
||||
- Conformes a `Identifiable` para SwiftUI
|
||||
- Conformes a `Hashable` para comparaciones
|
||||
|
||||
#### 2. Services (Lógica de Negocio)
|
||||
|
||||
**Ubicación**: `ios-app/Sources/Services/`
|
||||
|
||||
##### ManhwaWebScraper.swift
|
||||
|
||||
Responsable del scraping de contenido web:
|
||||
|
||||
```swift
|
||||
class ManhwaWebScraper: NSObject, ObservableObject {
|
||||
// Singleton instance
|
||||
static let shared = ManhwaWebScraper()
|
||||
|
||||
// Funciones principales:
|
||||
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga
|
||||
func scrapeChapters(mangaSlug: String) async throws -> [Chapter]
|
||||
func scrapeChapterImages(chapterSlug: String) async throws -> [String]
|
||||
}
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Usa `WKWebView` para ejecutar JavaScript
|
||||
- Implementa `async/await` para operaciones asíncronas
|
||||
- Patrón Singleton para compartir instancia
|
||||
- Manejo de errores con `ScrapingError`
|
||||
|
||||
##### StorageService.swift
|
||||
|
||||
Responsable del almacenamiento local:
|
||||
|
||||
```swift
|
||||
class StorageService {
|
||||
// Singleton instance
|
||||
static let shared = StorageService()
|
||||
|
||||
// Gestión de favoritos:
|
||||
func getFavorites() -> [String]
|
||||
func saveFavorite(mangaSlug: String)
|
||||
func removeFavorite(mangaSlug: String)
|
||||
|
||||
// Gestión de progreso:
|
||||
func saveReadingProgress(_ progress: ReadingProgress)
|
||||
func getReadingProgress(mangaSlug: String, chapterNumber: Int) -> ReadingProgress?
|
||||
|
||||
// Gestión de descargas:
|
||||
func saveDownloadedChapter(_ chapter: DownloadedChapter)
|
||||
func getDownloadedChapters() -> [DownloadedChapter]
|
||||
|
||||
// Gestión de imágenes:
|
||||
func saveImage(_ image: UIImage, ...) async throws -> URL
|
||||
func loadImage(...) -> UIImage?
|
||||
}
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Almacena favoritos en `UserDefaults`
|
||||
- Almacena progreso en `UserDefaults`
|
||||
- Guarda imágenes en el sistema de archivos
|
||||
- Usa `FileManager` para gestión de archivos
|
||||
|
||||
#### 3. ViewModels (Presentación)
|
||||
|
||||
**Ubicación**: Integrados en los archivos de Views
|
||||
|
||||
Los ViewModels coordinan entre Services y Views:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class MangaListViewModel: ObservableObject {
|
||||
@Published var mangas: [Manga] = []
|
||||
@Published var isLoading = false
|
||||
|
||||
private let scraper = ManhwaWebScraper.shared
|
||||
private let storage = StorageService.shared
|
||||
|
||||
func loadMangas() async
|
||||
func addManga(_ slug: String) async
|
||||
}
|
||||
```
|
||||
|
||||
**Responsabilidades:**
|
||||
- Mantener estado de la UI
|
||||
- Transformar datos para presentación
|
||||
- Manejar lógica de navegación
|
||||
- Coordinar llamadas a servicios
|
||||
|
||||
#### 4. Views (UI)
|
||||
|
||||
**Ubicación**: `ios-app/Sources/Views/`
|
||||
|
||||
##### ContentView.swift
|
||||
- Vista principal de la app
|
||||
- Lista de mangas con filtros
|
||||
- Búsqueda y añadir manga
|
||||
|
||||
##### MangaDetailView.swift
|
||||
- Detalle de un manga específico
|
||||
- Lista de capítulos
|
||||
- Descarga de capítulos
|
||||
|
||||
##### ReaderView.swift
|
||||
- Lector de imágenes
|
||||
- Gestos de zoom y pan
|
||||
- Configuración de lectura
|
||||
|
||||
## Flujo de Datos
|
||||
|
||||
### 1. Flujo de Scraping de Manga
|
||||
|
||||
```
|
||||
Usuario ingresa slug
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ContentView -> MangaListViewModel.addManga(slug) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ManhwaWebScraper.scrapeMangaInfo(mangaSlug) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ WKWebView carga URL de manhwaweb.com │
|
||||
│ https://manhwaweb.com/manga/{slug} │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ JavaScript ejecutado en WKWebView: │
|
||||
│ - Extrae título, descripción, géneros │
|
||||
│ - Extrae estado, imagen de portada │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ Datos parseados a struct Manga │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ViewModel actualiza @Published var mangas │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ SwiftUI detecta cambio y re-renderiza UI │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2. Flujo de Lectura de Capítulo
|
||||
|
||||
```
|
||||
Usuario selecciona capítulo
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ MangaDetailView -> ReaderView(manga, chapter) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ReaderViewModel.loadPages() │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
├──► ¿Capítulo descargado?
|
||||
│ │
|
||||
│ SÍ │ NO
|
||||
│ ▼
|
||||
│ ┌─────────────────────────────────────────────────┐
|
||||
│ │ StorageService.getDownloadedChapter() │
|
||||
│ │ Cargar páginas locales │
|
||||
│ └─────────────────────────────────────────────────┘
|
||||
│ │
|
||||
│ └──────────────────┐
|
||||
│ │
|
||||
└─────────────────────────────┼──────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ManhwaWebScraper.scrapeChapterImages(chapterSlug) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ WKWebView carga URL del capítulo │
|
||||
│ https://manhwaweb.com/leer/{slug} │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ JavaScript extrae URLs de imágenes: │
|
||||
│ - Selecciona todas las etiquetas <img> │
|
||||
│ - Filtra elementos de UI (avatars, icons) │
|
||||
│ - Elimina duplicados │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ Array de strings con URLs de imágenes │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ Convertir a [MangaPage] y mostrar en ReaderView │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ReaderView muestra imágenes con AsyncImage │
|
||||
│ Cache automático de imágenes en visualización │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. Flujo de Guardado de Progreso
|
||||
|
||||
```
|
||||
Usuario navega a página X
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ReaderViewModel.currentPage cambia a X │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ReaderViewModel.saveProgress() │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ Crear ReadingProgress(mangaSlug, chapterNumber, pageNumber) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ StorageService.saveReadingProgress(progress) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ JSONEncoder codifica a Data │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ UserDefaults.set(data, forKey: "readingProgress") │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Patrones de Diseño
|
||||
|
||||
### 1. Singleton Pattern
|
||||
|
||||
**Uso**: Services compartidos
|
||||
|
||||
```swift
|
||||
class StorageService {
|
||||
static let shared = StorageService()
|
||||
private init() { ... }
|
||||
}
|
||||
|
||||
class ManhwaWebScraper {
|
||||
static let shared = ManhwaWebScraper()
|
||||
private init() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Beneficios:**
|
||||
- Unica instancia compartida en toda la app
|
||||
- Reduce consumo de memoria
|
||||
- Facilita acceso desde cualquier View/ViewModel
|
||||
|
||||
### 2. MVVM (Model-View-ViewModel)
|
||||
|
||||
**Uso**: Arquitectura general de la app
|
||||
|
||||
**Separación de responsabilidades:**
|
||||
- **Model**: Datos puras (`struct`, `Codable`)
|
||||
- **View**: UI pura (`SwiftUI`, reactive)
|
||||
- **ViewModel**: Lógica de presentación (`ObservableObject`)
|
||||
|
||||
**Beneficios:**
|
||||
- Testabilidad de ViewModels sin UI
|
||||
- Reutilización de ViewModels
|
||||
- Separación clara de concerns
|
||||
|
||||
### 3. Repository Pattern
|
||||
|
||||
**Uso**: Abstracción de fuentes de datos
|
||||
|
||||
```swift
|
||||
class StorageService {
|
||||
// Abstrae UserDefaults, FileManager, etc.
|
||||
func getFavorites() -> [String]
|
||||
func saveFavorite(mangaSlug: String)
|
||||
}
|
||||
```
|
||||
|
||||
**Beneficios:**
|
||||
- Interfaz unificada para diferentes storage
|
||||
- Fácil cambiar implementación
|
||||
- Centraliza lógica de persistencia
|
||||
|
||||
### 4. Async/Await Pattern
|
||||
|
||||
**Uso**: Operaciones de scraping
|
||||
|
||||
```swift
|
||||
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga {
|
||||
// Operación asíncrona
|
||||
try await loadURLAndWait(url)
|
||||
let info = try await webView.evaluateJavaScript(...)
|
||||
return Manga(...)
|
||||
}
|
||||
```
|
||||
|
||||
**Beneficios:**
|
||||
- Código asíncrono legible
|
||||
- Manejo de errores claro
|
||||
- No bloquea el hilo principal
|
||||
|
||||
### 5. Observable Object Pattern
|
||||
|
||||
**Uso**: Reactividad en SwiftUI
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class MangaListViewModel: ObservableObject {
|
||||
@Published var mangas: [Manga] = []
|
||||
|
||||
func loadMangas() async {
|
||||
mangas = ... // SwiftUI detecta cambio
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Beneficios:**
|
||||
- UI se actualiza automáticamente
|
||||
- Código declarativo
|
||||
- Menos código boilerplate
|
||||
|
||||
### 6. Factory Pattern (Implícito)
|
||||
|
||||
**Uso**: Creación de modelos
|
||||
|
||||
```swift
|
||||
// Funciones estáticas que crean instancias
|
||||
Chapter(number: 1, title: "...", url: "...", slug: "...")
|
||||
MangaPage(url: "...", index: 0)
|
||||
```
|
||||
|
||||
**Beneficios:**
|
||||
- Creación consistente de objetos
|
||||
- Validación en inicialización
|
||||
- Fácil de mantener
|
||||
|
||||
## Diagramas de Secuencia
|
||||
|
||||
### Secuencia 1: Agregar Manga
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌────────────┐
|
||||
│ Usuario │ │ ViewModel │ │ Scraper │ │ WKWebView │
|
||||
└────┬────┘ └──────┬───────┘ └────────┬────────┘ └─────┬──────┘
|
||||
│ │ │ │
|
||||
│ ingresa slug │ │ │
|
||||
│───────────────>│ │ │
|
||||
│ │ │ │
|
||||
│ │ scrapeMangaInfo() │ │
|
||||
│ │────────────────────>│ │
|
||||
│ │ │ │
|
||||
│ │ │ load(URL) │
|
||||
│ │ │───────────────────>│
|
||||
│ │ │ │
|
||||
│ │ │ wait 3 seconds │
|
||||
│ │ │<───────────────────┤
|
||||
│ │ │ │
|
||||
│ │ │ evaluateJavaScript │
|
||||
│ │ │───────────────────>│
|
||||
│ │ │ (extrae datos) │
|
||||
│ │ │<───────────────────┤
|
||||
│ │ │ │
|
||||
│ │ Manga │ │
|
||||
│ │<────────────────────│ │
|
||||
│ │ │ │
|
||||
│ actualiza │ │ │
|
||||
│ UI │ │ │
|
||||
│<───────────────│ │ │
|
||||
│ │ │ │
|
||||
┌────┴────┐ ┌──────┴───────┘ └────────┴────────┘ └─────┴──────┘
|
||||
```
|
||||
|
||||
### Secuencia 2: Leer Capítulo
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────┐
|
||||
│ Usuario │ │ ReaderView │ │ ViewModel │ │ Storage │
|
||||
└────┬────┘ └──────┬───────┘ └────────┬────────┘ └────┬─────┘
|
||||
│ │ │ │
|
||||
│ tap capítulo │ │ │
|
||||
│───────────────>│ │ │
|
||||
│ │ │ │
|
||||
│ │ loadPages() │ │
|
||||
│ │────────────────────>│ │
|
||||
│ │ │ │
|
||||
│ │ │ isDownloaded()? │
|
||||
│ │ │──────────────────>│
|
||||
│ │ │ │
|
||||
│ │ │ NO │
|
||||
│ │ │<──────────────────┤
|
||||
│ │ │ │
|
||||
│ │ │ scrapeChapterImages│
|
||||
│ │ │ (via Scraper) │
|
||||
│ │ │ │
|
||||
│ │ [MangaPage] │ │
|
||||
│ │<────────────────────│ │
|
||||
│ │ │ │
|
||||
│ muestra páginas│ │ │
|
||||
│<───────────────│ │ │
|
||||
│ │ │ │
|
||||
┌────┴────┐ ┌──────┴───────┘ └────────┴────────┘ └────┴─────┘
|
||||
```
|
||||
|
||||
### Secuencia 3: Guardar Favorito
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────────┐ ┌─────────────────┐
|
||||
│ Usuario │ │ View │ │ StorageService │
|
||||
└────┬────┘ └──────┬───────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
│ tap corazón │ │
|
||||
│───────────────>│ │
|
||||
│ │ │
|
||||
│ │ toggleFavorite() │
|
||||
│ │────────────────────>│
|
||||
│ │ │
|
||||
│ │ │ getFavorites()
|
||||
│ │ │ (UserDefaults)
|
||||
│ │ │
|
||||
│ │ │ saveFavorite()
|
||||
│ │ │ (UserDefaults)
|
||||
│ │ │
|
||||
│ │ actualiza UI │
|
||||
│ │<────────────────────│
|
||||
│ │ │
|
||||
┌────┴────┘ ┌──────┴───────┘ └────────┴────────┘
|
||||
```
|
||||
|
||||
## Decisiones de Arquitectura
|
||||
|
||||
### ¿Por qué WKWebView para scraping?
|
||||
|
||||
1. **JavaScript Rendering**: manhwaweb.com usa JavaScript para cargar contenido
|
||||
2. **Sin dependencias externas**: No requiere librerías de terceros
|
||||
3. **Aislamiento**: El scraping ocurre en contexto separado
|
||||
4. **Control**: Full control sobre timeouts, cookies, headers
|
||||
|
||||
### ¿Por qué UserDefaults para favoritos/progreso?
|
||||
|
||||
1. **Simplicidad**: Datos pequeños y simples
|
||||
2. **Sincronización**: iCloud sync automático disponible
|
||||
3. **Rendimiento**: Lectura/escritura rápida
|
||||
4. **Persistencia**: Survive app reinstalls (si iCloud)
|
||||
|
||||
### ¿Por qué FileManager para imágenes?
|
||||
|
||||
1. **Tamaño**: Imágenes pueden ser grandes (MBs)
|
||||
2. **Performance**: Acceso directo a archivos
|
||||
3. **Cache control**: Control manual de qué guardar
|
||||
4. **Escalabilidad**: No limitado por UserDefaults
|
||||
|
||||
### ¿Por qué MVVM?
|
||||
|
||||
1. **SwiftUI nativo**: SwiftUI está diseñado para MVVM
|
||||
2. **Testabilidad**: ViewModels testeables sin UI
|
||||
3. **Reactibilidad**: `@Published` y `ObservableObject`
|
||||
4. **Separación**: UI separada de lógica de negocio
|
||||
|
||||
## Consideraciones de Escalabilidad
|
||||
|
||||
### Futuras Mejoras
|
||||
|
||||
1. **Database**: Migrar de UserDefaults a Core Data o SQLite
|
||||
2. **Background Tasks**: Descargas en background
|
||||
3. **Caching Strategy**: LRU cache para imágenes
|
||||
4. **Pagination**: Cargar capítulos bajo demanda
|
||||
5. **Sync Service**: Sincronización entre dispositivos
|
||||
|
||||
### Rendimiento
|
||||
|
||||
- **Lazy Loading**: Cargar imágenes bajo demanda
|
||||
- **Image Compression**: JPEG 80% calidad
|
||||
- **Request Batching**: Descargar páginas en paralelo
|
||||
- **Memory Management**: Liberar imágenes no visibles
|
||||
|
||||
## Seguridad
|
||||
|
||||
### Consideraciones
|
||||
|
||||
1. **No se almacenan credenciales**: La app no requiere login
|
||||
2. **SSL Pinning**: Considerar para producción
|
||||
3. **Input Validation**: Validar slugs antes de scraping
|
||||
4. **Rate Limiting**: No sobrecargar el servidor objetivo
|
||||
|
||||
---
|
||||
|
||||
**Última actualización**: Febrero 2026
|
||||
**Versión**: 1.0.0
|
||||
Reference in New Issue
Block a user