Files
MangaReader/docs/ARCHITECTURE.md
renato97 b474182dd9 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>
2026-02-04 15:34:18 +01:00

32 KiB

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

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:

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

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:

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:

@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

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

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

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

@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

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