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:
587
IMPLEMENTATION_GUIDE.md
Normal file
587
IMPLEMENTATION_GUIDE.md
Normal file
@@ -0,0 +1,587 @@
|
||||
# Quick Implementation Guide - MangaReader Optimizations
|
||||
|
||||
## 📂 Archivos Creados
|
||||
|
||||
```
|
||||
ios-app/Sources/Services/
|
||||
├── ManhwaWebScraperOptimized.swift # Scraper con cache inteligente
|
||||
├── StorageServiceOptimized.swift # Storage con compresión y thumbnails
|
||||
├── ImageCache.swift # Sistema de cache de imágenes
|
||||
└── CacheManager.swift # Gerente central de cache
|
||||
|
||||
ios-app/Sources/Views/
|
||||
└── ReaderViewOptimized.swift # Reader optimizado
|
||||
|
||||
OPTIMIZATION_SUMMARY.md # Documentación completa
|
||||
IMPLEMENTATION_GUIDE.md # Este archivo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Migración Rápida
|
||||
|
||||
### Opción 1: Reemplazo Directo (Recomendado para Testing)
|
||||
|
||||
```swift
|
||||
// 1. En tus ViewModels, cambia:
|
||||
private let scraper = ManhwaWebScraper.shared
|
||||
// Por:
|
||||
private let scraper = ManhwaWebScraperOptimized.shared
|
||||
|
||||
// 2. En tus Views, cambia:
|
||||
private let storage = StorageService.shared
|
||||
// Por:
|
||||
private let storage = StorageServiceOptimized.shared
|
||||
|
||||
// 3. En ReaderView:
|
||||
// Reemplaza completamente por ReaderViewOptimized
|
||||
```
|
||||
|
||||
### Opción 2: Migración Gradual (Más Segura)
|
||||
|
||||
```swift
|
||||
// Usa alias de tipo para migrar gradualmente
|
||||
typealias Scraper = ManhwaWebScraperOptimized
|
||||
typealias Storage = StorageServiceOptimized
|
||||
|
||||
// Código existente funciona sin cambios
|
||||
private let scraper = Scraper.shared
|
||||
private let storage = Storage.shared
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuración Inicial
|
||||
|
||||
### 1. Inicializar CacheManager en AppDelegate
|
||||
|
||||
```swift
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions ...
|
||||
) -> Bool {
|
||||
|
||||
// Inicializar cache manager
|
||||
let cacheManager = CacheManager.shared
|
||||
|
||||
#if DEBUG
|
||||
// Imprimir reporte inicial en debug
|
||||
cacheManager.printCacheReport()
|
||||
#endif
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Monitoring de background
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// El CacheManager ya maneja esto automáticamente
|
||||
// pero puedes agregar lógica adicional aquí
|
||||
}
|
||||
|
||||
// Monitoring de memory warnings
|
||||
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
|
||||
// CacheManager ya responde automáticamente
|
||||
print("⚠️ Memory warning received")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Configurar Ajustes de Cache
|
||||
|
||||
```swift
|
||||
// En algún lugar de configuración (ej: SettingsViewModel)
|
||||
|
||||
struct CacheSettings {
|
||||
// Ajusta según tus necesidades
|
||||
|
||||
// Tamaño máximo de cache (en bytes)
|
||||
static let maxImageCacheSize: Int64 = 500 * 1024 * 1024 // 500 MB
|
||||
static let maxMetadataCacheSize: Int64 = 50 * 1024 * 1024 // 50 MB
|
||||
|
||||
// Tiempos de expiración (en segundos)
|
||||
static let htmlCacheDuration: TimeInterval = 1800 // 30 min
|
||||
static let imageCacheDuration: TimeInterval = 7 * 86400 // 7 días
|
||||
static let thumbnailCacheDuration: TimeInterval = 30 * 86400 // 30 días
|
||||
|
||||
// Preloading
|
||||
static let preloadAdjacentPages: Int = 2 // 2 páginas antes/después
|
||||
static let enablePreloading: Bool = true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoreo y Debugging
|
||||
|
||||
### Ver Estadísticas en Debug
|
||||
|
||||
```swift
|
||||
#if DEBUG
|
||||
// Agregar botón de debug en settings
|
||||
struct DebugSettingsView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Button("Print Cache Report") {
|
||||
CacheManager.shared.printCacheReport()
|
||||
}
|
||||
|
||||
Button("Print Image Statistics") {
|
||||
ImageCache.shared.printStatistics()
|
||||
}
|
||||
|
||||
Button("Clear All Cache") {
|
||||
CacheManager.shared.clearAllCache()
|
||||
}
|
||||
|
||||
Button("Simulate Memory Warning") {
|
||||
NotificationCenter.default.post(
|
||||
name: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
### Instruments Configuration
|
||||
|
||||
```bash
|
||||
# Para perfilado de memoria
|
||||
1. Xcode → Product → Profile
|
||||
2. Seleccionar "Allocations"
|
||||
3. Monitorear:
|
||||
- Overall memory usage
|
||||
- Anonymous VM
|
||||
- Image cache size
|
||||
|
||||
# Para perfilado de tiempo
|
||||
1. Seleccionar "Time Profiler"
|
||||
2. Buscar funciones lentas
|
||||
3. Verificar que cache esté funcionando
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
### Tests Funcionales
|
||||
|
||||
```swift
|
||||
class CachePerformanceTests: XCTestCase {
|
||||
|
||||
func testScraperCache() async throws {
|
||||
let scraper = ManhwaWebScraperOptimized.shared
|
||||
|
||||
// Primer scraping (debe ser lento)
|
||||
let start1 = Date()
|
||||
_ = try await scraper.scrapeChapters(mangaSlug: "test-manga")
|
||||
let time1 = Date().timeIntervalSince(start1)
|
||||
|
||||
// Segundo scraping (debe ser rápido por cache)
|
||||
let start2 = Date()
|
||||
_ = try await scraper.scrapeChapters(mangaSlug: "test-manga")
|
||||
let time2 = Date().timeIntervalSince(start2)
|
||||
|
||||
// El segundo debe ser >10x más rápido
|
||||
XCTAssertLessThan(time2, time1 / 10)
|
||||
}
|
||||
|
||||
func testImageCompression() {
|
||||
let storage = StorageServiceOptimized.shared
|
||||
|
||||
// Crear imagen de prueba grande (4 MB)
|
||||
let largeImage = createTestImage(size: CGSize(width: 2000, height: 3000))
|
||||
|
||||
let expectation = XCTestExpectation(description: "Compress and save")
|
||||
|
||||
Task {
|
||||
let url = try await storage.saveImage(
|
||||
largeImage,
|
||||
mangaSlug: "test",
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
)
|
||||
|
||||
// Verificar que archivo resultante es < 2 MB
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
||||
let fileSize = attributes[.size] as! Int64
|
||||
|
||||
XCTAssertLessThan(fileSize, 2 * 1024 * 1024)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: 5.0)
|
||||
}
|
||||
|
||||
func testPreloading() {
|
||||
let viewModel = ReaderViewModelOptimized(
|
||||
manga: testManga,
|
||||
chapter: testChapter
|
||||
)
|
||||
|
||||
// Cargar páginas
|
||||
let expectation = XCTestExpectation(description: "Load pages")
|
||||
Task {
|
||||
await viewModel.loadPages()
|
||||
expectation.fulfill()
|
||||
}
|
||||
wait(for: [expectation], timeout: 10.0)
|
||||
|
||||
// Simular navegación a página 5
|
||||
viewModel.currentPage = 5
|
||||
|
||||
// Verificar que páginas 3, 4, 6, 7 están en cache
|
||||
let imageCache = ImageCache.shared
|
||||
XCTAssertNotNil(imageCache.image(for: viewModel.pages[3].url))
|
||||
XCTAssertNotNil(imageCache.image(for: viewModel.pages[4].url))
|
||||
XCTAssertNotNil(imageCache.image(for: viewModel.pages[6].url))
|
||||
XCTAssertNotNil(imageCache.image(for: viewModel.pages[7].url))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tests de Memoria
|
||||
|
||||
```swift
|
||||
class MemoryTests: XCTestCase {
|
||||
|
||||
func testMemoryUnderLoad() {
|
||||
let viewModel = ReaderViewModelOptimized(
|
||||
manga: largeManga, // 100 páginas
|
||||
chapter: largeChapter
|
||||
)
|
||||
|
||||
let memoryBefore = getMemoryUsage()
|
||||
|
||||
let expectation = XCTestExpectation(description: "Load all pages")
|
||||
Task {
|
||||
await viewModel.loadPages()
|
||||
|
||||
// Navegar por todas las páginas
|
||||
for i in 0..<viewModel.pages.count {
|
||||
viewModel.currentPage = i
|
||||
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: 60.0)
|
||||
|
||||
let memoryAfter = getMemoryUsage()
|
||||
let memoryIncrease = memoryAfter - memoryBefore
|
||||
|
||||
// No debe aumentar más de 150 MB
|
||||
XCTAssertLessThan(memoryIncrease, 150 * 1024 * 1024)
|
||||
}
|
||||
|
||||
func testMemoryWarningResponse() {
|
||||
// Llenar cache
|
||||
let cache = ImageCache.shared
|
||||
for i in 0..<100 {
|
||||
cache.setImage(createTestImage(), for: "test_\(i)")
|
||||
}
|
||||
|
||||
let itemsBefore = cache.getCacheStatistics().memoryCacheHits
|
||||
|
||||
// Simular memory warning
|
||||
NotificationCenter.default.post(
|
||||
name: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Esperar un poco
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
|
||||
// Cache debe estar vacío o casi vacío
|
||||
let itemsAfter = cache.getCacheStatistics().memoryCacheHits
|
||||
XCTAssertLessThan(itemsAfter, itemsBefore / 2)
|
||||
}
|
||||
|
||||
private func getMemoryUsage() -> UInt64 {
|
||||
var info = mach_task_basic_info()
|
||||
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
|
||||
|
||||
let result = withUnsafeMutablePointer(to: &info) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
||||
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
||||
}
|
||||
}
|
||||
|
||||
return result == KERN_SUCCESS ? info.resident_size : 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Optimizaciones por Categoría
|
||||
|
||||
### Scraper Optimizations
|
||||
|
||||
| Optimización | Archivo | Líneas Clave |
|
||||
|--------------|---------|--------------|
|
||||
| WKWebView reuse | `ManhwaWebScraperOptimized.swift` | 30-45 |
|
||||
| HTML cache | `ManhwaWebScraperOptimized.swift` | 48-62 |
|
||||
| Precompiled JS | `ManhwaWebScraperOptimized.swift` | 67-122 |
|
||||
| Adaptive timeout | `ManhwaWebScraperOptimized.swift` | 127-165 |
|
||||
| Concurrency control | `ManhwaWebScraperOptimized.swift` | 170-195 |
|
||||
|
||||
### Storage Optimizations
|
||||
|
||||
| Optimización | Archivo | Líneas Clave |
|
||||
|--------------|---------|--------------|
|
||||
| Adaptive compression | `StorageServiceOptimized.swift` | 28-52 |
|
||||
| Thumbnail system | `StorageServiceOptimized.swift` | 57-85 |
|
||||
| Lazy loading | `StorageServiceOptimized.swift` | 478-488 |
|
||||
| Auto cleanup | `StorageServiceOptimized.swift` | 330-380 |
|
||||
| Batch operations | `StorageServiceOptimized.swift` | 220-245 |
|
||||
|
||||
### Reader Optimizations
|
||||
|
||||
| Optimización | Archivo | Líneas Clave |
|
||||
|--------------|---------|--------------|
|
||||
| NSCache | `ImageCache.swift` | 22-45 |
|
||||
| Preloading | `ReaderViewOptimized.swift` | 195-215 |
|
||||
| Memory management | `ReaderViewOptimized.swift` | 420-445 |
|
||||
| Debouncing | `ReaderViewOptimized.swift` | 505-525 |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Problema: Cache no funciona
|
||||
|
||||
```swift
|
||||
// Verificar que el singleton se esté usando correctamente
|
||||
let scraper = ManhwaWebScraperOptimized.shared
|
||||
// NO: let scraper = ManhwaWebScraperOptimized()
|
||||
|
||||
// Verificar que los métodos tengan @MainActor si es necesario
|
||||
@MainActor
|
||||
func loadChapters() async throws {
|
||||
// código
|
||||
}
|
||||
```
|
||||
|
||||
### Problema: Memoria sigue alta
|
||||
|
||||
```swift
|
||||
// 1. Verificar que se estén liberando referencias
|
||||
deinit {
|
||||
progressSaveTimer?.invalidate()
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// 2. Usizar weak en closures
|
||||
Task { [weak self] in
|
||||
await self?.loadPages()
|
||||
}
|
||||
|
||||
// 3. Verificar en Instruments
|
||||
// - Revisar "Anonymous VM" (debe ser bajo)
|
||||
// - Buscar "Malloc" leaks
|
||||
```
|
||||
|
||||
### Problema: Preloading no funciona
|
||||
|
||||
```swift
|
||||
// Verificar que esté habilitado
|
||||
viewModel.enablePreloading = true // Debe ser true
|
||||
|
||||
// Verificar que onAppear se llame
|
||||
PageView(page: page)
|
||||
.onAppear {
|
||||
print("Page \(page.index) appeared") // Debug
|
||||
viewModel.preloadAdjacentPages(...)
|
||||
}
|
||||
|
||||
// Verificar priority queue
|
||||
Task(priority: .utility) {
|
||||
// Debe usar .utility, no .background
|
||||
}
|
||||
```
|
||||
|
||||
### Problema: Thumbnails no se generan
|
||||
|
||||
```swift
|
||||
// Verificar directorio de thumbnails
|
||||
let thumbDir = storage.getThumbnailDirectory(...)
|
||||
print("Thumb dir: \(thumbDir.path)")
|
||||
|
||||
// Verificar que se cree el directorio
|
||||
try? FileManager.default.createDirectory(
|
||||
at: thumbDir,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
// Verificar que se guarde correctamente
|
||||
try? thumbData.write(to: thumbnailURL)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 Métricas de Éxito
|
||||
|
||||
### Objetivos de Rendimiento
|
||||
|
||||
```swift
|
||||
struct PerformanceTargets {
|
||||
// Tiempos de carga
|
||||
static let maxChapterLoadTime: TimeInterval = 2.0 // segundos
|
||||
static let maxPageLoadTime: TimeInterval = 0.5 // segundos (con cache)
|
||||
static let maxAppLaunchTime: TimeInterval = 1.0 // segundos
|
||||
|
||||
// Memoria
|
||||
static let maxReaderMemory: UInt64 = 120 * 1024 * 1024 // 120 MB
|
||||
static let maxScraperMemory: UInt64 = 50 * 1024 * 1024 // 50 MB
|
||||
|
||||
// Cache
|
||||
static let minCacheHitRate: Double = 0.80 // 80%
|
||||
static let maxCacheSize: Int64 = 500 * 1024 * 1024 // 500 MB
|
||||
}
|
||||
|
||||
// Función de verificación
|
||||
func verifyPerformanceTargets() {
|
||||
let cacheStats = ImageCache.shared.getCacheStatistics()
|
||||
|
||||
let hitRate = cacheStats.hitRate
|
||||
XCTAssertGreaterThan(hitRate, PerformanceTargets.minCacheHitRate,
|
||||
"Cache hit rate \(hitRate) below target")
|
||||
|
||||
// ... más verificaciones
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Próximos Pasos
|
||||
|
||||
### Implementación Inmediata
|
||||
|
||||
1. **Backup del código existente**
|
||||
```bash
|
||||
cp -r ios-app/Sources ios-app/Sources.backup
|
||||
```
|
||||
|
||||
2. **Reemplazar archivos uno por uno**
|
||||
- Empezar con ManhwaWebScraperOptimized
|
||||
- Probar exhaustivamente
|
||||
- Continuar con StorageServiceOptimized
|
||||
- etc.
|
||||
|
||||
3. **Testing completo**
|
||||
- Unit tests
|
||||
- Integration tests
|
||||
- Manual testing
|
||||
|
||||
### Optimizaciones Futuras (Opcionales)
|
||||
|
||||
1. **Prefetching Predictivo**
|
||||
```swift
|
||||
// Usar ML para predecir próximo capítulo
|
||||
func predictNextChapter(userHistory: [Chapter]) -> Chapter? {
|
||||
// Implementación básica de ML
|
||||
}
|
||||
```
|
||||
|
||||
2. **Compresión HEIC**
|
||||
```swift
|
||||
// Usar HEIC en lugar de JPEG (50% más eficiente)
|
||||
if #available(iOS 11.0, *) {
|
||||
let heicData = image.heicData()
|
||||
}
|
||||
```
|
||||
|
||||
3. **Progressive Image Loading**
|
||||
```swift
|
||||
// Cargar versión baja resolución primero
|
||||
// Luego reemplazar con alta resolución
|
||||
```
|
||||
|
||||
4. **Background Sync**
|
||||
```swift
|
||||
// Sincronizar en background usando BGTaskScheduler
|
||||
func scheduleBackgroundSync() {
|
||||
let request = BGAppRefreshTaskRequest(...)
|
||||
try? BGTaskScheduler.shared.submit(request)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips y Best Practices
|
||||
|
||||
### DO ✅
|
||||
|
||||
```swift
|
||||
// 1. Usar singletons para caches
|
||||
static let shared = ImageCache()
|
||||
|
||||
// 2. Usar colas para I/O
|
||||
private let ioQueue = DispatchQueue(label: "...", qos: .utility)
|
||||
|
||||
// 3. Weak references en closures
|
||||
Task { [weak self] in
|
||||
await self?.loadData()
|
||||
}
|
||||
|
||||
// 4. Responder a memory warnings
|
||||
@objc func handleMemoryWarning() {
|
||||
cache.removeAllObjects()
|
||||
}
|
||||
|
||||
// 5. Debouncing de operaciones frecuentes
|
||||
func saveDebounced() {
|
||||
timer?.invalidate()
|
||||
timer = Timer.scheduledTimer(...)
|
||||
}
|
||||
```
|
||||
|
||||
### DON'T ❌
|
||||
|
||||
```swift
|
||||
// 1. NO crear múltiples instancias de cache
|
||||
// let cache1 = ImageCache()
|
||||
// let cache2 = ImageCache() // ❌
|
||||
|
||||
// 2. NO hacer I/O en main thread
|
||||
// let data = try Data(contentsOf: url) // ❌ Bloquea
|
||||
|
||||
// 3. NO olvidar weak en closures
|
||||
// Task {
|
||||
// self.doSomething() // ❌ Memory leak
|
||||
// }
|
||||
|
||||
// 4. NO ignorar memory warnings
|
||||
// @objc func handleMemoryWarning() {
|
||||
// // ❌ No hacer nada
|
||||
// }
|
||||
|
||||
// 5. NO guardar en cada cambio
|
||||
// func onChange() {
|
||||
// saveToDisk() // ❌ Demasiado frecuente
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Soporte
|
||||
|
||||
Si encuentras problemas:
|
||||
|
||||
1. Revisa `OPTIMIZATION_SUMMARY.md` para detalles técnicos
|
||||
2. Usa los tests de ejemplo como guía
|
||||
3. Habilita logging debug en development
|
||||
4. Usa Instruments para perfilado
|
||||
|
||||
---
|
||||
|
||||
**Happy Optimizing! 🚀**
|
||||
Reference in New Issue
Block a user