一键导入
ios-api-client-foundation
MANDATORY for API client and Services work. Use before writing APIClient, any file under Services/, or any networking code.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
MANDATORY for API client and Services work. Use before writing APIClient, any file under Services/, or any networking code.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | ios-api-client-foundation |
| description | MANDATORY for API client and Services work. Use before writing APIClient, any file under Services/, or any networking code. |
APIClient.shared. Never instantiate APIClient() in a view or view model — inject the shared instance or mock it in tests.async/await throughout — no closure-based callback variants, no Combine Publisher returns. Every method is async throws.DataResponse<T>, PaginatedResponse<T>, or EmptyResponse. Raw JSON dictionaries are a code smell.JSONEncoder uses .convertToSnakeCase and JSONDecoder uses .convertFromSnakeCase. Phoenix/Rails/Django all expect snake_case on the wire; camelCase on wire is a bug.KeychainService.getToken() on every authenticated request. The caller never touches Authorization headers.AppState.handleUnauthorized() on every 401, which logs the user out. Do not add per-call 401 handling.Configuration.apiBaseURL, which reads from Info.plist (fed by xcconfig). Never hardcode "http://..." in a service method.upload(path:imageData:filename:). Do not hand-roll boundary handling in feature code.APIClient.swift or Services/Auth*.swift.extension APIClient).struct DataResponse<T: Decodable>: Decodable { let data: T }
struct PaginatedResponse<T: Decodable>: Decodable {
let data: [T]
let cursor: String?
}
struct EmptyResponse: Decodable {}
Every endpoint returns one of these. Example:
// GET /posts/123 → { "data": { "id": 123, "title": "..." } }
let response: DataResponse<Post> = try await APIClient.shared.get(path: "/posts/123")
let post = response.data
@MainActor
final class APIClient: APIClientProtocol {
static let shared = APIClient()
func get<T: Decodable>(path: String) async throws -> T
func post<T: Decodable, B: Encodable>(path: String, body: B) async throws -> T
func post<T: Decodable>(path: String) async throws -> T
func put<T: Decodable, B: Encodable>(path: String, body: B) async throws -> T
func delete<T: Decodable>(path: String) async throws -> T
func upload<T: Decodable>(path: String, imageData: Data, filename: String, mimeType: String = "image/jpeg") async throws -> T
}
APIClientProtocol (see templates/APIClientProtocol.swift) is the seam ViewModels inject for testability; templates/MockAPIClient.swift is the matching test double.
Put new endpoints in a feature-specific extension file, not in the body of APIClient:
// Services/APIClient+Posts.swift
extension APIClient {
func fetchFeed(cursor: String? = nil) async throws -> PaginatedResponse<Post> {
let query = cursor.map { "?cursor=\($0)" } ?? ""
return try await get(path: "/feed\(query)")
}
func toggleLike(postId: Int, liked: Bool) async throws -> EmptyResponse {
if liked {
return try await delete(path: "/posts/\(postId)/like")
} else {
return try await post(path: "/posts/\(postId)/like")
}
}
}
private let encoder: JSONEncoder = {
let e = JSONEncoder()
e.keyEncodingStrategy = .convertToSnakeCase
e.dateEncodingStrategy = .iso8601
return e
}()
private let decoder: JSONDecoder = {
let d = JSONDecoder()
d.keyDecodingStrategy = .convertFromSnakeCase
d.dateDecodingStrategy = .iso8601
return d
}()
// Inside the shared request dispatch:
if httpResponse.statusCode == 401 {
await AppState.shared.handleUnauthorized()
throw APIError.unauthorized
}
AppState.handleUnauthorized() clears the keychain token and flips isAuthenticated = false. SwiftUI then routes to the login screen automatically.
convertToSnakeCase on the encoderSymptom: Phoenix replies with {"errors": {"first_name": ["can't be blank"]}} even though you sent {"firstName": "Joe"}.
Fix: encoder.keyEncodingStrategy = .convertToSnakeCase. Same for the decoder.
Symptom: Works on simulator, fails on device — or ships prod pointing at localhost.
Fix: Read from Configuration.apiBaseURL, which pulls from Info.plist, which is fed by Config.xcconfig (so Debug and Release can differ).
APIClient() in a testSymptom: Tests hit the real network.
Fix: APIClient.shared is the only instance. For tests, inject APIClientProtocol into view models and use MockAPIClient (both provided as templates).
Authorization header in feature codeSymptom: A view model pulls the token from Keychain and sets the header on a one-off URLRequest.
Fix: Move that call through APIClient. Bearer injection is centralized — feature code should never touch auth headers.
See <plugin-root>/templates/APIClient.swift for the full canonical implementation including envelopes, encoder/decoder, bearer injection, 401 handling, and multipart upload. Copy it into Services/APIClient.swift on day 1.
MANDATORY when adding or updating icons in AppIcon.appiconset/. Enforces 1024x1024 sizing and the per-variant alpha rules (primary opaque, dark transparent, tinted opaque grayscale).
MANDATORY for auth token storage. Use before writing any code that stores, reads, or deletes authentication credentials.
MANDATORY when the user asks to add or scaffold a new feature or screen. Chains Model + ViewModel + View + APIClient extension + test stub in one pass.
MANDATORY for Info.plist work. Use before adding imports that require user permission (PhotosUI, AVFoundation camera, CoreLocation, ATT, HealthKit, Contacts, EventKit).
MANDATORY for iOS project initialization and project.yml work. Use before creating or editing project.yml, Package.swift, or the top-level directory layout.
MANDATORY for any AsyncImage rendering a URL from a JSON response. Use before writing AsyncImage(url:) anywhere in the View layer.