Files
MangaReader/docs/API.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

993 lines
22 KiB
Markdown

# API Documentation - MangaReader
Este documento proporciona la documentación completa de la API de MangaReader, incluyendo modelos de datos, servicios, ViewModels y sus responsabilidades.
## Tabla de Contenidos
- [Modelos de Datos](#modelos-de-datos)
- [Servicios](#servicios)
- [ViewModels](#viewmodels)
- [Views](#views)
- [Errores y Manejo de Excepciones](#errores-y-manejo-de-excepciones)
---
## Modelos de Datos
Los modelos de datos se encuentran en `ios-app/Sources/Models/Manga.swift`. Son estructuras inmutables que conforman a protocolos Swift estándar para serialización e identificación.
### Manga
Representa la información completa de un manga.
```swift
struct Manga: Codable, Identifiable, Hashable {
let id: String { slug }
let slug: String
let title: String
let description: String
let genres: [String]
let status: String
let url: String
let coverImage: String?
enum CodingKeys: String, CodingKey {
case slug, title, description, genres, status, url, coverImage
}
var displayStatus: String { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `String` | Identificador único (computed, igual a `slug`) | `"one-piece_1695365223767"` |
| `slug` | `String` | Slug único del manga usado en URLs | `"one-piece_1695365223767"` |
| `title` | `String` | Título del manga | `"One Piece"` |
| `description` | `String` | Descripción o sinopsis | `"La historia de piratas..."` |
| `genres` | `[String]` | Array de géneros literarios | `["Acción", "Aventura"]` |
| `status` | `String` | Estado de publicación (crudo) | `"PUBLICANDOSE"` |
| `url` | `String` | URL completa del manga | `"https://manhwaweb.com/manga/..."` |
| `coverImage` | `String?` | URL de imagen de portada (opcional) | `"https://..."` |
#### Métodos Computados
**`displayStatus: String`**
- Retorna el estado traducido y formateado para mostrar en UI
- Mapeos:
- `"PUBLICANDOSE"``"En publicación"`
- `"FINALIZADO"``"Finalizado"`
- `"EN_PAUSA"`, `"EN_ESPERA"``"En pausa"`
- Otro → retorna valor original
#### Ejemplo de Uso
```swift
let manga = Manga(
slug: "one-piece_1695365223767",
title: "One Piece",
description: "La historia de Monkey D. Luffy...",
genres: ["Acción", "Aventura", "Comedia"],
status: "PUBLICANDOSE",
url: "https://manhwaweb.com/manga/one-piece_1695365223767",
coverImage: "https://example.com/cover.jpg"
)
print(manga.displayStatus) // "En publicación"
print(manga.id) // "one-piece_1695365223767"
```
---
### Chapter
Representa un capítulo individual de un manga.
```swift
struct Chapter: Codable, Identifiable, Hashable {
let id: Int { number }
let number: Int
let title: String
let url: String
let slug: String
var isRead: Bool = false
var isDownloaded: Bool = false
var lastReadPage: Int = 0
var displayNumber: String { ... }
var progress: Double { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `Int` | Identificador único (computed, igual a `number`) | `1` |
| `number` | `Int` | Número del capítulo | `1` |
| `title` | `String` | Título del capítulo | `"El inicio de la aventura"` |
| `url` | `String` | URL completa del capítulo | `"https://manhwaweb.com/leer/..."` |
| `slug` | `String` | Slug para identificar el capítulo | `"one-piece/capitulo-1"` |
| `isRead` | `Bool` | Estado de lectura (mutable) | `false` |
| `isDownloaded` | `Bool` | Estado de descarga (mutable) | `false` |
| `lastReadPage` | `Int` | Última página leída (mutable) | `5` |
#### Métodos Computados
**`displayNumber: String`**
- Retorna string formateado para mostrar
- Formato: `"Capítulo {number}"`
**`progress: Double`**
- Retorna progreso como `Double` para ProgressViews
- Valor: `Double(lastReadPage)`
#### Ejemplo de Uso
```swift
var chapter = Chapter(
number: 1,
title: "El inicio",
url: "https://manhwaweb.com/leer/one-piece/1",
slug: "one-piece/1"
)
chapter.isRead = true
chapter.lastReadPage = 15
print(chapter.displayNumber) // "Capítulo 1"
print(chapter.progress) // 15.0
```
---
### MangaPage
Representa una página individual (imagen) de un capítulo.
```swift
struct MangaPage: Codable, Identifiable, Hashable {
let id: String { url }
let url: String
let index: Int
var isCached: Bool = false
var thumbnailURL: String { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `String` | Identificador único (computed, igual a `url`) | `"https://..."` |
| `url` | `String` | URL completa de la imagen | `"https://example.com/page1.jpg"` |
| `index` | `Int` | Índice de la página en el capítulo | `0` |
| `isCached` | `Bool` | Estado de cache local (mutable) | `false` |
#### Métodos Computados
**`thumbnailURL: String`**
- Actualmente retorna la misma URL
- Futuro: implementar versión thumbnail optimizada
---
### ReadingProgress
Almacena el progreso de lectura de un usuario.
```swift
struct ReadingProgress: Codable {
let mangaSlug: String
let chapterNumber: Int
let pageNumber: Int
let timestamp: Date
var isCompleted: Bool { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `mangaSlug` | `String` | Slug del manga |
| `chapterNumber` | `Int` | Número del capítulo |
| `pageNumber` | `Int` | Página actual |
| `timestamp` | `Date` | Fecha/hora de lectura |
#### Métodos Computados
**`isCompleted: Bool`**
- Retorna `true` si el usuario leyó más de 5 páginas
- Lógica: `return pageNumber > 5`
---
### DownloadedChapter
Representa un capítulo descargado localmente.
```swift
struct DownloadedChapter: Codable, Identifiable {
let id: String { "\(mangaSlug)-chapter\(chapterNumber)" }
let mangaSlug: String
let mangaTitle: String
let chapterNumber: Int
let pages: [MangaPage]
let downloadedAt: Date
var totalSize: Int64 = 0
var displayTitle: String { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `id` | `String` | ID compuesto único |
| `mangaSlug` | `String` | Slug del manga |
| `mangaTitle` | `String` | Título del manga |
| `chapterNumber` | `Int` | Número del capítulo |
| `pages` | `[MangaPage]` | Array de páginas |
| `downloadedAt` | `Date` | Fecha de descarga |
| `totalSize` | `Int64` | Tamaño total en bytes |
---
## Servicios
Los servicios encapsulan la lógica de negocio y se encuentran en `ios-app/Sources/Services/`.
### ManhwaWebScraper
Servicio responsable del scraping de contenido web desde manhwaweb.com.
```swift
@MainActor
class ManhwaWebScraper: NSObject, ObservableObject
```
#### Propiedades
| Propiedad | Tipo | Acceso | Descripción |
|-----------|------|--------|-------------|
| `shared` | `ManhwaWebScraper` | `static` | Instancia singleton compartida |
| `webView` | `WKWebView?` | `private` | WebView para ejecutar JavaScript |
#### Métodos Públicos
##### `scrapeMangaInfo(mangaSlug:)`
Obtiene la información completa de un manga.
**Firma:**
```swift
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga
```
**Parámetros:**
- `mangaSlug`: `String` - Slug único del manga
**Retorna:**
- `Manga` - Objeto con información completa del manga
**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`
- `ScrapingError.noContentFound`
**Ejemplo:**
```swift
do {
let manga = try await ManhwaWebScraper.shared.scrapeMangaInfo(
mangaSlug: "one-piece_1695365223767"
)
print("Manga: \(manga.title)")
} catch {
print("Error: \(error.localizedDescription)")
}
```
**Proceso Interno:**
1. Construye URL: `https://manhwaweb.com/manga/{slug}`
2. Carga URL en WKWebView
3. Espera 3 segundos a que JavaScript renderice
4. Ejecuta JavaScript para extraer:
- Título (de `<h1>` o `<title>`)
- Descripción (de `<p>` con >100 caracteres)
- Géneros (de links `/genero/*`)
- Estado (via regex)
- Cover image (de `.cover img`)
5. Parsea resultado a `Manga`
---
##### `scrapeChapters(mangaSlug:)`
Obtiene la lista de capítulos de un manga.
**Firma:**
```swift
func scrapeChapters(mangaSlug: String) async throws -> [Chapter]
```
**Parámetros:**
- `mangaSlug`: `String` - Slug del manga
**Retorna:**
- `[Chapter]` - Array de capítulos ordenados descendente
**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`
- `ScrapingError.parsingError`
**Ejemplo:**
```swift
do {
let chapters = try await ManhwaWebScraper.shared.scrapeChapters(
mangaSlug: "one-piece_1695365223767"
)
print("Found \(chapters.count) chapters")
} catch {
print("Error: \(error.localizedDescription)")
}
```
**Proceso Interno:**
1. Carga página del manga
2. Ejecuta JavaScript que:
- Busca todos los links `/leer/*`
- Extrae número de capítulo via regex
- Filtra duplicados
- Ordena descendente
3. Parsea a array de `Chapter`
---
##### `scrapeChapterImages(chapterSlug:)`
Obtiene las URLs de las imágenes de un capítulo.
**Firma:**
```swift
func scrapeChapterImages(chapterSlug: String) async throws -> [String]
```
**Parámetros:**
- `chapterSlug`: `String` - Slug del capítulo
**Retorna:**
- `[String]` - Array de URLs de imágenes en orden
**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`
**Ejemplo:**
```swift
do {
let images = try await ManhwaWebScraper.shared.scrapeChapterImages(
chapterSlug: "one-piece/1"
)
print("Found \(images.count) pages")
for (index, imageUrl) in images.enumerated() {
print("Page \(index + 1): \(imageUrl)")
}
} catch {
print("Error: \(error.localizedDescription)")
}
```
**Proceso Interno:**
1. Carga URL del capítulo
2. Espera 5 segundos (para imágenes)
3. Ejecuta JavaScript que:
- Selecciona todas las etiquetas `<img>`
- Filtra elementos de UI (avatars, icons, logos)
- Elimina duplicados
4. Retorna array de URLs
---
#### Métodos Privados
##### `setupWebView()`
Configura el WKWebView con preferencias optimizadas.
##### `loadURLAndWait(_:waitForImages:)`
Carga una URL y espera a que JavaScript termine de renderizar.
**Parámetros:**
- `url`: `URL` - URL a cargar
- `waitForImages`: `Bool` - Si `true`, espera 5 segundos; si `false`, 3 segundos
---
### StorageService
Servicio responsable del almacenamiento local de datos.
```swift
class StorageService
```
#### Propiedades
| Propiedad | Tipo | Acceso | Descripción |
|-----------|------|--------|-------------|
| `shared` | `StorageService` | `static` | Instancia singleton compartida |
| `documentsDirectory` | `URL` | `private` | Directorio de documentos |
| `chaptersDirectory` | `URL` | `private` | Directorio de capítulos |
#### Gestión de Favoritos
##### `getFavorites()`
```swift
func getFavorites() -> [String]
```
Retorna array de slugs de mangas favoritos.
##### `saveFavorite(mangaSlug:)`
```swift
func saveFavorite(mangaSlug: String)
```
Guarda un manga como favorito (no duplica si ya existe).
##### `removeFavorite(mangaSlug:)`
```swift
func removeFavorite(mangaSlug: String)
```
Elimina un manga de favoritos.
##### `isFavorite(mangaSlug:)`
```swift
func isFavorite(mangaSlug: String) -> Bool
```
Verifica si un manga es favorito.
**Ejemplo:**
```swift
let storage = StorageService.shared
// Guardar favorito
storage.saveFavorite(mangaSlug: "one-piece_1695365223767")
// Verificar
if storage.isFavorite(mangaSlug: "one-piece_1695365223767") {
print("Es favorito")
}
// Listar todos
let favorites = storage.getFavorites()
print("Tienes \(favorites.count) favoritos")
// Eliminar
storage.removeFavorite(mangaSlug: "one-piece_1695365223767")
```
---
#### Gestión de Progreso de Lectura
##### `saveReadingProgress(_:)`
```swift
func saveReadingProgress(_ progress: ReadingProgress)
```
Guarda o actualiza el progreso de lectura.
##### `getReadingProgress(mangaSlug:chapterNumber:)`
```swift
func getReadingProgress(mangaSlug: String, chapterNumber: Int) -> ReadingProgress?
```
Retorna el progreso de un capítulo específico.
##### `getAllReadingProgress()`
```swift
func getAllReadingProgress() -> [ReadingProgress]
```
Retorna todo el progreso guardado.
##### `getLastReadChapter(mangaSlug:)`
```swift
func getLastReadChapter(mangaSlug: String) -> ReadingProgress?
```
Retorna el capítulo más reciente leído de un manga.
**Ejemplo:**
```swift
let storage = StorageService.shared
// Guardar progreso
let progress = ReadingProgress(
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1,
pageNumber: 15,
timestamp: Date()
)
storage.saveReadingProgress(progress)
// Recuperar progreso
if let savedProgress = storage.getReadingProgress(
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1
) {
print("Última página: \(savedProgress.pageNumber)")
}
// Último capítulo leído
if let lastChapter = storage.getLastReadChapter(mangaSlug: "one-piece_1695365223767") {
print("Último capítulo: \(lastChapter.chapterNumber)")
}
```
---
#### Gestión de Capítulos Descargados
##### `saveDownloadedChapter(_:)`
```swift
func saveDownloadedChapter(_ chapter: DownloadedChapter)
```
Guarda metadata de un capítulo descargado.
##### `getDownloadedChapters()`
```swift
func getDownloadedChapters() -> [DownloadedChapter]
```
Retorna todos los capítulos descargados.
##### `isChapterDownloaded(mangaSlug:chapterNumber:)`
```swift
func isChapterDownloaded(mangaSlug: String, chapterNumber: Int) -> Bool
```
Verifica si un capítulo está descargado.
##### `deleteDownloadedChapter(mangaSlug:chapterNumber:)`
```swift
func deleteDownloadedChapter(mangaSlug: String, chapterNumber: Int)
```
Elimina un capítulo descargado (archivos + metadata).
**Ejemplo:**
```swift
let storage = StorageService.shared
// Verificar si está descargado
if storage.isChapterDownloaded(
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1
) {
print("Capítulo ya descargado")
} else {
print("Capítulo no descargado")
}
// Eliminar capítulo
storage.deleteDownloadedChapter(
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1
)
```
---
#### Gestión de Imágenes
##### `saveImage(_:mangaSlug:chapterNumber:pageIndex:)`
```swift
func saveImage(_ image: UIImage, mangaSlug: String, chapterNumber: Int, pageIndex: Int) async throws -> URL
```
Guarda una imagen en disco local.
**Parámetros:**
- `image`: `UIImage` - Imagen a guardar
- `mangaSlug`: `String` - Slug del manga
- `chapterNumber`: `Int` - Número del capítulo
- `pageIndex`: `Int` - Índice de la página
**Retorna:**
- `URL` - Ruta del archivo guardado
**Errors:**
- Error si no se puede convertir o guardar
**Ejemplo:**
```swift
let storage = StorageService.shared
do {
let imageURL = try await storage.saveImage(
image: myUIImage,
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1,
pageIndex: 0
)
print("Imagen guardada en: \(imageURL.path)")
} catch {
print("Error guardando imagen: \(error)")
}
```
##### `loadImage(mangaSlug:chapterNumber:pageIndex:)`
```swift
func loadImage(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> UIImage?
```
Carga una imagen desde disco.
##### `getImageURL(mangaSlug:chapterNumber:pageIndex:)`
```swift
func getImageURL(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> URL?
```
Retorna la URL local de una imagen si existe.
---
#### Gestión de Almacenamiento
##### `getStorageSize()`
```swift
func getStorageSize() -> Int64
```
Retorna el tamaño total usado en bytes.
##### `clearAllDownloads()`
```swift
func clearAllDownloads()
```
Elimina todos los capítulos descargados.
##### `formatFileSize(_:)`
```swift
func formatFileSize(_ bytes: Int64) -> String
```
Formatea bytes a string legible (KB, MB, GB).
**Ejemplo:**
```swift
let storage = StorageService.shared
// Obtener tamaño usado
let size = storage.getStorageSize()
let formatted = storage.formatFileSize(size)
print("Almacenamiento usado: \(formatted)")
// Limpiar todo
storage.clearAllDownloads()
```
---
## ViewModels
Los ViewModels coordinan entre Services y Views.
### MangaListViewModel
ViewModel para la lista principal de mangas.
```swift
@MainActor
class MangaListViewModel: ObservableObject
```
#### Propiedades Publicadas
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `mangas` | `[Manga]` | Lista de mangas cargados |
| `isLoading` | `Bool` | Indicador de carga |
| `searchText` | `String` | Texto de búsqueda |
| `filter` | `MangaFilter` | Filtro actual (all/favorites/downloaded) |
| `newMangaSlug` | `String` | Slug del nuevo manga a agregar |
#### Métodos Públicos
##### `loadMangas()`
```swift
func loadMangas() async
```
Carga los mangas guardados en favoritos.
##### `addManga(_:)`
```swift
func addManga(_ slug: String) async
```
Agrega un nuevo manga mediante scraping.
#### Propiedades Computadas
##### `filteredMangas`
```swift
var filteredMangas: [Manga]
```
Retorna mangas filtrados por búsqueda y categoría.
**Ejemplo:**
```swift
@StateObject private var viewModel = MangaListViewModel()
// Cargar mangas
await viewModel.loadMangas()
// Agregar manga
await viewModel.addManga("one-piece_1695365223767")
// Filtrar
viewModel.filter = .favorites
viewModel.searchText = "one"
```
---
### MangaDetailViewModel
ViewModel para el detalle de un manga.
```swift
@MainActor
class MangaDetailViewModel: ObservableObject
```
#### Propiedades Publicadas
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `chapters` | `[Chapter]` | Lista de capítulos |
| `isLoadingChapters` | `Bool` | Indicador de carga |
| `isFavorite` | `Bool` | Estado de favorito |
| `selectedChapter` | `Chapter?` | Capítulo seleccionado |
| `showingDownloadAll` | `Bool` | Mostrar diálogo de descarga |
#### Métodos Públicos
##### `loadChapters()`
```swift
func loadChapters() async
```
Carga los capítulos del manga.
##### `toggleFavorite()`
```swift
func toggleFavorite()
```
Alterna el estado de favorito.
##### `downloadAllChapters()`
```swift
func downloadAllChapters()
```
Inicia descarga de todos los capítulos.
##### `downloadLastChapters(count:)`
```swift
func downloadLastChapters(count: Int)
```
Descarga los últimos N capítulos.
**Ejemplo:**
```swift
@StateObject private var viewModel = MangaDetailViewModel(manga: manga)
// Cargar capítulos
await viewModel.loadChapters()
// Marcar favorito
viewModel.toggleFavorite()
// Descargar últimos 10
viewModel.downloadLastChapters(count: 10)
```
---
### ReaderViewModel
ViewModel para el lector de capítulos.
```swift
@MainActor
class ReaderViewModel: ObservableObject
```
#### Propiedades Publicadas
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `pages` | `[MangaPage]` | Lista de páginas |
| `currentPage` | `Int` | Página actual |
| `isLoading` | `Bool` | Indicador de carga |
| `showError` | `Bool` | Mostrar error |
| `showControls` | `Bool` | Mostrar controles UI |
| `isFavorite` | `Bool` | Estado de favorito |
| `isDownloaded` | `Bool` | Capítulo descargado |
| `backgroundColor` | `Color` | Color de fondo |
| `readingMode` | `ReadingMode` | Modo de lectura |
#### Métodos Públicos
##### `loadPages()`
```swift
func loadPages() async
```
Carga las páginas del capítulo (desde local o web).
##### `cachePage(_:image:)`
```swift
func cachePage(_ page: MangaPage, image: Image) async
```
Cachea una página localmente (TODO: implementar).
##### `toggleFavorite()`
```swift
func toggleFavorite()
```
Alterna favorito del manga actual.
##### `cycleBackgroundColor()`
```swift
func cycleBackgroundColor()
```
Cicla entre colores de fondo (blanco/negro/sepia).
#### Propiedades Computadas
- `currentPageIndex`: `Int` - Índice de página actual
- `totalPages`: `Int` - Total de páginas
**Ejemplo:**
```swift
@StateObject private var viewModel = ReaderViewModel(manga: manga, chapter: chapter)
// Cargar páginas
await viewModel.loadPages()
// Ir a página específica
viewModel.currentPage = 10
// Cambiar fondo
viewModel.cycleBackgroundColor()
// Cambiar modo lectura
viewModel.readingMode = .horizontal
```
---
## Views
Las vistas son componentes SwiftUI que presentan la UI.
### ContentView
Vista principal que muestra la lista de mangas.
**Componentes:**
- `MangaRowView`: Fila individual de manga
- `MangaListViewModel`: ViewModel asociado
**Funcionalidades:**
- Búsqueda de mangas
- Filtros (todos/favoritos/descargados)
- Agregar manga manualmente
- Pull-to-refresh
### MangaDetailView
Vista de detalle de un manga específico.
**Componentes:**
- `ChapterRowView`: Fila de capítulo
- `FlowLayout`: Layout de géneros
- `MangaDetailViewModel`: ViewModel asociado
**Funcionalidades:**
- Mostrar información del manga
- Listar capítulos
- Marcar favorito
- Descargar capítulos
### ReaderView
Vista de lectura de capítulos.
**Componentes:**
- `PageView`: Vista de página individual
- `ReaderViewModel`: ViewModel asociado
**Funcionalidades:**
- Mostrar páginas con zoom/pan
- Navegación entre páginas
- Configurar fondo
- Cambiar modo de lectura
- Slider de navegación
---
## Errores y Manejo de Excepciones
### ScrapingError
Errores específicos del scraper.
```swift
enum ScrapingError: LocalizedError {
case webViewNotInitialized
case pageLoadFailed
case noContentFound
case parsingError
var errorDescription: String? { ... }
}
```
#### Casos
| Error | Descripción | Mensaje (Español) |
|-------|-------------|-------------------|
| `webViewNotInitialized` | WKWebView no configurado | "WebView no está inicializado" |
| `pageLoadFailed` | Error cargando página | "Error al cargar la página" |
| `noContentFound` | No se encontró contenido | "No se encontró contenido" |
| `parsingError` | Error procesando datos | "Error al procesar el contenido" |
### Estrategias de Manejo de Errores
1. **ViewModels**: Capturan errores del scraper
2. **Views**: Muestran alertas al usuario
3. **Servicios**: Propagan errores con `throws`
**Ejemplo:**
```swift
do {
let manga = try await scraper.scrapeMangaInfo(mangaSlug: slug)
// Manejar éxito
} catch ScrapingError.webViewNotInitialized {
// Mostrar alerta específica
errorMessage = "Error de configuración"
} catch {
// Error genérico
errorMessage = error.localizedDescription
}
```
---
**Última actualización**: Febrero 2026
**Versión**: 1.0.0