Communitygithub.com

zjinhu/SwiftMesh

一個基於 Alamofire + Codable 的網路請求封裝,支援 async/await、Combine、流暢配置、檔案上傳/下載、JSON 鍵路徑解析、彈性的 Codable 包裝器與內建日誌。

SwiftMesh 是什麼?

SwiftMesh 是一個專為 Swift 開發者設計的 AI Agent 技能,它封裝了 Alamofire 的強大功能,並結合 Codable 協議,提供了一個優雅且易於使用的網路請求庫。此技能的核心亮點在於其流暢的建構者模式(Builder Pattern),允許開發者透過鏈式呼叫逐步配置請求的各個方面,包括 HTTP 方法、URL、標頭、參數、編碼方式等。它不僅支援基本的 GET 和 POST 請求,還內建了強大的文件上傳(包括單檔、資料、串流和多部分表單上傳)與下載(含斷點續傳)功能。SwiftMesh 特別處理了 JSON 鍵路徑解析,讓開發者能輕鬆提取巢狀 JSON 結構中的特定資料,而無需解析整個回應。此外,它還提供了一系列彈性的 Codable 屬性包裝器(如 @Default、@IgnoreError、@ConvertToString),有效應對後端 API 回應不一致的情況,避免解碼失敗。該技能支援在 iOS、macOS 等 Apple 平台上使用,可與 SwiftUI 和 Combine 框架無縫整合,並內建了可配置的重試策略與詳細的網路日誌功能,顯著提升了開發效率和程式碼的穩健性。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add zjinhu/SwiftMesh

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

SwiftMesh AI 技能

關於使用 SwiftMesh 的全面 AI 參考指南 — 一個 Alamofire + Codable 的包裝器,支援 async/await、Combine、流暢配置、檔案上傳/下載、JSON 鍵路徑解析、彈性的 Codable 包裝器與內建日誌。


快速參考卡

功能方法描述
GET 請求.request(of: Model.self)將回應解碼為 Codable 模型
鍵路徑.request(of: Model.self, modelKeyPath: "data.user")提取巢狀 JSON
檔案上傳.upload(of: Result.self)上傳檔案/Data/串流/多部分
檔案下載.download()下載或續傳下載
原始資料.requestData()取得原始回應 Data
原始字串.requestString()將回應以字串形式取得
重試策略.setInterceptor(RetryPolicy())自動重試並帶有回退機制
日誌Mesh.enableLog()啟用網路日誌

核心架構

SwiftMesh 在 Mesh 類別上使用建構者模式。每個配置方法都回傳 Self,從而實現流暢的鏈式呼叫。流程如下:

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

檔案結構

檔案用途
Mesh.swift包含所有屬性的核心建構者類別
Config.swift流暢的鏈式設定器 + 全域配置
Request.swiftasync/await 請求執行
Handle.swiftURL 建構、錯誤處理、回應處理、RetryPolicy
Upload.swift檔案上傳(檔案、資料、串流、多部分)
Download.swift檔案下載(標準、可續傳)
KeyPath.swift用於巢狀提取的 JSON 鍵路徑解碼器
Codable+.swift彈性的屬性包裝器(@Default, @IgnoreError, @ConvertTo*)
Log.swift網路記錄器(cURL、狀態、計時、JSON)

使用模式

1. 基本 GET 請求

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

2. 帶有 JSON 鍵路徑提取的 GET 請求

僅提取 JSON 回應的巢狀部分,而無需解析整個結構:

// 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 請求

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

4. 帶有 JSON 編碼的 POST 請求

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. 帶有自定義標頭的請求

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

6. 帶有重試策略的請求

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

7. 基於 URLRequest 的請求

當你已經有一個 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. 原始資料回應

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

9. 原始字串回應

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

10. 檔案下載

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. 可續傳下載

let fileURL = try await Mesh()
    .setUrlHost("https://example.com")
    .setUrlPath("/files/large-file.zip")
    .setDownloadType(.resume)
    .setResumeData(savedResumeData)  // 來自先前中斷的下載
    .download()

12. 單一檔案上傳 (URL)

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

13. 單一檔案上傳 (Data)

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

14. 多部分表單上傳

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. 使用預先構建的 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"])  // 額外的表單欄位
    .upload(of: BatchResult.self)

