17 KiB
17 KiB
Yana iOS API 使用指南
📋 目录
🏗️ 架构概览
Yana iOS 项目采用基于 TCA (The Composable Architecture) 的现代化 API 架构设计:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ SwiftUI View │───▶│ TCA Reducer │───▶│ API Service │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ App State │ │ Network Layer │
└─────────────────┘ └─────────────────┘
核心组件
- APIService: 网络请求核心服务
- APIModels: 数据模型和协议定义
- APIEndpoints: API 端点配置
- APILogger: 请求日志记录
- BaseRequest: 基础请求参数管理
🚀 快速开始
1. 基本使用
import SwiftUI
struct ContentView: View {
@State private var isLoading = false
@State private var result = ""
var body: some View {
VStack {
Button("发起 API 请求") {
Task {
await makeAPIRequest()
}
}
.disabled(isLoading)
if isLoading {
ProgressView("请求中...")
}
Text(result)
}
}
private func makeAPIRequest() async {
isLoading = true
do {
// 创建 API 服务实例
let apiService = LiveAPIService()
// 创建请求
let request = ConfigRequest()
// 发起请求
let response = try await apiService.request(request)
await MainActor.run {
result = "请求成功: \(response)"
isLoading = false
}
} catch {
await MainActor.run {
result = "请求失败: \(error.localizedDescription)"
isLoading = false
}
}
}
}
2. TCA 集成使用
import ComposableArchitecture
@Reducer
struct APIFeature {
@ObservableState
struct State: Equatable {
var data: ConfigResponse?
var isLoading = false
var errorMessage: String?
}
enum Action: Equatable {
case loadConfig
case configLoaded(ConfigResponse)
case loadingFailed(String)
}
@Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .loadConfig:
state.isLoading = true
state.errorMessage = nil
return .run { send in
do {
let request = ConfigRequest()
let response = try await apiService.request(request)
await send(.configLoaded(response))
} catch {
await send(.loadingFailed(error.localizedDescription))
}
}
case let .configLoaded(response):
state.isLoading = false
state.data = response
return .none
case let .loadingFailed(error):
state.isLoading = false
state.errorMessage = error
return .none
}
}
}
}
⚙️ 环境配置
服务器环境
环境 | 地址 | 说明 |
---|---|---|
测试环境 | http://beta.api.molistar.xyz |
开发测试服务器 |
生产环境 | https://api.hfighting.com |
正式服务器 |
配置参数
struct APIConfiguration {
static let baseURL = "http://beta.api.molistar.xyz"
static let timeout: TimeInterval = 30.0
static let maxDataSize: Int = 50 * 1024 * 1024 // 50MB
}
默认请求头
所有请求都会自动添加以下请求头:
static var defaultHeaders: [String: String] {
return [
"Content-Type": "application/json",
"Accept": "application/json",
"Accept-Encoding": "gzip, br",
"Accept-Language": Locale.current.languageCode ?? "en",
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
]
}
📡 请求方式
1. GET 请求
struct ConfigRequest: APIRequestProtocol {
typealias Response = ConfigResponse
let endpoint = "/client/config"
let method: HTTPMethod = .GET
let includeBaseParameters = true
let queryParameters: [String: String]? = nil
let bodyParameters: [String: Any]? = nil
let headers: [String: String]? = nil
let timeout: TimeInterval = 30.0
}
2. POST 请求
struct LoginRequest: APIRequestProtocol {
typealias Response = LoginResponse
let endpoint = "/auth/login"
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]? = nil
let bodyParameters: [String: Any]?
let headers: [String: String]? = nil
let timeout: TimeInterval = 30.0
init(username: String, password: String) {
self.bodyParameters = [
"username": username,
"password": password
]
}
}
3. 基础参数说明
每个请求都会自动包含以下基础参数:
struct BaseRequest {
let acceptLanguage: String // 用户语言偏好
let os: String = "iOS" // 操作系统类型
let osVersion: String // 系统版本号
let netType: Int // 网络类型 (WiFi=2, 蜂窝=1)
let ispType: String // 运营商类型
let channel: String // 应用分发渠道
let model: String // 设备型号
let deviceId: String // 设备唯一标识
let appVersion: String // 应用版本
let app: String // 应用名称
let lang: String // 语言代码
let mcc: String? // 移动国家代码
let pubSign: String // 安全签名
}
❌ 错误处理
错误类型
enum APIError: Error, Equatable {
case invalidURL // 无效的 URL
case noData // 没有收到数据
case decodingError(String) // 数据解析失败
case networkError(String) // 网络错误
case httpError(statusCode: Int, message: String?) // HTTP 错误
case timeout // 请求超时
case resourceTooLarge // 响应数据过大
case unknown(String) // 未知错误
}
错误处理示例
do {
let response = try await apiService.request(request)
// 处理成功响应
} catch let apiError as APIError {
switch apiError {
case .networkError(let message):
print("网络错误: \(message)")
case .timeout:
print("请求超时,请检查网络连接")
case .httpError(let statusCode, let message):
print("服务器错误 \(statusCode): \(message ?? "未知错误")")
case .decodingError(let message):
print("数据解析失败: \(message)")
default:
print("其他错误: \(apiError.localizedDescription)")
}
} catch {
print("未知错误: \(error)")
}
🔐 安全机制
签名生成流程
- 参数过滤: 移除系统级参数
- 参数排序: 按字典 key 升序排序
- 字符串拼接:
"key0=value0&key1=value1"
- 添加密钥: 拼接
key=rpbs6us1m8r2j9g6u06ff2bo18orwaya
- MD5加密: 生成大写 MD5 签名
认证头部
// 用户认证相关头部(如果用户已登录)
if let userId = UserInfoManager.getCurrentUserId() {
headers["pub_uid"] = userId
}
if let userTicket = UserInfoManager.getCurrentUserTicket() {
headers["pub_ticket"] = userTicket
}
💡 最佳实践
1. 错误处理
// ✅ 推荐:完整的错误处理
do {
let response = try await apiService.request(request)
// 处理成功响应
} catch let urlError as URLError {
switch urlError.code {
case .notConnectedToInternet:
showAlert("网络不可用,请检查网络连接")
case .timedOut:
showAlert("请求超时,请重试")
default:
showAlert("网络错误: \(urlError.localizedDescription)")
}
} catch let apiError as APIError {
showAlert(apiError.localizedDescription)
} catch {
showAlert("未知错误: \(error)")
}
2. 主线程更新 UI
// ✅ 推荐:使用 MainActor 更新 UI
await MainActor.run {
self.isLoading = false
self.data = response
}
3. 取消请求
// ✅ 推荐:支持取消的请求
struct ContentView: View {
@State private var task: Task<Void, Never>?
private func makeRequest() {
task = Task {
do {
let response = try await apiService.request(request)
// 处理响应
} catch {
if !Task.isCancelled {
// 处理错误
}
}
}
}
private func cancelRequest() {
task?.cancel()
}
}
4. 重试机制
// ✅ 推荐:实现重试逻辑
func requestWithRetry<T: APIRequestProtocol>(
_ request: T,
maxRetries: Int = 3
) async throws -> T.Response {
var lastError: Error?
for attempt in 0..<maxRetries {
do {
return try await apiService.request(request)
} catch {
lastError = error
if attempt < maxRetries - 1 {
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
}
}
}
throw lastError!
}
📋 API接口列表
当前可用接口
接口名称 | 端点 | 方法 | 说明 |
---|---|---|---|
配置获取 | /client/config |
GET | 获取客户端配置信息 |
初始化配置 | /client/init |
GET | 获取初始化配置 |
用户登录 | /auth/login |
POST | 用户登录认证 |
接口扩展
要添加新的 API 接口,请按以下步骤操作:
- 在
APIEndpoints.swift
中添加端点:
enum APIEndpoint: String, CaseIterable {
case newEndpoint = "/new/endpoint"
// ...
}
- 创建请求模型:
struct NewRequest: APIRequestProtocol {
typealias Response = NewResponse
let endpoint = "/new/endpoint"
let method: HTTPMethod = .POST
let includeBaseParameters = true
// ... 其他属性
}
- 创建响应模型:
struct NewResponse: Codable {
let success: Bool
let data: SomeDataModel?
let message: String?
}
📝 示例代码
完整的登录功能示例
import SwiftUI
import ComposableArchitecture
// MARK: - Login Feature
@Reducer
struct LoginFeature {
@ObservableState
struct State: Equatable {
var username = ""
var password = ""
var isLoading = false
var isLoggedIn = false
var errorMessage: String?
}
enum Action: Equatable {
case usernameChanged(String)
case passwordChanged(String)
case loginButtonTapped
case loginResponse(Result<LoginResponse, APIError>)
}
@Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case let .usernameChanged(username):
state.username = username
return .none
case let .passwordChanged(password):
state.password = password
return .none
case .loginButtonTapped:
state.isLoading = true
state.errorMessage = nil
let request = LoginRequest(
username: state.username,
password: state.password
)
return .run { send in
do {
let response = try await apiService.request(request)
await send(.loginResponse(.success(response)))
} catch let error as APIError {
await send(.loginResponse(.failure(error)))
} catch {
await send(.loginResponse(.failure(.unknown(error.localizedDescription))))
}
}
case let .loginResponse(.success(response)):
state.isLoading = false
state.isLoggedIn = response.success
if !response.success {
state.errorMessage = response.message
}
return .none
case let .loginResponse(.failure(error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
return .none
}
}
}
}
// MARK: - Login View
struct LoginView: View {
let store: StoreOf<LoginFeature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack(spacing: 20) {
TextField("用户名", text: viewStore.binding(
get: \.username,
send: LoginFeature.Action.usernameChanged
))
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("密码", text: viewStore.binding(
get: \.password,
send: LoginFeature.Action.passwordChanged
))
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("登录") {
viewStore.send(.loginButtonTapped)
}
.disabled(viewStore.isLoading || viewStore.username.isEmpty || viewStore.password.isEmpty)
if viewStore.isLoading {
ProgressView("登录中...")
}
if let errorMessage = viewStore.errorMessage {
Text(errorMessage)
.foregroundColor(.red)
}
}
.padding()
}
}
}
// MARK: - Request & Response Models
struct LoginRequest: APIRequestProtocol {
typealias Response = LoginResponse
let endpoint = "/auth/login"
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]? = nil
let bodyParameters: [String: Any]?
let headers: [String: String]? = nil
let timeout: TimeInterval = 30.0
init(username: String, password: String) {
self.bodyParameters = [
"username": username,
"password": password
]
}
}
struct LoginResponse: Codable {
let success: Bool
let message: String?
let data: UserData?
}
struct UserData: Codable {
let userId: String
let username: String
let token: String
}
🔧 调试和日志
启用详细日志
API 请求和响应会自动记录到控制台,包括:
- 请求 URL 和参数
- 请求头信息
- 响应状态码和数据
- 请求耗时
- 错误信息
日志示例
🚀 API Request: GET /client/config
📋 Headers: ["Content-Type": "application/json", "Accept": "application/json"]
📊 Query Parameters: ["deviceId": "ABC123", "appVersion": "1.0.0"]
✅ API Response: 200 OK (0.45s)
📦 Response Size: 1.2KB
📄 Response Data: {"success": true, "data": {...}}
📚 相关文档
🤝 贡献指南
- 遵循现有的代码风格和架构模式
- 为新的 API 接口添加完整的文档和示例
- 确保所有请求都包含适当的错误处理
- 添加单元测试覆盖新功能
- 更新相关文档
注意: 本文档基于当前项目架构编写,如有架构变更请及时更新文档内容。