Communitygithub.com

SwiftMesh

Un emballage complet autour d'Alamofire et Codable en Swift avec prise en charge d'async/await, Combine, configuration fluide, téléchargement de fichiers, analyse de chemins clés JSON, wrappers Codable résilients et journalisation intégrée.

Qu'est-ce que SwiftMesh ?

SwiftMesh est une compétence d'agent IA conçue pour les développeurs Swift, fournissant un emballage haut niveau autour d'Alamofire et Codable qui simplifie les requêtes réseau, les téléchargements de fichiers, et la gestion des réponses JSON. Elle prend en charge les flux de travail modernes async/await et Combine, permettant une intégration transparente avec les architectures SwiftUI et UIKit. La compétence offre un modèle de construction fluide via la classe `Mesh`, permettant une configuration en chaîne pour les méthodes HTTP, les en-têtes personnalisés, les politiques de reprise et les formats de réponse, y compris l'analyse de chemins clés JSON pour extraire des données imbriquées. De plus, elle inclut des wrappers de propriété Codable résilients (`@Default`, `@IgnoreError`, `@ConvertToString`) pour gérer les réponses API incohérentes sans erreurs, et une journalisation intégrée pour les requêtes cURL, les codes d'état et les métriques de temps. Cette compétence est particulièrement utile pour les agents IA qui doivent automatiser des tests réseau, générer du code client, ou fournir des solutions de débogage pour des API instables dans des projets Swift sur les plateformes Apple.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Compétence IA SwiftMesh

Une référence IA complète pour utiliser SwiftMesh — un emballage autour d'Alamofire + Codable avec async/await, Combine, configuration fluide, téléchargement de fichiers, analyse de chemins clés JSON, wrappers Codable résilients et journalisation intégrée.


Fiche de Référence Rapide

FonctionnalitéMéthodeDescription
Requête GET.request(of: Model.self)Décoder la réponse en modèle Codable
Chemin clé.request(of: Model.self, modelKeyPath: "data.user")Extraire du JSON imbriqué
Téléchargement de fichier.upload(of: Result.self)Télécharger un fichier/Data/flux/multipart
Téléchargement de fichier.download()Télécharger ou reprendre un téléchargement
Données brutes.requestData()Obtenir les données brutes de la réponse
Chaîne brute.requestString()Obtenir la réponse sous forme de chaîne
Politique de reprise.setInterceptor(RetryPolicy())Reprise automatique avec backoff
JournalisationMesh.enableLog()Activer la journalisation réseau

Architecture de Base

SwiftMesh utilise un modèle de construction sur la classe Mesh. Chaque méthode de configuration retourne Self, permettant un enchaînement fluide. Le flux est :

Configurer (Mesh + Config) → Exécuter (Requête/Téléchargement/Téléchargement de fichier) → Gérer (Handle)

Structure des Fichiers

FichierObjectif
Mesh.swiftClasse de construction de base avec toutes les propriétés
Config.swiftSetters fluides enchaînables + configuration globale
Request.swiftExécution de requête async/await
Handle.swiftConstruction d'URL, gestion d'erreurs, traitement de réponse, RetryPolicy
Upload.swiftTéléchargement de fichier (fichier, données, flux, multipart)
Download.swiftTéléchargement de fichier (standard, reprenable)
KeyPath.swiftDécodeur de chemin clé JSON pour extraction imbriquée
Codable+.swiftWrappers de propriété résilients (@Default, @IgnoreError, @ConvertTo*)
Log.swiftJournaliseur réseau (cURL, statut, timing, JSON)

Modèles d'Utilisation

1. Requête GET de Base

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

2. GET avec Extraction de Chemin Clé JSON

Extraire uniquement une partie imbriquée de la réponse JSON sans analyser toute la structure :

// 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. Requête POST avec Paramètres

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 avec Encodage 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. Requête avec En-têtes Personnalisés

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

6. Requête avec Politique de Reprise

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

