✨ 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>
19 KiB
Contributing to MangaReader
Gracias por tu interés en contribuir a MangaReader. Este documento proporciona una guía completa para contribuyentes, incluyendo cómo agregar nuevas fuentes de manga, modificar el scraper, estándares de código y testing.
Tabla de Contenidos
- Configuración del Entorno de Desarrollo
- Cómo Agregar Nuevas Fuentes de Manga
- Cómo Modificar el Scraper
- Estándares de Código
- Testing
- Pull Request Guidelines
- Troubleshooting
Configuración del Entorno de Desarrollo
Requisitos Previos
- MacOS con Xcode 15+
- iOS 15+ device o simulator
- Git para control de versiones
- Cuenta de Developer de Apple (opcional, para firmar)
Pasos Iniciales
-
Fork el repositorio (si es proyecto open source)
-
Clona tu fork:
git clone https://github.com/tu-usuario/MangaReader.git cd MangaReader/ios-app -
Abre el proyecto en Xcode:
open MangaReader.xcodeproj -
Configura el signing:
- Selecciona el proyecto en el sidebar
- En "Signing & Capabilities", elige tu Team
- Asegúrate que "Automatically manage signing" esté activado
-
Ejecuta el proyecto:
- Selecciona un dispositivo o simulador
- Presiona
Cmd + R
Cómo Agregar Nuevas Fuentes de Manga
MangaReader está diseñado para soportar múltiples fuentes de manga. Actualmente soporta manhwaweb.com, pero puedes agregar más fuentes siguiendo estos pasos.
Arquitectura de Fuentes
Las fuentes se implementan usando un protocolo común:
protocol MangaSource {
var name: String { get }
var baseURL: String { get }
func fetchMangaInfo(slug: String) async throws -> Manga
func fetchChapters(mangaSlug: String) async throws -> [Chapter]
func fetchChapterImages(chapterSlug: String) async throws -> [String]
}
Paso 1: Crear el Scraper de la Nueva Fuente
Crea un nuevo archivo en ios-app/Sources/Services/:
Ejemplo: MangaTownScraper.swift
import Foundation
import WebKit
/// Scraper para mangatown.com
/// Implementa el protocolo MangaSource para agregar soporte de esta fuente
@MainActor
class MangaTownScraper: NSObject, ObservableObject, MangaSource {
// MARK: - MangaSource Protocol
let name = "MangaTown"
let baseURL = "https://www.mangatown.com"
// MARK: - Properties
static let shared = MangaTownScraper()
private var webView: WKWebView?
// MARK: - Initialization
private override init() {
super.init()
setupWebView()
}
// MARK: - Setup
private func setupWebView() {
let configuration = WKWebViewConfiguration()
configuration.applicationNameForUserAgent = "Mozilla/5.0"
webView = WKWebView(frame: .zero, configuration: configuration)
webView?.navigationDelegate = self
}
// MARK: - MangaSource Implementation
/// Obtiene la información de un manga desde MangaTown
/// - Parameter slug: Identificador único del manga
/// - Returns: Objeto Manga con información completa
/// - Throws: ScrapingError si falla el scraping
func fetchMangaInfo(slug: String) async throws -> Manga {
guard let webView = webView else {
throw ScrapingError.webViewNotInitialized
}
let url = URL(string: "\(baseURL)/manga/\(slug)")!
try await loadURLAndWait(url, webView: webView)
// Extraer información usando JavaScript
let info: [String: Any] = try await webView.evaluateJavaScript("""
(function() {
// Implementación específica para MangaTown
return {
title: document.querySelector('.manga-title')?.textContent || '',
description: document.querySelector('.manga-summary')?.textContent || '',
// ... más extracciones
};
})();
""") as! [String: Any]
return Manga(
slug: slug,
title: info["title"] as? String ?? "",
description: info["description"] as? String ?? "",
genres: [],
status: "",
url: url.absoluteString,
coverImage: nil
)
}
/// Obtiene los capítulos de un manga
/// - Parameter mangaSlug: Slug del manga
/// - Returns: Array de capítulos ordenados
/// - Throws: ScrapingError si falla
func fetchChapters(mangaSlug: String) async throws -> [Chapter] {
// Implementación similar a scrapeChapters
// ...
return []
}
/// Obtiene las imágenes de un capítulo
/// - Parameter chapterSlug: Slug del capítulo
/// - Returns: Array de URLs de imágenes
/// - Throws: ScrapingError si falla
func fetchChapterImages(chapterSlug: String) async throws -> [String] {
// Implementación similar a scrapeChapterImages
// ...
return []
}
// MARK: - Helper Methods
private func loadURLAndWait(_ url: URL, webView: WKWebView, waitTime: Double = 3.0) async throws {
try await withCheckedThrowingContinuation { continuation in
webView.load(URLRequest(url: url))
DispatchQueue.main.asyncAfter(deadline: .now() + waitTime) {
continuation.resume()
}
}
}
}
// MARK: - WKNavigationDelegate
extension MangaTownScraper: WKNavigationDelegate {
nonisolated func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Navegación completada
}
nonisolated func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("Navigation failed: \(error.localizedDescription)")
}
}
Paso 2: Registrar la Nueva Fuente
Agrega la fuente al registro de fuentes disponibles. Crea un archivo MangaSourceRegistry.swift:
/// Registro de fuentes de manga disponibles
enum MangaSourceRegistry {
static let allSources: [MangaSource] = [
ManhwaWebScraper.shared,
MangaTownScraper.shared,
]
static func source(named name: String) -> MangaSource? {
allSources.first { $0.name == name }
}
}
Paso 3: Actualizar la UI para Seleccionar Fuente
Modifica ContentView.swift para permitir selección de fuente:
// En MangaListViewModel
@Published var selectedSource: MangaSource = ManhwaWebScraper.shared
// En ContentView
Picker("Fuente", selection: $viewModel.selectedSource) {
ForEach(MangaSourceRegistry.allSources, id: \.name) { source in
Text(source.name).tag(source)
}
}
Paso 4: Testing de la Nueva Fuente
Crea tests para verificar que el scraper funciona correctamente:
import XCTest
@testable import MangaReader
class MangaTownScraperTests: XCTestCase {
var scraper: MangaTownScraper!
override func setUp() {
super.setUp()
scraper = MangaTownScraper.shared
}
func testFetchMangaInfo() async throws {
let manga = try await scraper.fetchMangaInfo(slug: "one-piece")
XCTAssertFalse(manga.title.isEmpty)
XCTAssertFalse(manga.description.isEmpty)
}
func testFetchChapters() async throws {
let chapters = try await scraper.fetchChapters(mangaSlug: "one-piece")
XCTAssertFalse(chapters.isEmpty)
XCTAssertTrue(chapters.first?.number == 1)
}
}
Cómo Modificar el Scraper
Si la estructura de manhwaweb.com cambia y el scraper deja de funcionar, sigue estos pasos para actualizarlo.
Paso 1: Investigar la Nueva Estructura
- Abre manhwaweb.com en Safari/Chrome
- Abre Web Inspector (F12 o Cmd+Option+I)
- Navega a un manga/capítulo
- Inspecciona el HTML para encontrar los nuevos selectores
Paso 2: Identificar Selectores Clave
Busca los siguientes elementos:
Para Manga Info:
- Selector del título (ej:
h1,.title,[class*="title"]) - Selector de la descripción (ej:
p,.description) - Selector de géneros (ej:
a[href*="/genero/"],.genres a) - Selector de estado (ej: regex en body o
.status) - Selector de cover (ej:
.cover img,[class*="cover"] img)
Para Capítulos:
- Selector de links (ej:
a[href*="/leer/"],.chapter-link) - Cómo extraer el número de capítulo (regex o atributo)
Para Imágenes:
- Selector de imágenes (ej:
img,.page-image img) - Cómo distinguir imágenes de contenido de UI
Paso 3: Actualizar el JavaScript
En ManhwaWebScraper.swift, actualiza el JavaScript en los métodos correspondientes:
Ejemplo: Actualizar scrapeMangaInfo
let mangaInfo: [String: Any] = try await webView.evaluateJavaScript("""
(function() {
// ACTUALIZAR: Nuevo selector de título
let title = '';
const titleEl = document.querySelector('.nuevo-selector-titulo');
if (titleEl) {
title = titleEl.textContent?.trim() || '';
}
// ACTUALIZAR: Nueva lógica de descripción
let description = '';
const descEl = document.querySelector('.nuevo-selector-desc');
if (descEl) {
description = descEl.textContent?.trim() || '';
}
// Resto de extracciones...
return {
title: title,
description: description,
// ...
};
})();
""") as! [String: Any]
Paso 4: Probar los Cambios
- Compila y ejecuta la app
- Intenta agregar un manga existente
- Verifica que la información se muestre correctamente
- Intenta leer un capítulo
- Verifica que las imágenes carguen
Paso 5: Manejo de Errores
Agrega manejo de errores robusto:
// Agregar fallbacks
let titleEl = document.querySelector('.nuevo-selector') ||
document.querySelector('.selector-respalgo') ||
document.querySelector('h1'); // Último recurso
if (titleEl) {
title = titleEl.textContent?.trim() || '';
}
Consejos para Troubleshooting
-
Incrementa el tiempo de espera si la página carga lento:
try await loadURLAndWait(url, waitForImages: true) // Aumenta el tiempo en loadURLAndWait -
Verifica que JavaScript esté habilitado en WKWebView
-
Revisa la consola del WebView agregando logging:
webView.evaluateJavaScript("console.log('Debug info')") -
Usa
try?en vez detry!temporalmente para evitar crashes:let info = try? webView.evaluateJavaScript(...) as? [String: Any] print("Info: \(info ?? [:])")
Estándares de Código
Swift Style Guide
Sigue las convenciones de Swift para código limpio y mantenible.
1. Nomenclatura
Clases y Structs: PascalCase
class ManhwaWebScraper { }
struct Manga { }
Propiedades y Métodos: camelCase
var mangaTitle: String
func scrapeMangaInfo() { }
Constantes Privadas: camelCase con prefijo si es necesario
private let favoritesKey = "favoriteMangas"
2. Organización de Código
Usa MARK comments para organizar:
class ManhwaWebScraper {
// MARK: - Properties
private var webView: WKWebView?
// MARK: - Initialization
init() { }
// MARK: - Public Methods
func scrapeMangaInfo() { }
// MARK: - Private Methods
private func loadURLAndWait() { }
}
3. Documentación
Documenta todas las funciones públicas:
/// Obtiene la lista de capítulos de un manga
///
/// Este método carga la página del manga en un WKWebView,
/// ejecuta JavaScript para extraer los capítulos, y retorna
/// un array ordenado de manera descendente.
///
/// - Parameter mangaSlug: El slug único del manga
/// - Returns: Array de capítulos ordenados por número (descendente)
/// - Throws: `ScrapingError` si el WebView no está inicializado
/// o si falla la extracción de contenido
///
/// # Example
/// ```swift
/// do {
/// let chapters = try await scraper.scrapeChapters(mangaSlug: "one-piece")
/// print("Found \(chapters.count) chapters")
/// } catch {
/// print("Error: \(error)")
/// }
/// ```
func scrapeChapters(mangaSlug: String) async throws -> [Chapter] {
// Implementación
}
4. Async/Await
Usa async/await para código asíncrono:
// BUENO
func loadManga() async throws -> Manga {
let manga = try await scraper.scrapeMangaInfo(slug: slug)
return manga
}
// MAL (evitar completion handlers si es posible)
func loadManga(completion: @escaping (Result<Manga, Error>) -> Void) {
// ...
}
5. Error Handling
Usa errores tipados en vez de genéricos:
// BUENO
enum ScrapingError: LocalizedError {
case webViewNotInitialized
case pageLoadFailed
}
// MAL
func scrape() throws {
throw NSError(domain: "Error", code: 1, userInfo: nil)
}
6. Opcionalidad
Usa opcionales con cuidado:
// BUENO - unwrap seguro
if let coverImage = manga.coverImage {
// Usar coverImage
}
// BUENO - nil coalescing
let title = manga.title ?? "Unknown"
// MAL - force unwrap (evitar a menos que estés 100% seguro)
let image = UIImage(contentsOfFile: path)!
7. Closures
Usa trailing closure syntax cuando sea el último parámetro:
// BUENO
DispatchQueue.main.async {
print("Async code")
}
// ACEPTABLE
DispatchQueue.main.async(execute: {
print("Async code")
})
Testing
Escribir Tests
Crea tests para nuevas funcionalidades:
Ejemplo de Unit Test:
import XCTest
@testable import MangaReader
class StorageServiceTests: XCTestCase {
var storage: StorageService!
override func setUp() {
super.setUp()
storage = StorageService.shared
}
override func tearDown() {
// Limpiar después de cada test
storage.clearAllDownloads()
super.tearDown()
}
func testSaveAndRetrieveFavorite() {
// Given
let slug = "test-manga"
// When
storage.saveFavorite(mangaSlug: slug)
let isFavorite = storage.isFavorite(mangaSlug: slug)
// Then
XCTAssertTrue(isFavorite)
}
func testRemoveFavorite() {
// Given
let slug = "test-manga"
storage.saveFavorite(mangaSlug: slug)
// When
storage.removeFavorite(mangaSlug: slug)
let isFavorite = storage.isFavorite(mangaSlug: slug)
// Then
XCTAssertFalse(isFavorite)
}
}
Ejecutar Tests
- En Xcode, presiona
Cmd + Upara ejecutar todos los tests - Para ejecutar un test específico, click en el diamante junto al nombre del test
- Los tests deben ejecutarse en el simulator o en un dispositivo real
Cobertura de Código
Apunta a tener al menos 70% de cobertura de código:
- Servicios: 80%+
- ViewModels: 70%+
- Models: 90%+ (son datos simples)
- Views: 50%+ (UI testing es más difícil)
Pull Request Guidelines
Antes de Abrir un PR
-
Actualiza tu rama:
git checkout main git pull upstream main git checkout tu-rama git rebase main -
Resuelve conflicts si los hay
-
Ejecuta los tests:
xcodebuild test -scheme MangaReader -destination 'platform=iOS Simulator,name=iPhone 15' -
Limpia el código:
- Remueve
print()statements de debugging - Formatea el código (Xcode puede hacerlo automáticamente: Ctrl + I)
- Remueve comentarios TODO/FIXME implementados
- Remueve
Estructura del PR
Usa esta plantilla para tu PR:
## Descripción
Breve descripción de los cambios.
## Tipo de Cambio
- [ ] Bug fix (non-breaking change que arregla un issue)
- [ ] New feature (non-breaking change que agrega funcionalidad)
- [ ] Breaking change (fix or feature que causaría breaking changes)
- [ ] Documentation update
## Testing
Describe los tests que ejecutaste:
- [ ] Unit tests pasan
- [ ] Manual testing en device/simulator
- [ ] Probado en iOS 15, iOS 16, iOS 17
## Screenshots (si aplica)
Before/After screenshots para cambios de UI.
## Checklist
- [ ] Mi código sigue los style guidelines
- [ ] He realizado self-review de mi código
- [ ] He comentado código complejo
- [ ] He actualizado la documentación
- [ ] No hay nuevos warnings
- [ ] He agregado tests que prueban mis cambios
- [ ] Todos los tests pasan
Review Process
- Automated Checks: CI ejecutará tests automáticamente
- Code Review: Al menos 1 revisor debe aprobar
- Testing: El revisor probará los cambios en un dispositivo
- Merge: El maintainer mergeará si todo está bien
Troubleshooting
Issues Comunes
1. WebView no carga contenido
Síntoma: scrapeMangaInfo retorna datos vacíos
Solución:
- Aumenta el tiempo de espera en
loadURLAndWait - Verifica que la URL sea correcta
- Agrega logging para ver qué JavaScript retorna
2. Tests fallan en CI pero pasan localmente
Síntoma: Tests pasan en tu máquina pero fallan en GitHub Actions
Solución:
- Asegúrate de que los tests no dependen de datos locales
- Usa mocks en vez de scrapers reales en tests
- Verifica que la configuración de iOS sea la misma
3. Imágenes no cargan
Síntoma: ReaderView muestra placeholders en vez de imágenes
Solución:
- Verifica que las URLs sean válidas
- Agrega logging en
scrapeChapterImages - Prueba las URLs en un navegador
- Verifica que no haya bloqueo de red
4. El proyecto no compila
Síntoma: Errores de "Cannot find type" o "No such module"
Solución:
- Limpia el proyecto:
Cmd + Shift + K - Cierra Xcode
- Borra
DerivedData:rm -rf ~/Library/Developer/Xcode/DerivedData - Abre Xcode y rebuild
Pedir Ayuda
Si estás atascado:
- Busca en Issues existentes: Puede que alguien ya tuvo el mismo problema
- Crea un Issue con:
- Descripción detallada del problema
- Pasos para reproducir
- Logs relevantes
- Tu entorno (Xcode version, iOS version, macOS version)
- Únete a Discord/Slack (si existe) para ayuda en tiempo real
Recursos Adicionales
Documentación de Referencia
Herramientas Útiles
- SwiftLint: Linter para Swift
- SwiftFormat: Formateador de código
- SwiftGen: Generador de código para recursos
Comunidad
Última actualización: Febrero 2026 Versión: 1.0.0