SwiftMesh : wrapper Alamofire + Codable

Référence complète pour SwiftMesh, un wrapper Alamofire avec Codable, async/await, Combine, téléchargement, et logging intégré.

Spar Skills Guide Bot
DeveloppementIntermédiaire
1027/07/2026
Claude CodeCursorWindsurfCopilotCodex
#swiftmesh#alamofire#codable#async-await#networking

Recommandé pour

SwiftMesh AI Skill

A comprehensive AI reference for using SwiftMesh — an Alamofire + Codable wrapper with async/await, Combine, fluent configuration, file upload/download, JSON key path parsing, resilient Codable wrappers, and built-in logging.


Quick Reference Card

| Feature | Method | Description | |---------|--------|-------------| | GET Request | .request(of: Model.self) | Decode response to Codable model | | Key Path | .request(of: Model.self, modelKeyPath: "data.user") | Extract nested JSON | | File Upload | .upload(of: Result.self) | Upload file/Data/stream/multipart | | File Download | .download() | Download or resume download | | Raw Data | .requestData() | Get raw response Data | | Raw String | .requestString() | Get response as String | | Retry Policy | .setInterceptor(RetryPolicy()) | Auto-retry with backoff | | Logging | Mesh.enableLog() | Enable network logging |


Core Architecture

SwiftMesh uses a builder pattern on the Mesh class. Every configuration method returns Self, enabling fluent chaining. The flow is:

Configure (Mesh + Config) → Execute (Request/Upload/Download) → Handle (Handle)

File Structure

| File | Purpose | |------|---------| | Mesh.swift | Core builder class with all properties | | Config.swift | Fluent chainable setters + global config | | Request.swift | async/await request execution | | Handle.swift | URL construction, error handling, response processing, RetryPolicy | | Upload.swift | File upload (file, data, stream, multipart) | | Download.swift | File download (standard, resumable) | | KeyPath.swift | JSON key path decoder for nested extraction | | Codable+.swift | Resilient property wrappers (@Default, @IgnoreError, @ConvertTo*) | | Log.swift | Network logger (cURL, status, timing, JSON) |


Usage Patterns

1. Basic GET Request

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

2. GET with JSON Key Path Extraction

Extract only a nested portion of the JSON response without parsing the entire 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. POST Request with Parameters

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 with JSON Encoding

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. Request with Custom Headers

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

6. Request with Retry Policy

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

7. URLRequest-based Request

When you already have a 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. Raw Data Response

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

9. Raw String Response

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

10. File Download

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. Resumable Download

let fileURL = try await Mesh()
    .setUrlHost("https://example.com")
    .setUrlPath("/files/large-file.zip")
    .setDownloadType(.resume)
    .setResumeData(savedResumeData)  // from previous interrupted download
    .download()

12. Single File Upload (URL)

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

13. Single File Upload (Data)

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

14. Multipart Form Upload

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 with Pre-built UploadDatas

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"])  // additional form fields
    .upload(of: BatchResult.self)

16. Custom Timeout

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

Global Configuration

Set these once at app launch (e.g., in AppDelegate):

// Enable network logging
Mesh.enableLog(.log)  // or .print

// Set global default headers
Mesh.setHeaders(["Authorization": "Bearer token", "App-Version": "1.0"])

// Set global default parameters
Mesh.setParameters(["platform": "ios", "sdk_version": "2.0"])

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

Then per-request configuration only needs the path:

// Uses global headers, parameters, and urlHost
let result = try await Mesh()
    .setUrlPath("/weather")
    .request(of: Weather.self)

Resilient Codable Property Wrappers

Handle inconsistent API responses gracefully without decoding failures.

@Default Wrappers

Provide fallback values when fields are missing or invalid:

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

@IgnoreError

Returns nil instead of throwing when a field fails to decode:

struct Response: Codable {
    @IgnoreError var description: String?   // Invalid type → nil (no crash)
    @IgnoreError var nested: NestedModel?   // Missing/malformed → nil
}

@ConvertToString

Accepts String, Int, or Double from JSON → converts to String?:

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

@ConvertToInt

Accepts Int, String, or Double from JSON → converts to Int?:

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

@ConvertToDouble

Accepts Double, Int, Float, or String from JSON → converts to Double?:

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

@ConvertToFloat

Accepts Float, Int, Double, or String from JSON → converts to Float?:

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

JSON Key Path Decoder

Extract nested JSON values without parsing the entire response:

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