7. Requête Basée sur URLRequest

Lorsque vous avez déjà 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. Réponse de Données Brutes

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

9. Réponse de Chaîne Brute

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

10. Téléchargement de Fichier

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. Téléchargement Reprenable

let fileURL = try await Mesh()
    .setUrlHost("https://example.com")
    .setUrlPath("/files/large-file.zip")
    .setDownloadType(.resume)
    .setResumeData(savedResumeData)  // à partir d'un téléchargement interrompu précédent
    .download()

12. Téléchargement de Fichier Unique (URL)

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

13. Téléchargement de Fichier Unique (Données)

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

14. Téléchargement de Formulaire 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 avec UploadDatas Pré-construites

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"])  // champs de formulaire supplémentaires
    .upload(of: BatchResult.self)

16. Délai d'Attente Personnalisé

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

Configuration Globale

Définissez ces valeurs une fois au lancement de l'application (par exemple, dans AppDelegate) :

// Activer la journalisation réseau
Mesh.enableLog(.log)  // ou .print

// Définir les en-têtes globaux par défaut
Mesh.setHeaders(["Authorization": "Bearer token", "App-Version": "1.0"])

// Définir les paramètres globaux par défaut
Mesh.setParameters(["platform": "ios", "sdk_version": "2.0"])

// Définir l'hôte URL global
Mesh.setUrlHost("https://api.example.com")

Ensuite, la configuration par requête ne nécessite que le chemin :

// Utilise les en-têtes, paramètres et urlHost globaux
let result = try await Mesh()
    .setUrlPath("/weather")
    .request(of: Weather.self)

Wrappers de Propriété Codable Résilients

Gérer les réponses API incohérentes avec élégance sans échecs de décodage.

Wrappers @Default

Fournir des valeurs de repli lorsque des champs sont manquants ou invalides :

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

@IgnoreError

Retourne nil au lieu de lever une exception lorsqu'un champ ne peut pas être décodé :

struct Response: Codable {
    @IgnoreError var description: String?   // Type invalide → nil (pas de plantage)
    @IgnoreError var nested: NestedModel?   // Manquant/malformé → nil
}

@ConvertToString

Accepte les chaînes, Int ou Double du JSON → convertit en String? :

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

@ConvertToInt

Accepte Int, String ou Double du JSON → convertit en Int? :

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

@ConvertToDouble

Accepte Double, Int, Float ou String du JSON → convertit en Double? :

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

@ConvertToFloat

Accepte Float, Int, Double ou String du JSON → convertit en Float? :

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

Décodeur de Chemin Clé JSON

Extraire des valeurs JSON imbriquées sans analyser toute la réponse :

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

// Extraire un seul objet imbriqué
let firstItem = try await Mesh()
    .setRequestMethod(.get)
    .setUrlPath("/items")
    .request(of: Item.self, modelKeyPath: "data.list.0")

// Le décodeur KeyPath fonctionne également directement :
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")

Le décodeur par défaut se configure automatiquement :

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

Intégration Combine + SwiftUI

Modèle 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 }
            }
        }
    }
}

Utilisation avec 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() }
    }
}

Utilisation avec 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("Weather: \(String(describing: weather))")
            }
            .store(in: &cancellables)
        
        model.fetchWeather()
    }
}

Gestion des Erreurs

SwiftMesh normalise les erreurs réseau courantes en NSError convivial :

Code d'ErreurDescription
NSURLErrorNotConnectedToInternetPas de connexion Internet
NSURLErrorTimedOutDélai d'attente de la requête dépassé
NSURLErrorCannotFindHostHôte introuvable
NSURLErrorCannotConnectToHostImpossible de se connecter à l'hôte
NSURLErrorNetworkConnectionLostConnexion perdue pendant la requête

Toutes les erreurs normalisées retournent le message : "Impossible de se connecter au serveur"

Attraper les erreurs dans votre code asynchrone :

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

