Communitygithub.com

zjinhu/SwiftMesh

一个基于 Alamofire 和 Codable 的 Swift 网络层封装,支持 async/await、Combine、流畅配置、文件上传下载、JSON 键路径解析、弹性 Codable 包装器和内置日志。

SwiftMesh 是什么?

SwiftMesh 是一个为 Swift 开发者设计的 AI 助手技能,它提供了一套完整的网络请求解决方案。该技能基于 SwiftMesh 库,该库围绕 Alamofire 和 Codable 构建,采用构建者模式,使得配置和发起网络请求变得流畅且直观。它支持 async/await 和 Combine,便于与现代 SwiftUI 和 UIKit 应用集成。其核心特性包括:自动解析 JSON 响应到 Codable 模型、通过键路径提取嵌套 JSON、文件上传(单文件/多部分)和下载(可恢复)、内置重试策略(线性退避)、以及可通过全局配置设置的详细日志。SwiftMesh 还通过 `@Default`、`@IgnoreError` 和 `@ConvertTo*` 等弹性属性包装器优雅地处理不一致的 API 响应,从而防止解码失败。该技能适用于任何使用 Swift 和 SwiftUI/UIKit 进行 iOS/macOS 开发的 AI 编程代理(如 Claude Code、Cursor 等),可显著提高网络层编码效率。

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

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

SwiftMesh AI 技能

一份全面的 AI 参考文档,用于使用 SwiftMesh —— 一个基于 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()获取响应 String
重试策略.setInterceptor(RetryPolicy())带退避的自动重试
日志Mesh.enableLog()启用网络日志

核心架构

SwiftMesh 在 Mesh 类上使用构建者模式。每个配置方法都返回 Self,从而实现流畅的链式调用。流程如下:

配置 (Mesh + Config) → 执行 (Request/Upload/Download) → 处理 (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, or 1.0 → "1.0", "1", "1.0"
}

@ConvertToInt

接受 JSON 中的 Int、String 或 Double → 转换为 Int?:

struct Response: Codable {
    @ConvertToInt var count: Int?  // 42, "42", or 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 (蛇形命名法 → 驼峰命名法)
  • 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 \(error.code): \(error.localizedDescription)")
}

RetryPolicy

内置线性退避重试(1s, 2s, 3s...):

// 默认: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
.setDestination(_:)DownloadRequest.Destination?自定义文件目标路径
.setResumeData(_:)Data?用于恢复下载的数据

上传配置

方法类型描述
.setUploadType(_:)UploadType.file, .data, .stream, .multipart
.setFileURL(_:)URL?要上传的文件的 URL
.setFileData(_:)Data?要上传的数据
.setStreamURL(_:)URL?要上传的流 URL
.setUploadDatas(_:)[MultipleUpload]多部分表单数据数组
.setAddformData(name:fileName:fileData:mimeType:)-添加单个表单数据项

响应处理

方法类型描述
.request(of:)泛型 T: Codable解码为 Model
.request(of:modelKeyPath:)泛型 T: Codable通过键路径解码
.requestData()异步 Data返回原始 Data
.requestString()异步 String返回原始 String
.upload(of:)泛型 T: Codable上传并解码响应
.download()异步 URL下载文件到目标位置
.urlRequest(_:type:)泛型 T: Codable基于 URLRequest 的请求

版本兼容性

SwiftMesh 版本需求
>= 1.0.0Swift 5.5+ / iOS 15+ / macOS 12+

安装

Swift Package Manager

在 Package.swift 中添加:

dependencies: [
    .package(url: "https://github.com/zjinhu/SwiftMesh.git", from: "1.0.0")
]

或者通过 Xcode 添加:

  1. 文件 → 添加包依赖...
  2. 输入仓库 URL:https://github.com/zjinhu/SwiftMesh.git

许可证

SwiftMesh 采用 MIT 许可证发布。

相关技能