255 lines
7.5 KiB
Markdown
255 lines
7.5 KiB
Markdown
---
|
|
name: swift-networking
|
|
description: HTTP client, API handling, Error management cho Swift Enterprise. Use for REST APIs, URLSession, async networking, hoặc khi cần structured API layer.
|
|
compatibility: "Swift 5.9+, iOS 17+, Foundation"
|
|
metadata:
|
|
author: Velik Ho
|
|
version: "1.0"
|
|
references: "URLSession, Swift Concurrency"
|
|
---
|
|
|
|
# Swift Networking Patterns
|
|
|
|
HTTP client và API handling patterns cho Swift Enterprise applications.
|
|
|
|
## When to Use This Skill / Khi Nào Sử Dụng
|
|
|
|
Use this skill when:
|
|
- Building REST API client / Xây dựng REST API client
|
|
- Need URLSession wrapper / Cần wrapper cho URLSession
|
|
- Implementing token-based auth / Triển khai auth token-based
|
|
- Error handling for APIs / Xử lý lỗi API
|
|
|
|
## Core Patterns / Mẫu Chính
|
|
|
|
### API Error Enum
|
|
|
|
```swift
|
|
// Services/APIError.swift
|
|
|
|
/// API error types
|
|
/// Các loại lỗi API
|
|
enum APIError: Error, LocalizedError {
|
|
case invalidURL
|
|
case noData
|
|
case decodingError(Error)
|
|
case networkError(Error)
|
|
case serverError(statusCode: Int, message: String?)
|
|
case unauthorized
|
|
case forbidden
|
|
case notFound
|
|
case rateLimited
|
|
case unknown
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .invalidURL:
|
|
return "Invalid URL / URL không hợp lệ"
|
|
case .noData:
|
|
return "No data received / Không nhận được dữ liệu"
|
|
case .decodingError(let error):
|
|
return "Decoding error: \(error.localizedDescription)"
|
|
case .networkError(let error):
|
|
return "Network error: \(error.localizedDescription)"
|
|
case .serverError(let code, let message):
|
|
return "Server error (\(code)): \(message ?? "Unknown")"
|
|
case .unauthorized:
|
|
return "Unauthorized / Chưa xác thực"
|
|
case .forbidden:
|
|
return "Access forbidden / Truy cập bị từ chối"
|
|
case .notFound:
|
|
return "Resource not found / Không tìm thấy"
|
|
case .rateLimited:
|
|
return "Rate limited / Giới hạn request"
|
|
case .unknown:
|
|
return "Unknown error / Lỗi không xác định"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### HTTP Method Enum
|
|
|
|
```swift
|
|
/// HTTP request methods
|
|
/// Các phương thức HTTP
|
|
enum HTTPMethod: String {
|
|
case get = "GET"
|
|
case post = "POST"
|
|
case put = "PUT"
|
|
case patch = "PATCH"
|
|
case delete = "DELETE"
|
|
}
|
|
```
|
|
|
|
### API Service Protocol
|
|
|
|
```swift
|
|
/// API service protocol for DI
|
|
/// Protocol API service cho DI
|
|
protocol APIServiceProtocol {
|
|
func request<T: Decodable>(
|
|
endpoint: String,
|
|
method: HTTPMethod,
|
|
body: Encodable?,
|
|
headers: [String: String]?
|
|
) async throws -> T
|
|
}
|
|
```
|
|
|
|
### API Service Implementation
|
|
|
|
```swift
|
|
// Services/APIService.swift
|
|
|
|
/// Main API service
|
|
/// Dịch vụ API chính
|
|
final class APIService: APIServiceProtocol {
|
|
|
|
// MARK: - Singleton
|
|
|
|
static let shared = APIService()
|
|
|
|
// MARK: - Properties
|
|
|
|
private let session: URLSession
|
|
private let encoder: JSONEncoder
|
|
private let decoder: JSONDecoder
|
|
|
|
// MARK: - Init
|
|
|
|
init(session: URLSession = .shared) {
|
|
self.session = session
|
|
|
|
self.encoder = JSONEncoder()
|
|
encoder.keyEncodingStrategy = .convertToSnakeCase
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
|
|
self.decoder = JSONDecoder()
|
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
}
|
|
|
|
// MARK: - Request
|
|
|
|
/// Perform network request
|
|
/// Thực hiện request network
|
|
func request<T: Decodable>(
|
|
endpoint: String,
|
|
method: HTTPMethod = .get,
|
|
body: Encodable? = nil,
|
|
headers: [String: String]? = nil
|
|
) async throws -> T {
|
|
|
|
// Build URL
|
|
guard let url = URL(string: APIConfig.baseURL + APIConfig.apiVersion + endpoint) else {
|
|
throw APIError.invalidURL
|
|
}
|
|
|
|
// Create request
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = method.rawValue
|
|
request.timeoutInterval = APIConfig.timeout
|
|
|
|
// Set headers
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
|
|
// Add auth token
|
|
if let token = await AuthManager.shared.accessToken {
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
}
|
|
|
|
// Add custom headers
|
|
headers?.forEach { key, value in
|
|
request.setValue(value, forHTTPHeaderField: key)
|
|
}
|
|
|
|
// Set body
|
|
if let body = body {
|
|
request.httpBody = try encoder.encode(body)
|
|
}
|
|
|
|
// Perform request
|
|
let (data, response) = try await session.data(for: request)
|
|
|
|
// Handle response
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw APIError.unknown
|
|
}
|
|
|
|
// Check status code
|
|
switch httpResponse.statusCode {
|
|
case 200...299:
|
|
do {
|
|
return try decoder.decode(T.self, from: data)
|
|
} catch {
|
|
throw APIError.decodingError(error)
|
|
}
|
|
case 401:
|
|
await AuthManager.shared.handleUnauthorized()
|
|
throw APIError.unauthorized
|
|
case 403:
|
|
throw APIError.forbidden
|
|
case 404:
|
|
throw APIError.notFound
|
|
case 429:
|
|
throw APIError.rateLimited
|
|
default:
|
|
let message = String(data: data, encoding: .utf8)
|
|
throw APIError.serverError(statusCode: httpResponse.statusCode, message: message)
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience Methods
|
|
|
|
/// GET request
|
|
func get<T: Decodable>(endpoint: String) async throws -> T {
|
|
try await request(endpoint: endpoint, method: .get, body: nil as String?, headers: nil)
|
|
}
|
|
|
|
/// POST request
|
|
func post<T: Decodable, B: Encodable>(endpoint: String, body: B) async throws -> T {
|
|
try await request(endpoint: endpoint, method: .post, body: body, headers: nil)
|
|
}
|
|
|
|
/// PUT request
|
|
func put<T: Decodable, B: Encodable>(endpoint: String, body: B) async throws -> T {
|
|
try await request(endpoint: endpoint, method: .put, body: body, headers: nil)
|
|
}
|
|
|
|
/// DELETE request
|
|
func delete<T: Decodable>(endpoint: String) async throws -> T {
|
|
try await request(endpoint: endpoint, method: .delete, body: nil as String?, headers: nil)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Common Mistakes / Lỗi Thường Gặp
|
|
|
|
| Mistake | Problem | Solution |
|
|
|---------|---------|----------|
|
|
| No error handling | Crashes | Use `do-catch` with custom errors |
|
|
| Force unwrap URLs | Crashes | Guard with `APIError.invalidURL` |
|
|
| Blocking main thread | Frozen UI | Use `async/await` |
|
|
| No timeout | Hanging requests | Set `timeoutInterval` |
|
|
| Hardcoded URLs | Hard to maintain | Use `APIConfig` enum |
|
|
|
|
## Quick Reference / Tham Chiếu Nhanh
|
|
|
|
| Category | Standard |
|
|
|----------|----------|
|
|
| HTTP Client | URLSession with async/await |
|
|
| Errors | Custom `APIError` enum |
|
|
| Encoding | snake_case JSON |
|
|
| Auth | Bearer token injection |
|
|
| Timeout | 30 seconds default |
|
|
|
|
## Resources / Tài Nguyên
|
|
|
|
- [Swift Enterprise Architect](../swift-enterprise-architect/SKILL.md) - Architecture
|
|
- [Swift Security](../swift-security/SKILL.md) - Token management
|
|
- [Apple URLSession](https://developer.apple.com/documentation/foundation/urlsession)
|