Communitygithub.com

SwiftMesh

Una referencia completa de IA para usar SwiftMesh: un envoltorio de Alamofire + Codable con async/await, Combine, configuración fluida, carga/descarga de archivos, análisis de rutas de claves JSON, envoltorios Codable resilientes y registro integrado.

¿Qué es SwiftMesh?

SwiftMesh es una habilidad de IA para desarrolladores de iOS que necesitan una capa de red Swift potente y flexible. Construido sobre Alamofire y Codable, ofrece async/await, Combine, un patrón de constructor fluido, carga y descarga de archivos, extracción de JSON anidado mediante rutas de claves, envoltorios Codable resilientes (@Default, @IgnoreError, @ConvertTo*) y registro de red. Es compatible con plataformas iOS/macOS donde se usa Swift y Alamofire. La habilidad simplifica tareas comunes de red: desde solicitudes GET/POST básicas hasta subidas multipartes y descargas reanudables, reduciendo el código repetitivo y mejorando la robustez frente a APIs inconsistentes.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add zjinhu/SwiftMesh

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Habilidad de IA SwiftMesh

Una referencia integral de IA para usar SwiftMesh: un envoltorio de Alamofire + Codable con async/await, Combine, configuración fluida, carga/descarga de archivos, análisis de rutas de claves JSON, envoltorios Codable resilientes y registro integrado.


Tarjeta de Referencia Rápida

CaracterísticaMétodoDescripción
Solicitud GET.request(of: Model.self)Decodificar respuesta a modelo Codable
Ruta de clave.request(of: Model.self, modelKeyPath: "data.user")Extraer JSON anidado
Subida de archivo.upload(of: Result.self)Subir archivo/Datos/flujo/multipart
Descarga de archivo.download()Descargar o reanudar descarga
Datos sin procesar.requestData()Obtener datos de respuesta sin procesar
Cadena sin procesar.requestString()Obtener respuesta como cadena
Política de reintento.setInterceptor(RetryPolicy())Reintento automático con retroceso
RegistroMesh.enableLog()Habilitar registro de red

Arquitectura Principal

SwiftMesh usa un patrón de constructor en la clase Mesh. Cada método de configuración devuelve Self, permitiendo encadenamiento fluido. El flujo es:

Configurar (Mesh + Config) → Ejecutar (Solicitud/Subida/Descarga) → Manejar (Handle)

Estructura de Archivos

ArchivoPropósito
Mesh.swiftClase constructora principal con todas las propiedades
Config.swiftSetters encadenables fluidos + configuración global
Request.swiftEjecución de solicitud async/await
Handle.swiftConstrucción de URL, manejo de errores, procesamiento de respuestas, RetryPolicy
Upload.swiftSubida de archivos (archivo, datos, flujo, multipart)
Download.swiftDescarga de archivos (estándar, reanudable)
KeyPath.swiftDecodificador de rutas de claves JSON para extracción anidada
Codable+.swiftEnvoltorios de propiedades resilientes (@Default, @IgnoreError, @ConvertTo*)
Log.swiftRegistrador de red (cURL, estado, tiempo, JSON)

Patrones de Uso

1. Solicitud GET Básica

let result = try await Mesh()
    .setRequestMethod(.get)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/weather/city/101030100")
    .request(of: Weather.self)

2. GET con Extracción de Ruta de Clave JSON

Extraer solo una parte anidada de la respuesta JSON sin analizar toda la estructura:

// JSON: { "code": 200, "data": { "yesterday": { "temp": 25, "notice": "Sunny" } } }
let yesterday = try await Mesh()
    .setRequestMethod(.get)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/weather")
    .request(of: Forecast.self, modelKeyPath: "data.yesterday")

3. Solicitud POST con Parámetros

let result = try await Mesh()
    .setRequestMethod(.post)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/login")
    .setParameters(["username": "admin", "password": "123456"])
    .request(of: LoginResult.self)

4. POST con Codificación JSON

let result = try await Mesh()
    .setRequestMethod(.post)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/api/data")
    .setRequestEncoding(JSONEncoding.default)
    .setParameters(["key": "value"])
    .request(of: Response.self)

5. Solicitud con Encabezados Personalizados

let result = try await Mesh()
    .setUrlHost("https://api.example.com")
    .setUrlPath("/secure/data")
    .setHeads(["Authorization": "Bearer token123"])
    .request(of: SecureData.self)

6. Solicitud con Política de Reintento

let result = try await Mesh()
    .setUrlHost("https://api.example.com")
    .setUrlPath("/unstable-api")
    .setInterceptor(RetryPolicy(maxRetryCount: 3))
    .request(of: Data.self)

7. Solicitud Basada en URLRequest

Cuando ya tienes un URLRequestConvertible:

let urlRequest = try URLRequest(url: URL(string: "https://api.example.com/data")!, method: .get)
let result = try await Mesh()
    .urlRequest(urlRequest, type: Response.self)

8. Respuesta de Datos Sin Procesar

