✨ 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>
736 lines
19 KiB
Markdown
736 lines
19 KiB
Markdown
# 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](#configuración-del-entorno-de-desarrollo)
|
|
- [Cómo Agregar Nuevas Fuentes de Manga](#cómo-agregar-nuevas-fuentes-de-manga)
|
|
- [Cómo Modificar el Scraper](#cómo-modificar-el-scraper)
|
|
- [Estándares de Código](#estándares-de-código)
|
|
- [Testing](#testing)
|
|
- [Pull Request Guidelines](#pull-request-guidelines)
|
|
- [Troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## Configuración del Entorno de Desarrollo
|
|
|
|
### Requisitos Previos
|
|
|
|
1. **MacOS** con Xcode 15+
|
|
2. **iOS 15+** device o simulator
|
|
3. **Git** para control de versiones
|
|
4. **Cuenta de Developer** de Apple (opcional, para firmar)
|
|
|
|
### Pasos Iniciales
|
|
|
|
1. **Fork el repositorio** (si es proyecto open source)
|
|
2. **Clona tu fork**:
|
|
```bash
|
|
git clone https://github.com/tu-usuario/MangaReader.git
|
|
cd MangaReader/ios-app
|
|
```
|
|
|
|
3. **Abre el proyecto en Xcode**:
|
|
```bash
|
|
open MangaReader.xcodeproj
|
|
```
|
|
|
|
4. **Configura el signing**:
|
|
- Selecciona el proyecto en el sidebar
|
|
- En "Signing & Capabilities", elige tu Team
|
|
- Asegúrate que "Automatically manage signing" esté activado
|
|
|
|
5. **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:
|
|
|
|
```swift
|
|
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`**
|
|
|
|
```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`:
|
|
|
|
```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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
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
|
|
|
|
1. Abre manhwaweb.com en Safari/Chrome
|
|
2. Abre Web Inspector (F12 o Cmd+Option+I)
|
|
3. Navega a un manga/capítulo
|
|
4. 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`**
|
|
|
|
```swift
|
|
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
|
|
|
|
1. Compila y ejecuta la app
|
|
2. Intenta agregar un manga existente
|
|
3. Verifica que la información se muestre correctamente
|
|
4. Intenta leer un capítulo
|
|
5. Verifica que las imágenes carguen
|
|
|
|
### Paso 5: Manejo de Errores
|
|
|
|
Agrega manejo de errores robusto:
|
|
|
|
```swift
|
|
// 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
|
|
|
|
1. **Incrementa el tiempo de espera** si la página carga lento:
|
|
```swift
|
|
try await loadURLAndWait(url, waitForImages: true)
|
|
// Aumenta el tiempo en loadURLAndWait
|
|
```
|
|
|
|
2. **Verifica que JavaScript esté habilitado** en WKWebView
|
|
|
|
3. **Revisa la consola** del WebView agregando logging:
|
|
```swift
|
|
webView.evaluateJavaScript("console.log('Debug info')")
|
|
```
|
|
|
|
4. **Usa `try?`** en vez de `try!` temporalmente para evitar crashes:
|
|
```swift
|
|
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
|
|
```swift
|
|
class ManhwaWebScraper { }
|
|
struct Manga { }
|
|
```
|
|
|
|
**Propiedades y Métodos**: camelCase
|
|
```swift
|
|
var mangaTitle: String
|
|
func scrapeMangaInfo() { }
|
|
```
|
|
|
|
**Constantes Privadas**: camelCase con prefijo si es necesario
|
|
```swift
|
|
private let favoritesKey = "favoriteMangas"
|
|
```
|
|
|
|
#### 2. Organización de Código
|
|
|
|
Usa MARK comments para organizar:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
/// 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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
// 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:**
|
|
|
|
```swift
|
|
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
|
|
|
|
1. En Xcode, presiona `Cmd + U` para ejecutar todos los tests
|
|
2. Para ejecutar un test específico, click en el diamante junto al nombre del test
|
|
3. 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
|
|
|
|
1. **Actualiza tu rama**:
|
|
```bash
|
|
git checkout main
|
|
git pull upstream main
|
|
git checkout tu-rama
|
|
git rebase main
|
|
```
|
|
|
|
2. **Resuelve conflicts** si los hay
|
|
|
|
3. **Ejecuta los tests**:
|
|
```bash
|
|
xcodebuild test -scheme MangaReader -destination 'platform=iOS Simulator,name=iPhone 15'
|
|
```
|
|
|
|
4. **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
|
|
|
|
### Estructura del PR
|
|
|
|
Usa esta plantilla para tu PR:
|
|
|
|
```markdown
|
|
## 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
|
|
|
|
1. **Automated Checks**: CI ejecutará tests automáticamente
|
|
2. **Code Review**: Al menos 1 revisor debe aprobar
|
|
3. **Testing**: El revisor probará los cambios en un dispositivo
|
|
4. **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**:
|
|
1. Limpia el proyecto: `Cmd + Shift + K`
|
|
2. Cierra Xcode
|
|
3. Borra `DerivedData`: `rm -rf ~/Library/Developer/Xcode/DerivedData`
|
|
4. Abre Xcode y rebuild
|
|
|
|
### Pedir Ayuda
|
|
|
|
Si estás atascado:
|
|
|
|
1. **Busca en Issues existentes**: Puede que alguien ya tuvo el mismo problema
|
|
2. **Crea un Issue** con:
|
|
- Descripción detallada del problema
|
|
- Pasos para reproducir
|
|
- Logs relevantes
|
|
- Tu entorno (Xcode version, iOS version, macOS version)
|
|
3. **Únete a Discord/Slack** (si existe) para ayuda en tiempo real
|
|
|
|
---
|
|
|
|
## Recursos Adicionales
|
|
|
|
### Documentación de Referencia
|
|
|
|
- [Swift Language Guide](https://docs.swift.org/swift-book/)
|
|
- [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui)
|
|
- [WKWebView Reference](https://developer.apple.com/documentation/webkit/wkwebview)
|
|
- [Swift Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html)
|
|
|
|
### Herramientas Útiles
|
|
|
|
- **SwiftLint**: Linter para Swift
|
|
- **SwiftFormat**: Formateador de código
|
|
- **SwiftGen**: Generador de código para recursos
|
|
|
|
### Comunidad
|
|
|
|
- [Swift Forums](https://forums.swift.org/)
|
|
- [Stack Overflow - swift tag](https://stackoverflow.com/questions/tagged/swift)
|
|
- [r/swift subreddit](https://www.reddit.com/r/swift/)
|
|
|
|
---
|
|
|
|
**Última actualización**: Febrero 2026
|
|
**Versión**: 1.0.0
|