RetryPolicy (Politique de Reprise)

Reprise intégrée avec backoff linéaire (1s, 2s, 3s...) :

// Par défaut : 3 reprises
let policy = RetryPolicy()

// Personnalisé : 5 reprises
let policy = RetryPolicy(maxRetryCount: 5)

// Utilisation avec une requête
let result = try await Mesh()
    .setUrlPath("/unstable-api")
    .setInterceptor(policy)
    .request(of: Response.self)

Journalisation

Activer au lancement de l'application :

// Utilisation du journaliseur unifié d'Apple (os.log)
Mesh.enableLog(.log)

// Utilisation de print() Swift
Mesh.enableLog(.print)

La sortie inclut :

  • Commande cURL (pour reproduction facile)
  • Code d'état HTTP
  • Temps écoulé
  • Réponse JSON joliment formatée

Référence des Méthodes de Configuration

Toutes les méthodes sont enchaînables (retournent Self) :

Configuration de Base

MéthodeTypeDescription
.setTimeout(_:)TimeIntervalDélai d'attente de la requête en secondes (par défaut : 15)
.setInterceptor(_:)RequestInterceptor?Politique de reprise / gestionnaire d'identifiants
.setRequestMethod(_:)HTTPMethodGET, POST, PUT, DELETE, etc.
.setHeads(_:)[String: String]En-têtes par requête
.setRequestEncoding(_:)ParameterEncodingURLEncoding, JSONEncoding, etc.
.setUrlHost(_:)String?Hôte URL par requête
.setUrlPath(_:)String?Chemin URL
.setParameters(_:)[String: Any]?Paramètres de la requête

Configuration du Téléchargement

MéthodeTypeDescription
.setDownloadType(_:)DownloadType.download ou .resume
.setResumeData(_:)Data?Données de reprise pour téléchargements reprenables
.setDestination(_:)DownloadRequest.Destination?Chemin de destination personnalisé

Configuration du Téléchargement de Fichier

MéthodeTypeDescription
.setUploadType(_:)UploadType.file, .data, .stream, .multipart
.setFileURL(_:)URL?URL du fichier à télécharger
.setFileData(_:)Data?Données à télécharger
.setUploadDatas(_:)[MultipleUpload]?Tableau de données de téléchargement multipart
.setAddformData(name:fileName:fileData:mimeType:)-Ajouter un champ de formulaire multipart

Dépendances

SwiftMesh nécessite :

Alamofire ~> 5.0

Inclure dans votre Package.swift :

dependencies: [
    .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"),
    .package(url: "https://github.com/zjinhu/SwiftMesh.git", branch: "main")
]

Considérations de Performance

  • Utilisez modelKeyPath pour les structures JSON volumineuses afin d'éviter une surcharge de décodage inutile
  • Les wrappers @Default et @IgnoreError ont un coût de performance minime
  • La journalisation peut être désactivée en production avec Mesh.enableLog(.none)
  • Politique de reprise avec backoff linéaire pour éviter les surcharges du serveur
  • Les téléchargements reprenables reprennent à partir de l'octet interrompu, économisant la bande passante

Dépannage

ProblèmeSolution
Échec du décodageVérifiez que le modèle est conforme à Codable ; utilisez modelKeyPath ou des wrappers résilients
Timeout des requêtesAugmentez le délai d'attente ou vérifiez l'état du réseau
Échec du téléchargementVérifiez le type MIME et la taille du fichier ; activez la journalisation pour diagnostiquer
Erreur de compilation (Alamofire)Assurez-vous que la version d'Alamofire est ~> 5.0
Journalisation non activéeAppelez Mesh.enableLog(.log) ou .print avant d'effectuer des requêtes
Échec du téléchargement reprenableAssurez-vous que le serveur prend en charge les téléchargements partiels (en-tête Range)

Pour plus de détails, consultez le dépôt GitHub SwiftMesh et la documentation Alamofire.

Skills associés