Files
e-party-iOS/yana/Managers/NetworkManager.swift
edwinQQQ a0200c8859 feat: 添加项目基础文件和依赖管理
新增.gitignore、Podfile和Podfile.lock文件以管理项目依赖,添加README.md文件提供项目简介和安装步骤,创建NIMSessionManager、ClientConfig、LogManager和NetworkManager等管理类以支持网络请求和日志记录功能,更新AppDelegate和ContentView以集成NIM SDK和实现用户登录功能。
2025-05-29 16:14:28 +08:00

668 lines
23 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}
}