feat: 添加Swift Package管理和API功能模块

新增Package.swift和Package.resolved文件以支持Swift Package管理,创建API相关文件(API.swift、APICaller.swift、APIConstants.swift、APIEndpoints.swift、APIService.swift、APILogger.swift、APIModels.swift、Integration-Guide.md)以实现API请求管理和网络交互功能,增强项目的功能性和可扩展性。同时更新.gitignore以排除构建文件和临时文件。
This commit is contained in:
edwinQQQ
2025-06-04 17:25:21 +08:00
parent 3007820335
commit 007c10daaf
30 changed files with 2123 additions and 864 deletions

View File

@@ -21,7 +21,9 @@ struct NIMConfigurationManager {
static func setupChatSDK(with option: NIMSDKOption) {
let v2Option = V2NIMSDKOption()
v2Option.enableV2CloudConversation = false
IMKitClient.instance.setupIM2(option, v2Option)
// TODO: IMKitClient API
// IMKitClient.shared.setupIM2(option, v2Option)
print("⚠️ NIM SDK 配置暂时被注释,需要修复 IMKitClient API")
}
static func configureNIMSDKOption() -> NIMSDKOption {

View File

@@ -1,667 +0,0 @@
import Foundation
import Alamofire
import CoreTelephony
import UIKit
import Darwin // utsname
import CommonCrypto
//
//enum AppConfig {
// static let baseURL = "https://api.example.com" // API URL
//}
//
enum NetworkStatus: Int {
case notReachable = 0
case reachableViaWWAN = 1
case reachableViaWiFi = 2
}
//
enum NetworkError: Error {
case invalidURL
case requestFailed(statusCode: Int, message: String?)
case invalidResponse
case decodingFailed
case networkUnavailable
case serverError(message: String)
case unauthorized
case rateLimited
var localizedDescription: String {
switch self {
case .invalidURL:
return "无效的 URL"
case .requestFailed(let statusCode, let message):
return "请求失败: \(statusCode), \(message ?? "未知错误")"
case .invalidResponse:
return "无效的响应"
case .decodingFailed:
return "数据解析失败"
case .networkUnavailable:
return "网络不可用"
case .serverError(let message):
return "服务器错误: \(message)"
case .unauthorized:
return "未授权访问"
case .rateLimited:
return "请求过于频繁"
}
}
}
// MARK: - MD5
extension String {
func md5() -> String {
let str = self.cString(using: .utf8)
let strLen = CUnsignedInt(self.lengthOfBytes(using: .utf8))
let digestLen = Int(CC_MD5_DIGEST_LENGTH)
let result = UnsafeMutablePointer<UInt8>.allocate(capacity: digestLen)
CC_MD5(str!, strLen, result)
let hash = NSMutableString()
for i in 0..<digestLen {
hash.appendFormat("%02x", result[i])
}
result.deallocate()
return hash as String
}
}
//
struct BaseParameters: Encodable {
let acceptLanguage: String
let os: String = "iOS"
let osVersion: String
let ispType: String
let channel: String
let model: String
let deviceId: String
let appVersion: String
let app: String
let mcc: String?
let pub_sign: String
enum CodingKeys: String, CodingKey {
case acceptLanguage = "Accept-Language"
case os, osVersion, ispType, channel, model, deviceId
case appVersion, app, mcc
case pub_sign
}
init() {
// 使
self.acceptLanguage = LanguageManager.getCurrentLanguage()
//
self.osVersion = UIDevice.current.systemVersion
//
let networkInfo = CTTelephonyNetworkInfo()
var ispType = "65535"
var mcc: String? = nil // mcc
if #available(iOS 12.0, *) {
// 使 API
if let carriers = networkInfo.serviceSubscriberCellularProviders,
let carrier = carriers.values.first {
ispType = (
carrier.mobileNetworkCode != nil ? carrier.mobileNetworkCode : ""
) ?? "65535"
mcc = carrier.mobileCountryCode
}
} else {
//
if let carrier = networkInfo.subscriberCellularProvider {
ispType = (
carrier.mobileNetworkCode != nil ? carrier.mobileNetworkCode : ""
) ?? "65535"
mcc = carrier.mobileCountryCode
}
}
self.ispType = ispType
self.mcc = mcc // mcc
//
self.channel = ChannelManager.getCurrentChannel()
//
self.model = DeviceManager.getDeviceModel()
//
self.deviceId = UIDevice.current.identifierForVendor?.uuidString ?? ""
//
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
//
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? ""
// pub_sign
let key = "rpbs6us1m8r2j9g6u06ff2bo18orwaya"
let signString = "key=\(key)"
self.pub_sign = signString.md5().uppercased()
}
}
final class NetworkManager {
static let shared = NetworkManager()
//
struct NetworkResponse<T> {
let statusCode: Int
let data: T?
let headers: [AnyHashable: Any]
let metrics: URLSessionTaskMetrics?
var isSuccessful: Bool {
return (200...299).contains(statusCode)
}
}
private let reachability = NetworkReachabilityManager()
private var isNetworkReachable = false
private let baseURL = AppConfig.baseURL
private let session: Session
private let retryLimit = 2
//
var networkStatusChanged: ((NetworkStatus) -> Void)?
init() {
let configuration = URLSessionConfiguration.af.default
configuration.httpShouldSetCookies = true
configuration.httpCookieAcceptPolicy = .always
configuration.timeoutIntervalForRequest = 60
configuration.timeoutIntervalForResource = 60
configuration.httpMaximumConnectionsPerHost = 10
//
configuration.httpAdditionalHeaders = [
"Accept": "application/json, text/json, text/javascript, text/html, text/plain, image/jpeg, image/png",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json"
]
// TLS 1.2+ HTTP/1.1
configuration.tlsMinimumSupportedProtocolVersion = .TLSv12
configuration.httpShouldUsePipelining = true
//
let retrier = RetryPolicy(retryLimit: UInt(retryLimit))
session = Session(
configuration: configuration,
interceptor: retrier,
eventMonitors: [AlamofireLogger()]
)
//
setupReachability()
}
private func setupReachability() {
reachability?.startListening { [weak self] status in
guard let self = self else { return }
switch status {
case .reachable(.ethernetOrWiFi):
self.isNetworkReachable = true
self.networkStatusChanged?(.reachableViaWiFi)
case .reachable(.cellular):
self.isNetworkReachable = true
self.networkStatusChanged?(.reachableViaWWAN)
case .notReachable:
self.isNetworkReachable = false
self.networkStatusChanged?(.notReachable)
case .unknown:
self.isNetworkReachable = false
self.networkStatusChanged?(.notReachable)
case .reachable(_):
self.isNetworkReachable = true
self.networkStatusChanged?(.reachableViaWiFi)
@unknown default:
fatalError("未知的网络状态")
}
}
}
// MARK: - 便
/// GET
func get<T: Decodable>(
path: String,
queryItems: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
enhancedRequest(
path: path,
method: .get,
queryItems: queryItems,
responseType: T.self
) { result in
switch result {
case .success(let response):
if let data = response.data as? T {
completion(.success(data))
} else {
completion(.failure(.decodingFailed))
}
case .failure(let error):
completion(.failure(error))
}
}
}
/// POST
func post<T: Decodable, P: Encodable>(
path: String,
parameters: P,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
enhancedRequest(
path: path,
method: .post,
bodyParameters: parameters,
responseType: T.self
) { result in
switch result {
case .success(let response):
if let data = response.data as? T {
completion(.success(data))
} else {
completion(.failure(.decodingFailed))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// MARK: -
func enhancedRequest<T>(
path: String,
method: HTTPMethod = .get,
queryItems: [String: String]? = nil,
bodyParameters: Encodable? = nil,
responseType: T.Type = Data.self,
completion: @escaping (Result<NetworkResponse<T>, NetworkError>) -> Void
) {
//
guard isNetworkReachable else {
completion(.failure(.networkUnavailable))
return
}
guard let baseURL = URL(string: baseURL) else {
completion(.failure(.invalidURL))
return
}
var urlComponents = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true)
urlComponents?.queryItems = queryItems?.map { URLQueryItem(name: $0.key, value: $0.value) }
guard let finalURL = urlComponents?.url else {
completion(.failure(.invalidURL))
return
}
//
// TODO: pub_sign
let baseParams = BaseParameters()
var parameters: Parameters = baseParams.dictionary ?? [:]
if let customParams = bodyParameters {
if let dict = try? customParams.asDictionary() {
parameters.merge(dict) { (_, new) in new }
}
}
session.request(finalURL, method: method, parameters: parameters, encoding: JSONEncoding.default, headers: commonHeaders)
.validate()
.responseData { [weak self] response in
guard let self = self else { return }
let statusCode = response.response?.statusCode ?? -1
let headers = response.response?.allHeaderFields ?? [:]
let metrics = response.metrics
switch response.result {
case .success(let decodedData):
do {
let resultData: T
if T.self == Data.self {
resultData = decodedData as! T
} else if let decodableData = decodedData as? T {
resultData = decodableData
} else {
throw AFError.responseSerializationFailed(reason: .decodingFailed(error: NetworkError.decodingFailed))
}
let networkResponse = NetworkResponse(
statusCode: statusCode,
data: resultData,
headers: headers,
metrics: metrics
)
if networkResponse.isSuccessful {
completion(.success(networkResponse))
} else {
self.handleErrorResponse(statusCode: statusCode, completion: completion)
}
} catch {
completion(.failure(.decodingFailed))
}
case .failure(let error):
self.handleRequestError(error, statusCode: statusCode, completion: completion)
}
}
}
// MARK: -
private func handleErrorResponse<T>(
statusCode: Int,
completion: (Result<NetworkResponse<T>, NetworkError>) -> Void
) {
switch statusCode {
case 401:
completion(.failure(.unauthorized))
case 429:
completion(.failure(.rateLimited))
case 500...599:
completion(.failure(.serverError(message: "服务器错误 \(statusCode)")))
default:
completion(.failure(.requestFailed(statusCode: statusCode, message: "请求失败")))
}
}
private func handleRequestError<T>(
_ error: AFError,
statusCode: Int,
completion: (Result<NetworkResponse<T>, NetworkError>) -> Void
) {
if let underlyingError = error.underlyingError as? URLError {
switch underlyingError.code {
case .notConnectedToInternet:
completion(.failure(.networkUnavailable))
default:
completion(.failure(.requestFailed(
statusCode: statusCode,
message: underlyingError.localizedDescription
)))
}
} else {
completion(.failure(.invalidResponse))
}
}
// MARK: -
private var commonHeaders: HTTPHeaders {
var headers = HTTPHeaders()
//
if let language = Locale.preferredLanguages.first {
headers.add(name: "Accept-Language", value: language)
}
headers.add(name: "App-Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0")
//
let uid = "" //AccountInfoStorage.instance?.getUid() ?? ""
let ticket = "" //AccountInfoStorage.instance?.getTicket() ?? ""
headers.add(name: "pub_uid", value: uid)
headers.add(name: "pub_ticket", value: ticket)
return headers
}
// MARK: -
func request<T: Decodable, P: Encodable>(
_ path: String,
method: HTTPMethod = .get,
parameters: P? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
enhancedRequest(
path: path,
method: method,
bodyParameters: parameters,
responseType: T.self
) { result in
switch result {
case .success(let response):
if let data = response.data {
completion(.success(data))
} else {
completion(.failure(.decodingFailed))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// 便
func request<T: Decodable>(
_ path: String,
method: HTTPMethod = .get,
parameters: [String: Any]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
// Data [String: String]
if let params = parameters {
do {
let jsonData = try JSONSerialization.data(withJSONObject: params)
let decoder = JSONDecoder()
let encodableParams = try decoder.decode([String: String].self, from: jsonData)
request(path, method: method, parameters: encodableParams, completion: completion)
} catch {
completion(.failure(.decodingFailed))
}
} else {
// 使
request(path, method: method, parameters: [String: String](), completion: completion)
}
}
// MARK: -
func getCurrentLanguage() -> String {
return LanguageManager.getCurrentLanguage()
}
func updateLanguage(_ language: String) {
LanguageManager.updateLanguage(language)
}
}
// MARK: - Logger
final class AlamofireLogger: EventMonitor {
func requestDidResume(_ request: Request) {
let allHeaders = request.request?.allHTTPHeaderFields ?? [:]
let relevantHeaders = allHeaders.filter { !$0.key.contains("Authorization") }
print("🚀 Request Started: \(request.description)")
print("📝 Headers: \(relevantHeaders)")
if let httpBody = request.request?.httpBody,
let parameters = try? JSONSerialization.jsonObject(with: httpBody) {
print("📦 Parameters: \(parameters)")
}
}
func request(_ request: DataRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, data: Data?) {
print("📥 Response Status: \(response.statusCode)")
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data) {
print("📄 Response Data: \(json)")
}
}
}
// MARK: - Encodable Extension
private extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
func asDictionary() throws -> [String: Any] {
let data = try JSONEncoder().encode(self)
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
throw NetworkError.decodingFailed
}
return dictionary
}
}
// MARK: -
enum LanguageManager {
static let languageKey = "UserSelectedLanguage"
//
static func mapLanguage(_ language: String) -> String {
// "zh-Hans""zh-Hant""zh-HK"
if language.hasPrefix("zh-Hans") || language.hasPrefix("zh-CN") {
return "zh-Hant" //
} else if language.hasPrefix("zh") {
return "zh-Hant" //
} else if language.hasPrefix("ar") {
return "ar" //
} else if language.hasPrefix("tr") {
return "tr" //
} else {
return "en" //
}
}
//
static func getCurrentLanguage() -> String {
// UserDefaults
if let savedLanguage = UserDefaults.standard.string(forKey: languageKey) {
return savedLanguage
}
//
let preferredLanguages = Locale.preferredLanguages.first
// let systemLanguage = preferredLanguages.first ?? Locale.current.languageCode ?? "en"
//
let mappedLanguage = mapLanguage(preferredLanguages ?? "en")
UserDefaults.standard.set(mappedLanguage, forKey: languageKey)
UserDefaults.standard.synchronize()
return mappedLanguage
}
//
static func updateLanguage(_ language: String) {
let mappedLanguage = mapLanguage(language)
UserDefaults.standard.set(mappedLanguage, forKey: languageKey)
UserDefaults.standard.synchronize()
}
//
static func getSystemLanguageInfo() -> (preferred: [String], current: String?) {
return (
Bundle.main.preferredLocalizations,
Locale.current.languageCode
)
}
}
// MARK: -
enum DeviceManager {
//
static func getDeviceIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
return identifier
}
//
static func mapDeviceModel(_ identifier: String) -> String {
switch identifier {
// iPhone
case "iPhone13,1": return "iPhone 12 mini"
case "iPhone13,2": return "iPhone 12"
case "iPhone13,3": return "iPhone 12 Pro"
case "iPhone13,4": return "iPhone 12 Pro Max"
case "iPhone14,4": return "iPhone 13 mini"
case "iPhone14,5": return "iPhone 13"
case "iPhone14,2": return "iPhone 13 Pro"
case "iPhone14,3": return "iPhone 13 Pro Max"
case "iPhone14,7": return "iPhone 14"
case "iPhone14,8": return "iPhone 14 Plus"
case "iPhone15,2": return "iPhone 14 Pro"
case "iPhone15,3": return "iPhone 14 Pro Max"
case "iPhone15,4": return "iPhone 15"
case "iPhone15,5": return "iPhone 15 Plus"
case "iPhone16,1": return "iPhone 15 Pro"
case "iPhone16,2": return "iPhone 15 Pro Max"
// iPad
case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro 12.9-inch (5th generation)"
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11": return "iPad Pro 12.9-inch (6th generation)"
// iPod
case "iPod9,1": return "iPod touch (7th generation)"
//
case "i386", "x86_64", "arm64": return "Simulator \(mapDeviceModel(ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
default: return identifier //
}
}
//
static func getDeviceModel() -> String {
let identifier = getDeviceIdentifier()
return mapDeviceModel(identifier)
}
}
// MARK: -
enum ChannelManager {
static let enterpriseBundleId = "com.stupidmonkey.yana.yana"//"com.hflighting.yumi"
enum ChannelType: String {
case enterprise = "enterprise"
case testflight = "testflight"
case appstore = "appstore"
}
//
static func isEnterprise() -> Bool {
let bundleId = Bundle.main.bundleIdentifier ?? ""
return bundleId == enterpriseBundleId
}
// TestFlight
static func isTestFlight() -> Bool {
return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
}
//
static func getCurrentChannel() -> String {
if isEnterprise() {
return ChannelType.enterprise.rawValue
} else if isTestFlight() {
return ChannelType.testflight.rawValue
} else {
return ChannelType.appstore.rawValue
}
}
}