// Extract a single nested object
let firstItem = try await Mesh()
    .setRequestMethod(.get)
    .setUrlPath("/items")
    .request(of: Item.self, modelKeyPath: "data.list.0")

// The KeyPath decoder also works directly:
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")

The default decoder auto-configures:

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

Combine + SwiftUI Integration

ObservableObject Pattern

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 }
            }
        }
    }
}

SwiftUI Usage

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

UIKit + Combine Usage

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

Error Handling

SwiftMesh normalizes common network errors into user-friendly NSError:

| Error Code | Description | |------------|-------------| | NSURLErrorNotConnectedToInternet | No internet connection | | NSURLErrorTimedOut | Request timeout | | NSURLErrorCannotFindHost | Host not found | | NSURLErrorCannotConnectToHost | Cannot connect to host | | NSURLErrorNetworkConnectionLost | Connection lost during request |

All normalized errors return the message: "Unable to connect to the server"

Catch errors in your async code:

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

RetryPolicy

Built-in retry with linear backoff (1s, 2s, 3s...):

// Default: 3 retries
let policy = RetryPolicy()

// Custom: 5 retries
let policy = RetryPolicy(maxRetryCount: 5)

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

Logging

Enable at app launch:

// Using Apple's unified Logger (os.log)
Mesh.enableLog(.log)

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

Output includes:

  • cURL command (for easy reproduction)
  • HTTP status code
  • Elapsed time
  • Pretty-printed JSON response

Configuration Method Reference

All methods are chainable (return Self):

Basic Configuration

| Method | Type | Description | |--------|------|-------------| | .setTimeout(_:) | TimeInterval | Request timeout in seconds (default: 15) | | .setInterceptor(_:) | RequestInterceptor? | Retry policy / credential handler | | .setRequestMethod(_:) | HTTPMethod | GET, POST, PUT, DELETE, etc. | | .setHeads(_:) | [String: String] | Per-request headers | | .setRequestEncoding(_:) | ParameterEncoding | URLEncoding, JSONEncoding, etc. | | .setUrlHost(_:) | String? | Per-request URL host | | .setUrlPath(_:) | String? | URL path | | .setParameters(_:) | [String: Any]? | Request parameters |

Download Configuration

| Method | Type | Description | |--------|------|-------------| | .setDownloadType(_:) | DownloadType | .download or .resume | | .setDestination(_:) | DownloadRequest.Destination | File save location | | .setResumeData(_:) | Data? | Resume data from interrupted download |

Upload Configuration

| Method | Type | Description | |--------|------|-------------| | .setUploadType(_:) | UploadType | .file, .data, .stream, .multipart | | .setFileURL(_:) | URL? | File URL for upload | | .setFileData(_:) | Data? | File data for upload | | .setStream(_:) | InputStream? | Input stream for upload | | .setUploadDatas(_:) | [MultipleUpload] | Multipart form entries | | .setAddformData(name:fileName:fileData:fileURL:mimeType:) | Multiple | Quick add form field |

Global Static Methods

| Method | Description | |--------|-------------| | Mesh.enableLog(_:) | Enable network logging | | Mesh.setHeaders(_:) | Set global default headers | | Mesh.setParameters(_:) | Set global default parameters | | Mesh.setUrlHost(_:) | Set global default URL host |


Enums Reference

DownloadType

| Case | Description | |------|-------------| | .download | Standard download | | .resume | Resumable download (requires resumeData) |

UploadType

| Case | Description | |------|-------------| | .file | Upload from file URL | | .data | Upload from Data object | | .stream | Upload from InputStream | | .multipart | Multipart form upload |

LogType

| Case | Description | |------|-------------| | .print | Output via Swift print() | | .log | Output via Apple os.log |


AI Prompt Templates

When using an AI coding assistant, use these templates:

"Make a GET request"

Use SwiftMesh to make a GET request to {URL} and decode the response into a {Model} struct. Use key path "{keyPath}" to extract nested data.

"Upload a file"

Use SwiftMesh to upload a {file/data/stream} to {URL} with multipart form field name "{name}" and MIME type "{mimeType}". Decode the response as {Model}.

"Download a file"

Use SwiftMesh to download a file from {URL} and save it to {destination}. Use resumable download if needed.

"Handle inconsistent API types"

Create a Codable struct for this JSON: {json}. Use @Default, @IgnoreError, and @ConvertTo* wrappers to handle missing or inconsistent types.
Skills similaires