let data = try await Mesh()
    .setRequestMethod(.get)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/raw")
    .requestData()

9. Respuesta de Cadena Sin Procesar

let string = try await Mesh()
    .setRequestMethod(.get)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/text")
    .requestString()

10. Descarga de Archivo

let fileURL = try await Mesh()
    .setRequestMethod(.get)
    .setUrlHost("https://example.com")
    .setUrlPath("/files/document.pdf")
    .setDestination { _, _ in
        let dest = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("document.pdf")
        return (dest, [.removePreviousFile, .createIntermediateDirectories])
    }
    .download()

11. Descarga Reanudable

let fileURL = try await Mesh()
    .setUrlHost("https://example.com")
    .setUrlPath("/files/large-file.zip")
    .setDownloadType(.resume)
    .setResumeData(savedResumeData)  // de descarga interrumpida anterior
    .download()

12. Subida de Archivo Único (URL)

let result = try await Mesh()
    .setRequestMethod(.post)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/upload")
    .setUploadType(.file)
    .setFileURL(fileURL)
    .upload(of: UploadResult.self)

13. Subida de Archivo Único (Datos)

let result = try await Mesh()
    .setRequestMethod(.post)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/upload")
    .setUploadType(.data)
    .setFileData(imageData)
    .upload(of: UploadResult.self)

14. Subida de Formulario Multipart

let result = try await Mesh()
    .setRequestMethod(.post)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/upload/multi")
    .setUploadType(.multipart)
    .setAddformData(name: "file",
                    fileName: "photo.jpg",
                    fileData: imageData,
                    mimeType: "image/jpeg")
    .setAddformData(name: "description",
                    fileData: "My photo".data(using: .utf8))
    .upload(of: UploadResult.self)

15. Multipart con UploadDatas Preconstruidos

let uploads = [
    MultipleUpload.formData(name: "file1", fileName: "a.jpg", fileData: data1, mimeType: "image/jpeg"),
    MultipleUpload.formData(name: "file2", fileName: "b.pdf", fileURL: fileURL)
]

let result = try await Mesh()
    .setRequestMethod(.post)
    .setUrlHost("https://api.example.com")
    .setUrlPath("/upload/batch")
    .setUploadType(.multipart)
    .setUploadDatas(uploads)
    .setParameters(["userId": "123"])  // campos de formulario adicionales
    .upload(of: BatchResult.self)

16. Tiempo de Espera Personalizado

let result = try await Mesh()
    .setUrlHost("https://api.example.com")
    .setUrlPath("/slow-api")
    .setTimeout(60)  // 60 segundos
    .request(of: Response.self)

Configuración Global

Establece estos una vez al iniciar la aplicación (por ejemplo, en AppDelegate):

// Habilitar registro de red
Mesh.enableLog(.log)  // o .print

// Establecer encabezados globales predeterminados
Mesh.setHeaders(["Authorization": "Bearer token", "App-Version": "1.0"])

// Establecer parámetros globales predeterminados
Mesh.setParameters(["platform": "ios", "sdk_version": "2.0"])

// Establecer host URL global
Mesh.setUrlHost("https://api.example.com")

Luego, la configuración por solicitud solo necesita la ruta:

// Usa encabezados, parámetros y urlHost globales
let result = try await Mesh()
    .setUrlPath("/weather")
    .request(of: Weather.self)

Envoltorios de Propiedades Codable Resilientes

Maneja respuestas de API inconsistentes sin fallos de decodificación.

Envoltorios @Default

Proporciona valores de respaldo cuando faltan campos o son inválidos:

struct Response: Codable {
    @Default.True var isEnabled: Bool              // Falta → true
    @Default.False var isDeleted: Bool             // Falta → false
    @Default.EmptyString var name: String          // Falta → ""
    @Default.EmptyInt var count: Int               // Falta → 0
    @Default.EmptyArray var tags: [String]         // Falta → []
    @Default.EmptyDictionary var meta: [String: Int] // Falta → [:]
    @Default.Now var createdAt: Date               // Falta → Date()
}

@IgnoreError

Devuelve nil en lugar de lanzar una excepción cuando un campo no se puede decodificar:

struct Response: Codable {
    @IgnoreError var description: String?   // Tipo inválido → nil (sin colapso)
    @IgnoreError var nested: NestedModel?   // Faltante/malformado → nil
}

@ConvertToString

Acepta String, Int o Double de JSON → convierte a String?:

struct Response: Codable {
    @ConvertToString var version: String?  // "1.0", 1 o 1.0 → "1.0", "1", "1.0"
}

@ConvertToInt

Acepta Int, String o Double de JSON → convierte a Int?:

struct Response: Codable {
    @ConvertToInt var count: Int?  // 42, "42" o 42.9 → 42, 42, 42
}

@ConvertToDouble

Acepta Double, Int, Float o String de JSON → convierte a Double?:

struct Response: Codable {
    @ConvertToDouble var price: Double?  // 9.99, 10, "9.99" → 9.99, 10.0, 9.99
}

@ConvertToFloat

