Resources
13Install
npx skillscat add zjinhu/swiftmesh Install via the SkillsCat registry.
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.