16. 自定義超時

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

全域配置

在應用程式啟動時設定一次(例如在 AppDelegate 中):

// 啟用網路日誌
Mesh.enableLog(.log)  // 或 .print

// 設定全域預設標頭
Mesh.setHeaders(["Authorization": "Bearer token", "App-Version": "1.0"])

// 設定全域預設參數
Mesh.setParameters(["platform": "ios", "sdk_version": "2.0"])

// 設定全域 URL 主機
Mesh.setUrlHost("https://api.example.com")

那麼單次請求的配置只需要路徑:

// 使用全域標頭、參數和 urlHost
let result = try await Mesh()
    .setUrlPath("/weather")
    .request(of: Weather.self)

彈性的 Codable 屬性包裝器

優雅地處理不一致的 API 回應,而不會導致解碼失敗。

@Default 包裝器

當欄位缺失或無效時提供預設值:

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

@IgnoreError

當欄位解碼失敗時回傳 nil 而不是拋出錯誤:

struct Response: Codable {
    @IgnoreError var description: String?   // 無效類型 → nil(不會崩潰)
    @IgnoreError var nested: NestedModel?   // 缺失/格式錯誤 → nil
}

@ConvertToString

從 JSON 中接收 String、Int 或 Double → 轉換為 String?:

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

@ConvertToInt

從 JSON 中接收 Int、String 或 Double → 轉換為 Int?:

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

@ConvertToDouble

從 JSON 中接收 Double、Int、Float 或 String → 轉換為 Double?:

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

@ConvertToFloat

從 JSON 中接收 Float、Int、Double 或 String → 轉換為 Float?:

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

JSON 鍵路徑解碼器

無需解析整個回應即可提取巢狀 JSON 值:

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

// 提取單個巢狀物件
let firstItem = try await Mesh()
    .setRequestMethod(.get)
    .setUrlPath("/items")
    .request(of: Item.self, modelKeyPath: "data.list.0")

// KeyPath 解碼器也可以直接使用:
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")

預設解碼器會自動配置:

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

Combine + SwiftUI 整合

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

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

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

錯誤處理

SwiftMesh 將常見的網路錯誤標準化為使用者友善的 NSError:

錯誤代碼描述
NSURLErrorNotConnectedToInternet無網路連線
NSURLErrorTimedOut請求逾時
NSURLErrorCannotFindHost找不到主機
NSURLErrorCannotConnectToHost無法連線到主機
NSURLErrorNetworkConnectionLost請求過程中連線中斷

所有標準化後的錯誤都會回傳訊息:"無法連線到伺服器"

在非同步程式碼中捕獲錯誤:

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

RetryPolicy

內建帶有線性回退的重試機制(1秒、2秒、3秒...):

// 預設:3 次重試
let policy = RetryPolicy()

// 自定義:5 次重試
let policy = RetryPolicy(maxRetryCount: 5)

// 與請求一起使用
let result = try await Mesh()
    .setUrlPath("/unstable-api")
    .setInterceptor(policy)
    .request(of: Response.self)

日誌

在應用程式啟動時啟用:

// 使用 Apple 的統一 Logger (os.log)
Mesh.enableLog(.log)

// 使用 Swift print()
Mesh.enableLog(.print)

輸出包含:

  • cURL 命令(便於重現)
  • HTTP 狀態碼
  • 耗時
  • 美化列印的 JSON 回應

配置方法參考

所有方法都是可鏈式的(回傳 Self):

基本配置

方法類型描述
.setTimeout(_:)TimeInterval請求逾時秒數(預設:15)
.setInterceptor(_:)RequestInterceptor?重試策略 / 憑證處理器
.setRequestMethod(_:)HTTPMethodGET、POST、PUT、DELETE 等
.setHeads(_:)[String: String]每次請求的標頭
.setRequestEncoding(_:)ParameterEncodingURLEncoding、JSONEncoding 等
.setUrlHost(_:)String?每次請求的 URL 主機
.setUrlPath(_:)String?URL 路徑
.setParameters(_:)[String: Any]?請求參數

下載配置

方法類型描述
.setDownloadType(_:)DownloadType.download 或 .resume

...

[為翻譯而截斷]

相關技能