Acepta Float, Int, Double o String de JSON → convierte a Float?:

struct Response: Codable {
    @ConvertToFloat var rating: Float?  // 4.5, 5, "4.5" → 4.5, 5.0, 4.5
}

Decodificador de Ruta de Clave JSON

Extrae valores JSON anidados sin analizar toda la respuesta:

// JSON: { "code": 200, "data": { "list": [{ "id": 1 }, { "id": 2 }] } }

// Extraer un solo objeto anidado
let firstItem = try await Mesh()
    .setRequestMethod(.get)
    .setUrlPath("/items")
    .request(of: Item.self, modelKeyPath: "data.list.0")

// El decodificador KeyPath también funciona directamente:
let decoder = JSONDecoder.default
let item = try decoder.decode(Item.self, from: jsonData, keyPath: "data.list.0")
let items = try decoder.decodeArray([Item].self, from: jsonData, keyPath: "data.list")

El decodificador predeterminado se autoconfigura:

  • keyDecodingStrategy = .convertFromSnakeCase (snake_case → camelCase)
  • dateDecodingStrategy = .iso8601

Integración con Combine + SwiftUI

Patrón ObservableObject

class RequestModel: ObservableObject {
    @MainActor @Published var weather: Weather?
    @MainActor @Published var errorMessage: String?
    
    func fetchWeather() {
        Task {
            do {
                let result = try await Mesh()
                    .setRequestMethod(.get)
                    .setUrlHost("https://api.example.com")
                    .setUrlPath("/weather")
                    .request(of: Weather.self)
                
                await MainActor.run { self.weather = result }
            } catch {
                await MainActor.run { self.errorMessage = error.localizedDescription }
            }
        }
    }
}

Uso en SwiftUI

struct WeatherView: View {
    @StateObject private var model = RequestModel()
    
    var body: some View {
        VStack {
            if let weather = model.weather {
                Text("\(weather.temperature)°C")
            }
            if let error = model.errorMessage {
                Text(error).foregroundColor(.red)
            }
        }
        .onAppear { model.fetchWeather() }
    }
}

Uso en UIKit + Combine

class ViewController: UIViewController {
    private var model = RequestModel()
    private var cancellables: Set<AnyCancellable> = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        model.$weather
            .receive(on: RunLoop.main)
            .sink { weather in
                print("Clima: \(String(describing: weather))")
            }
            .store(in: &cancellables)
        
        model.fetchWeather()
    }
}

Manejo de Errores

SwiftMesh normaliza errores de red comunes en NSError amigable para el usuario:

Código de ErrorDescripción
NSURLErrorNotConnectedToInternetSin conexión a internet
NSURLErrorTimedOutTiempo de espera de solicitud agotado
NSURLErrorCannotFindHostHost no encontrado
NSURLErrorCannotConnectToHostNo se puede conectar al host
NSURLErrorNetworkConnectionLostConexión perdida durante la solicitud

Todos los errores normalizados devuelven el mensaje: "No se puede conectar al servidor"

Captura errores en tu código async:

do {
    let result = try await Mesh()
        .setUrlPath("/api")
        .request(of: Response.self)
} catch let error as NSError {
    print("Error \(error.code): \(error.localizedDescription)")
}

RetryPolicy

Reintento integrado con retroceso lineal (1s, 2s, 3s...):

// Predeterminado: 3 reintentos
let policy = RetryPolicy()

// Personalizado: 5 reintentos
let policy = RetryPolicy(maxRetryCount: 5)

// Usar con solicitud
let result = try await Mesh()
    .setUrlPath("/unstable-api")
    .setInterceptor(policy)
    .request(of: Response.self)

Registro

Habilitar al iniciar la aplicación:

// Usando el registrador unificado de Apple (os.log)
Mesh.enableLog(.log)

// Usando Swift print()
Mesh.enableLog(.print)

La salida incluye:

  • Comando cURL (para reproducción fácil)
  • Código de estado HTTP
  • Tiempo transcurrido
  • Respuesta JSON formateada

Referencia de Métodos de Configuración

Todos los métodos son encadenables (devuelven Self):

Configuración Básica

MétodoTipoDescripción
.setTimeout(_:)TimeIntervalTiempo de espera de solicitud en segundos (predeterminado: 15)
.setInterceptor(_:)RequestInterceptor?Política de reintento / manejador de credenciales
.setRequestMethod(_:)HTTPMethodGET, POST, PUT, DELETE, etc.
.setHeads(_:)[String: String]Encabezados por solicitud
.setRequestEncoding(_:)ParameterEncodingURLEncoding, JSONEncoding, etc.
.setUrlHost(_:)String?Host URL por solicitud
.setUrlPath(_:)String?Ruta URL
.setParameters(_:)[String: Any]?Parámetros de solicitud

Configuración de Descarga

MétodoTipoDescripción
.setDownloadType(_:)DownloadType.download o .resume

(Nota: El documento original se trunca aquí; se ha traducido todo el contenido disponible.)

Skills relacionados