├── .gitignore ├── APIService ├── Assets │ └── .gitkeep └── Classes │ ├── Cache │ └── CacheTool.swift │ ├── Core │ ├── APICache.swift │ ├── APIClient+Alamofire.swift │ ├── APIClient.swift │ ├── APIConfig.swift │ ├── APIError.swift │ ├── APIParsable.swift │ ├── APIPlugin.swift │ ├── APIRequest.swift │ ├── APIResponse.swift │ ├── APIResult.swift │ ├── APIService.swift │ └── DebugUtils.swift │ └── Plugin │ └── NetworkActivityPlugin.swift ├── CSAPIService.podspec ├── Documentation └── .gitkeep ├── Example ├── .jazzy.yaml ├── APIService.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── APIService-Example.xcscheme ├── APIService.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── APIService │ ├── APIModelWrapper.swift │ ├── APIResult+CS.swift │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── BetterCodaleExtensions.swift │ ├── CSAPIRequest.swift │ ├── CSAPIRequestProtocol.swift │ ├── CSBaseResponseModel.swift │ ├── HomeBanner.swift │ ├── HomeBannerAPI.swift │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── NetworkConstants.swift │ └── ViewController.swift ├── Doc │ └── jazzy-theme │ │ ├── assets │ │ ├── css │ │ │ ├── highlight.css.scss │ │ │ └── jazzy.css.scss │ │ ├── img │ │ │ ├── carat.png │ │ │ ├── dash.png │ │ │ └── gh.png │ │ └── js │ │ │ ├── jazzy.js │ │ │ └── jquery.min.js │ │ └── templates │ │ ├── doc.mustache │ │ ├── footer.mustache │ │ ├── header.mustache │ │ ├── nav.mustache │ │ ├── parameter.mustache │ │ ├── task.mustache │ │ └── tasks.mustache ├── Podfile └── Podfile.lock ├── LICENSE ├── README.md ├── _Pods.xcodeproj ├── docs ├── Classes.html ├── Classes │ ├── APIConfig.html │ ├── APIService.html │ └── CacheTool.html ├── Enums.html ├── Enums │ ├── APICacheError.html │ ├── APICacheExpiry.html │ ├── APICacheReadMode.html │ ├── APICacheWriteMode.html │ ├── APICompletionHandlerSourceType.html │ ├── APIError.html │ ├── APIRequestError.html │ ├── APIRequestTaskType.html │ ├── APIResponseError.html │ ├── APIResult.html │ ├── NetworkActivityChangeType.html │ └── NetworkStatus.html ├── Extensions.html ├── Extensions │ └── Data.html ├── Protocols.html ├── Protocols │ ├── APICacheTool.html │ ├── APIClient.html │ ├── APIJSONParsable.html │ ├── APIParsable.html │ ├── APIPlugin.html │ ├── APIRequest.html │ └── APIRequestTask.html ├── Structs.html ├── Structs │ ├── APICache.html │ ├── APICachePackage.html │ ├── APIResponse.html │ └── NetworkActivityPlugin.html ├── Typealiases.html ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ ├── APIService.docset │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes.html │ │ │ ├── Classes │ │ │ │ ├── APIConfig.html │ │ │ │ ├── APIService.html │ │ │ │ └── CacheTool.html │ │ │ ├── Enums.html │ │ │ ├── Enums │ │ │ │ ├── APICacheError.html │ │ │ │ ├── APICacheExpiry.html │ │ │ │ ├── APICacheReadMode.html │ │ │ │ ├── APICacheWriteMode.html │ │ │ │ ├── APICompletionHandlerSourceType.html │ │ │ │ ├── APIError.html │ │ │ │ ├── APIRequestError.html │ │ │ │ ├── APIRequestTaskType.html │ │ │ │ ├── APIResponseError.html │ │ │ │ ├── APIResult.html │ │ │ │ ├── NetworkActivityChangeType.html │ │ │ │ └── NetworkStatus.html │ │ │ ├── Extensions.html │ │ │ ├── Extensions │ │ │ │ └── Data.html │ │ │ ├── Protocols.html │ │ │ ├── Protocols │ │ │ │ ├── APICacheTool.html │ │ │ │ ├── APIClient.html │ │ │ │ ├── APIJSONParsable.html │ │ │ │ ├── APIParsable.html │ │ │ │ ├── APIPlugin.html │ │ │ │ ├── APIRequest.html │ │ │ │ └── APIRequestTask.html │ │ │ ├── Structs.html │ │ │ ├── Structs │ │ │ │ ├── APICache.html │ │ │ │ ├── APICachePackage.html │ │ │ │ ├── APIResponse.html │ │ │ │ └── NetworkActivityPlugin.html │ │ │ ├── Typealiases.html │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ └── gh.png │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ └── jquery.min.js │ │ │ └── search.json │ │ │ └── docSet.dsidx │ └── APIService.tgz ├── img │ ├── carat.png │ ├── dash.png │ └── gh.png ├── index.html ├── js │ ├── jazzy.js │ └── jquery.min.js ├── search.json └── undocumented.json ├── images └── flow.png └── uploadPod.sh /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | 3 | .DS_Store 4 | 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | # CocoaPods 33 | Pods/ 34 | 35 | 36 | # Carthage 37 | Carthage/Build 38 | 39 | # fastlane 40 | fastlane/report.xml 41 | fastlane/Preview.html 42 | fastlane/screenshots/**/*.png 43 | fastlane/test_output 44 | 45 | iOSInjectionProject/ 46 | 47 | ### Objective-C Patch ### 48 | 49 | ### Swift ### 50 | 51 | 52 | ## Playgrounds 53 | timeline.xctimeline 54 | playground.xcworkspace 55 | 56 | # Swift Package Manager 57 | Packages/ 58 | Package.pins 59 | Package.resolved 60 | .build/ 61 | 62 | 63 | # Accio dependency management 64 | Dependencies/ 65 | .accio/ 66 | 67 | 68 | ## Xcode Patch 69 | *.xcodeproj/* 70 | !*.xcodeproj/project.pbxproj 71 | !*.xcodeproj/xcshareddata/ 72 | !*.xcworkspace/contents.xcworkspacedata 73 | /*.gcno 74 | **/xcshareddata/WorkspaceSettings.xcsettings 75 | 76 | 77 | -------------------------------------------------------------------------------- /APIService/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/APIService/Assets/.gitkeep -------------------------------------------------------------------------------- /APIService/Classes/Cache/CacheTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultCacheStore.swift 3 | // CSAPIService 4 | // 5 | // Created by CoderStar on 2022/9/19. 6 | // 7 | 8 | import Cache 9 | import Foundation 10 | 11 | /// 默认的缓存工具 12 | public final class CacheTool { 13 | /// 单例 14 | public static let shared = CacheTool() 15 | 16 | /// 内存缓存配置 17 | public var memoryConfig: MemoryConfig? 18 | 19 | /// 磁盘缓存配置 20 | public var diskConfig: DiskConfig? 21 | 22 | private let defaultMemoryConfig = MemoryConfig() 23 | 24 | /// 默认最大尺寸 50M 25 | private let defaultDiskConfig = DiskConfig( 26 | name: "Cache", 27 | expiry: .never, 28 | maxSize: 1024 * 1024 * 50, 29 | directory: try! FileManager.default.url( 30 | for: .cachesDirectory, 31 | in: .userDomainMask, 32 | appropriateFor: nil, 33 | create: true 34 | ).appendingPathComponent("com.coderstar.APIService"), 35 | protectionType: .complete 36 | ) 37 | 38 | private var storage: Storage? 39 | 40 | private var hybridStorage: HybridStorage? 41 | 42 | private var memoryStorage: MemoryStorage? 43 | 44 | private var diskStorage: DiskStorage? 45 | 46 | private init() { 47 | NotificationCenter.default.addObserver(self, selector: #selector(clearMemoryCache), name: UIApplication.didReceiveMemoryWarningNotification, object: nil) 48 | NotificationCenter.default.addObserver(self, selector: #selector(cleanExpiredDiskCache), name: UIApplication.willTerminateNotification, object: nil) 49 | NotificationCenter.default.addObserver(self, selector: #selector(backgroundCleanExpiredDiskCache), name: UIApplication.didEnterBackgroundNotification, object: nil) 50 | } 51 | 52 | private func getDiskStorage() throws -> DiskStorage { 53 | if let storage = diskStorage { 54 | return storage 55 | } else { 56 | let diskStorage = try DiskStorage(config: diskConfig ?? defaultDiskConfig, transformer: TransformerFactory.forCodable(ofType: APICachePackage.self)) 57 | self.diskStorage = diskStorage 58 | return diskStorage 59 | } 60 | } 61 | 62 | private func getMemoryStorage() -> MemoryStorage { 63 | if let storage = memoryStorage { 64 | return storage 65 | } else { 66 | let memoryStorage = MemoryStorage(config: memoryConfig ?? defaultMemoryConfig) 67 | self.memoryStorage = memoryStorage 68 | return memoryStorage 69 | } 70 | } 71 | 72 | private func getStorage() throws -> Storage { 73 | if let storage = storage { 74 | return storage 75 | } else { 76 | let memoryStorage = getMemoryStorage() 77 | let diskStorage = try getDiskStorage() 78 | let hybridStorage = HybridStorage(memoryStorage: memoryStorage, diskStorage: diskStorage) 79 | let storage = Storage(hybridStorage: hybridStorage) 80 | 81 | self.hybridStorage = hybridStorage 82 | self.storage = storage 83 | 84 | return storage 85 | } 86 | } 87 | 88 | @objc 89 | private func clearMemoryCache() { 90 | self.memoryStorage?.removeAll() 91 | } 92 | 93 | @objc 94 | private func cleanExpiredDiskCache() { 95 | self.storage?.async.removeExpiredObjects { _ in } 96 | } 97 | 98 | @objc 99 | private func backgroundCleanExpiredDiskCache() { 100 | let sharedApplication = UIApplication.shared 101 | 102 | func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) { 103 | sharedApplication.endBackgroundTask(task) 104 | task = UIBackgroundTaskIdentifier.invalid 105 | } 106 | 107 | var backgroundTask: UIBackgroundTaskIdentifier! 108 | backgroundTask = sharedApplication.beginBackgroundTask { 109 | endBackgroundTask(&backgroundTask!) 110 | } 111 | 112 | self.storage?.async.removeExpiredObjects { _ in 113 | endBackgroundTask(&backgroundTask!) 114 | } 115 | } 116 | } 117 | 118 | extension CacheTool: APICacheTool { 119 | public func set(forKey key: String, data: APICachePackage, writeMode: APICacheWriteMode, expiry: APICacheExpiry, completion: ((Swift.Result) -> Void)?) { 120 | let resultExpiry: Expiry 121 | 122 | switch expiry { 123 | case .never: 124 | resultExpiry = .never 125 | case let .seconds(seconds): 126 | resultExpiry = .seconds(seconds) 127 | case let .date(date): 128 | resultExpiry = .date(date) 129 | } 130 | 131 | do { 132 | switch writeMode { 133 | case .none: 134 | break 135 | case .memory: 136 | getMemoryStorage().setObject(data, forKey: key, expiry: resultExpiry) 137 | case .disk: 138 | try getStorage().async.serialQueue.async { [weak self] in 139 | guard let self = self else { 140 | completion?(.failure(StorageError.deallocated)) 141 | return 142 | } 143 | 144 | do { 145 | try self.getDiskStorage().setObject(data, forKey: key, expiry: resultExpiry) 146 | completion?(.success(())) 147 | } catch { 148 | completion?(.failure(error)) 149 | } 150 | } 151 | case .memoryAndDisk: 152 | try getStorage().async.setObject(data, forKey: key, expiry: resultExpiry) { result in 153 | switch result { 154 | case let .value(value): 155 | completion?(.success(value)) 156 | case let .error(error): 157 | completion?(.failure(error)) 158 | } 159 | } 160 | } 161 | } catch { 162 | completion?(.failure(error)) 163 | } 164 | } 165 | 166 | public func set(forKey key: String, data: APICachePackage, writeMode: APICacheWriteMode, expiry: APICacheExpiry) throws { 167 | let resultExpiry: Expiry 168 | switch expiry { 169 | case .never: 170 | resultExpiry = .never 171 | case let .seconds(seconds): 172 | resultExpiry = .seconds(seconds) 173 | case let .date(date): 174 | resultExpiry = .date(date) 175 | } 176 | 177 | switch writeMode { 178 | case .none: 179 | return 180 | case .memory: 181 | getMemoryStorage().setObject(data, forKey: key, expiry: resultExpiry) 182 | case .disk: 183 | try getDiskStorage().setObject(data, forKey: key, expiry: resultExpiry) 184 | case .memoryAndDisk: 185 | try getStorage().setObject(data, forKey: key, expiry: resultExpiry) 186 | } 187 | } 188 | 189 | public func getValidObject(byKey key: String, completion: @escaping (Swift.Result) -> Void) { 190 | do { 191 | let storage = try getStorage() 192 | storage.async.entry(forKey: key) { result in 193 | switch result { 194 | case let .value(value): 195 | if value.expiry.isExpired { 196 | completion(.failure(APICacheError.expire(key: key, data: value.object))) 197 | } 198 | completion(.success(value.object)) 199 | case let .error(error): 200 | completion(.failure(error)) 201 | } 202 | } 203 | } catch { 204 | completion(.failure(error)) 205 | } 206 | } 207 | 208 | public func getValidObject(byKey key: String) throws -> APICachePackage { 209 | let entry = try getStorage().entry(forKey: key) 210 | if entry.expiry.isExpired { 211 | throw APICacheError.expire(key: key, data: entry.object) 212 | } 213 | 214 | return entry.object 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APICache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APICache.swift 3 | // CSAPIService 4 | // 5 | // Created by CoderStar on 2022/9/6. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 缓存使用模式 11 | public enum APICacheUsageMode { 12 | /// 不使用缓存 13 | case none 14 | 15 | /// 命中缓存后,仍然发起网络请求,但是不会执行网络回调 16 | /// 使用场景:命中缓存后业务方使用缓存,并请求网络刷新缓存 17 | case alsoNetwork 18 | 19 | /// 命中缓存之后,仍然发起网络请求并执行网络回调 20 | case alsoNetworkWithCallback 21 | 22 | /// 取消网络请求 23 | case cancelNetwork 24 | } 25 | 26 | /// 缓存写入模式 27 | public enum APICacheWriteMode { 28 | /// 不缓存 29 | case none 30 | 31 | /// 内存缓存 32 | case memory 33 | 34 | /// 磁盘缓存 35 | case disk 36 | 37 | /// 内存、磁盘两级缓存 38 | case memoryAndDisk 39 | } 40 | 41 | /// 缓存过期策略 42 | public enum APICacheExpiry { 43 | /// 永不过期 44 | case never 45 | 46 | /// 指定有效期 47 | case seconds(TimeInterval) 48 | 49 | /// 指定日期 50 | case date(Date) 51 | } 52 | 53 | /// 缓存 54 | public struct APICache { 55 | public init() {} 56 | 57 | /// 缓存使用模式 58 | public var usageMode: APICacheUsageMode = .none 59 | 60 | /// 写入缓存模式 61 | public var writeNode: APICacheWriteMode = .none 62 | 63 | /// 只有 writeNode 不为 .none 时,后面参数有效 64 | 65 | /// 缓存过期策略类型 66 | public var expiry: APICacheExpiry = .seconds(0) 67 | 68 | /// 额外的缓存key部分 69 | /// 可添加app版本号、用户id、缓存版本等 70 | public var extraCacheKey = "" 71 | 72 | /// 自定义缓存key 73 | /// 闭包参数为框架内部按照规则生成的key值,实际的cacheKey还会再此基础上进行md5 74 | public var customCacheKeyHandler: ((String) -> String)? 75 | } 76 | 77 | /// 缓存 78 | public struct APICachePackage: Codable { 79 | /// 缓存创建时间 80 | public var creationDate: Date 81 | 82 | /// 缓存数据 83 | public var data: Data 84 | } 85 | 86 | public protocol APICacheTool { 87 | // MARK: - 异步 88 | 89 | func set(forKey key: String, data: APICachePackage, writeMode: APICacheWriteMode, expiry: APICacheExpiry, completion: ((Result) -> Void)?) 90 | 91 | /// 如果缓存找不到,缓存过期都走错误分支 92 | /// 使用 APICacheError 93 | func getValidObject(byKey key: String, completion: @escaping (Result) -> Void) 94 | 95 | // MARK: - 同步 96 | 97 | func set(forKey key: String, data: APICachePackage, writeMode: APICacheWriteMode, expiry: APICacheExpiry) throws 98 | 99 | /// 如果缓存找不到、缓存过期都扔出错误 100 | /// 使用 APICacheError 101 | func getValidObject(byKey key: String) throws -> APICachePackage 102 | } 103 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIClient+Alamofire.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API+Alamofire.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2022/3/24. 6 | // 7 | 8 | import Alamofire 9 | import Foundation 10 | 11 | // MARK: - 别名 12 | 13 | /// Method 14 | public typealias APIRequestMethod = HTTPMethod 15 | /// Header 16 | public typealias APIRequestHeaders = HTTPHeaders 17 | /// APIDataResponse 18 | public typealias APIDataResponse = AFDataResponse 19 | /// APIDownloadResponse 20 | public typealias APIDownloadResponse = AFDownloadResponse 21 | /// APIRequestAdapter 22 | public typealias APIRequestAdapter = RequestAdapter 23 | /// APIDownloadDestination 24 | public typealias APIDownloadDestination = DownloadRequest.Destination 25 | 26 | /// APIMultipartFormData 27 | public typealias APIMultipartFormData = MultipartFormData 28 | /// APIParameterEncoding 29 | public typealias APIParameterEncoding = ParameterEncoding 30 | /// APIJSONEncoding 31 | public typealias APIJSONEncoding = JSONEncoding 32 | /// APIURLEncoding 33 | public typealias APIURLEncoding = URLEncoding 34 | /// APINetworkReachabilityManager 35 | public typealias APINetworkReachabilityManager = NetworkReachabilityManager 36 | 37 | /// APIDataResponseCompletionHandler 38 | public typealias APIDataResponseCompletionHandler = (APIDataResponse) -> Void 39 | /// APIDownloadResponseCompletionHandler 40 | public typealias APIDownloadResponseCompletionHandler = (APIDownloadResponse) -> Void 41 | 42 | extension Request: APIRequestTask {} 43 | 44 | // MARK: - AlamofireAPIClient 45 | 46 | struct AlamofireAPIClient: APIClient { 47 | let sessionManager: Session = { 48 | let sessionManager = Session(configuration: APIConfig.shared.urlSessionConfiguration, startRequestsImmediately: false) 49 | return sessionManager 50 | }() 51 | 52 | func createDataRequest( 53 | request: URLRequest, 54 | queue: DispatchQueue, 55 | progressHandler: APIProgressHandler?, 56 | completionHandler: @escaping APIDataResponseCompletionHandler 57 | ) -> APIRequestTask { 58 | let request = sessionManager.request(request).validate().responseData(queue: queue) { response in 59 | completionHandler(response) 60 | } 61 | if let tempProgressHandler = progressHandler { 62 | request.downloadProgress(closure: tempProgressHandler) 63 | } 64 | return request 65 | } 66 | 67 | func createDownloadRequest( 68 | request: URLRequest, 69 | to: @escaping APIDownloadDestination, 70 | queue: DispatchQueue, 71 | progressHandler: APIProgressHandler?, 72 | completionHandler: @escaping APIDownloadResponseCompletionHandler 73 | ) -> APIRequestTask { 74 | let request = sessionManager.download(request, to: to).validate().responseData(queue: queue) { response in 75 | completionHandler(response) 76 | } 77 | if let tempProgressHandler = progressHandler { 78 | request.downloadProgress(closure: tempProgressHandler) 79 | } 80 | return request 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2022/3/4. 6 | // 7 | 8 | import Foundation 9 | 10 | /// APIProgressHandler 11 | public typealias APIProgressHandler = (Progress) -> Void 12 | 13 | /// 网络请求任务协议 14 | public protocol APIRequestTask { 15 | /// 恢复 16 | @discardableResult 17 | func resume() -> Self 18 | 19 | /// 取消 20 | @discardableResult 21 | func cancel() -> Self 22 | } 23 | 24 | /// 网络请求客户端协议 25 | public protocol APIClient { 26 | /// 创建数据请求 27 | /// 28 | /// - Parameters: 29 | /// - request: 请求 30 | /// - progressHandler: 进度回调 31 | /// - completionHandler: 结果回调 32 | /// - Returns: 请求任务 33 | func createDataRequest( 34 | request: URLRequest, 35 | queue: DispatchQueue, 36 | progressHandler: APIProgressHandler?, 37 | completionHandler: @escaping APIDataResponseCompletionHandler 38 | ) -> APIRequestTask 39 | 40 | /// 创建下载请求 41 | /// 42 | /// - Parameters: 43 | /// - request: 请求 44 | /// - to: 设置下载的地址以及配置 45 | /// - progressHandler: 进度回调 46 | /// - completionHandler: 结果回调 47 | /// - Returns: 请求任务 48 | func createDownloadRequest( 49 | request: URLRequest, 50 | to: @escaping APIDownloadDestination, 51 | queue: DispatchQueue, 52 | progressHandler: APIProgressHandler?, 53 | completionHandler: @escaping APIDownloadResponseCompletionHandler 54 | ) -> APIRequestTask 55 | } 56 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIConfig.swift 3 | // CSAPIService 4 | // 5 | // Created by CoderStar on 2022/9/6. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class APIConfig { 11 | private init() {} 12 | 13 | public static let shared = APIConfig() 14 | 15 | // MARK: - 调试 16 | 17 | public var debugLogEnabled = false 18 | 19 | // MARK: - 网络 20 | 21 | /// URLSessionConfiguration 22 | /// 一次性赋值,有效时机在没有发任何请求之前 23 | public var urlSessionConfiguration: URLSessionConfiguration = { 24 | let configuration = URLSessionConfiguration.default 25 | configuration.timeoutIntervalForRequest = 20 26 | return configuration 27 | }() 28 | 29 | // MARK: - 缓存 30 | 31 | public var cacheTool: APICacheTool? 32 | 33 | /// 默认插件 34 | public var defaultPlugins: [APIPlugin] = [] 35 | } 36 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIError.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2022/3/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// APIError 11 | public enum APIError: LocalizedError { 12 | /// 网络不可用 13 | /// 目前是在发送请求前进行检查 14 | case networkError 15 | 16 | /// 发送错误 17 | case requestError(Error) 18 | 19 | /// 连接错误 20 | case connectionError(Error) 21 | 22 | /// 接收错误 23 | /// 解析等步骤 24 | case responseError(Error) 25 | 26 | /// 缓存错误 27 | case cache(Error) 28 | 29 | public var errorDescription: String? { 30 | switch self { 31 | case let .requestError(error): 32 | return "发出请求错误(\(error.localizedDescription))" 33 | case let .connectionError(error): 34 | return "请求错误(\(error.localizedDescription))" 35 | case let .responseError(error): 36 | return "结果处理错误(\(error.localizedDescription))" 37 | case .networkError: 38 | return "当前网络不可用" 39 | case let .cache(error): 40 | return "缓存错误(\(error.localizedDescription))" 41 | } 42 | } 43 | } 44 | 45 | extension APIError: Equatable { 46 | public static func == (lhs: APIError, rhs: APIError) -> Bool { 47 | return "\(lhs)" == "\(rhs)" 48 | } 49 | } 50 | 51 | /// APIRequestError 52 | public enum APIRequestError: LocalizedError { 53 | /// 不合理的请求链接 54 | case invalidURLRequest 55 | 56 | public var errorDescription: String? { 57 | switch self { 58 | case .invalidURLRequest: 59 | return "不合理的请求链接" 60 | } 61 | } 62 | } 63 | 64 | /// APIResponseError 65 | public enum APIResponseError: LocalizedError { 66 | /// 无法正确解析Response 67 | case invalidParseResponse(Error) 68 | 69 | public var errorDescription: String? { 70 | switch self { 71 | case let .invalidParseResponse(error): 72 | return error.localizedDescription 73 | } 74 | } 75 | } 76 | 77 | /// APICacheError 78 | public enum APICacheError: LocalizedError { 79 | /// 缓存过期 80 | case expire(key: String, data: Any) 81 | 82 | /// 缓存找不到 83 | case notFound(key: String) 84 | 85 | public var errorDescription: String? { 86 | switch self { 87 | case .expire: 88 | return "缓存过期" 89 | case .notFound: 90 | return "缓存找不到" 91 | } 92 | } 93 | } 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIParsable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIParsable.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2022/3/4. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - APIParsable 11 | 12 | /// 解析角色 13 | public protocol APIParsable { 14 | /// 将 Data 解析成 Model 15 | /// - Parameter data: data 16 | /// - Returns: Model 17 | static func parse(data: Data) throws -> Self 18 | } 19 | 20 | extension Data: APIParsable { 21 | /// Data默认实现解析协议 22 | /// 使请求时可以请求Data数据,业务方自己根据实际情况进行解析 23 | /// 24 | /// - Parameter data: data 25 | /// - Returns: 还是data 26 | public static func parse(data: Data) throws -> Self { 27 | return data 28 | } 29 | } 30 | 31 | // MARK: - APIJSONParsable 32 | 33 | /// JSON解析角色 34 | public protocol APIJSONParsable: APIParsable {} 35 | 36 | extension APIJSONParsable where Self: Decodable { 37 | /// APIJSONParsable 默认实现 38 | /// 使用 JSONDecoder 的方式 39 | /// 40 | /// - Parameter data: data 41 | /// - Returns: 解析结果 42 | public static func parse(data: Data) throws -> Self { 43 | do { 44 | let model = try JSONDecoder().decode(self, from: data) 45 | return model 46 | } catch { 47 | throw APIResponseError.invalidParseResponse(error) 48 | } 49 | } 50 | } 51 | 52 | /// 如果使用默认方式Decodable进行解析,最外层Model就可以直接实现该协议 53 | public typealias APIDefaultJSONParsable = APIJSONParsable & Decodable 54 | 55 | // MARK: - 默认实现 56 | 57 | extension String: APIParsable {} 58 | 59 | extension String: APIJSONParsable {} 60 | 61 | extension Array: APIParsable where Array.Element: APIDefaultJSONParsable {} 62 | 63 | extension Array: APIJSONParsable where Element: APIDefaultJSONParsable {} 64 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIPlugin.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2022/3/26. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 插件 11 | public protocol APIPlugin { 12 | /// 构造URLRequest 13 | /// 单个API的超时时间可在此进行设置 14 | func prepare(_ request: URLRequest, targetRequest: T) -> URLRequest 15 | 16 | /// 发送之前 17 | /// 返回值控制是否允许发送,true:允许发送(默认值),false:不允许发送 18 | func willSend(_ request: URLRequest, targetRequest: T) -> Bool 19 | 20 | /// 接收结果,时机在返回给调用方之前 21 | func willReceive(_ result: APIResponse, targetRequest: T) 22 | 23 | /// 接收结果,时机在返回给调用方之后 24 | func didReceive(_ result: APIResponse, targetRequest: T) 25 | } 26 | 27 | // MARK: - 默认实现 28 | 29 | extension APIPlugin { 30 | public func prepare(_ request: URLRequest, targetRequest: T) -> URLRequest { request } 31 | 32 | public func willSend(_ request: URLRequest, targetRequest: T) -> Bool { 33 | return true 34 | } 35 | 36 | public func willReceive(_ result: APIResponse, targetRequest: T) { } 37 | 38 | public func didReceive(_ result: APIResponse, targetRequest: T) {} 39 | } 40 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRequest.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2022/3/4. 6 | // 7 | 8 | import Foundation 9 | import CommonCrypto 10 | 11 | /// 任务类型 12 | public enum APIRequestTaskType { 13 | /// 请求 14 | case request 15 | /// 下载 16 | case download(APIDownloadDestination) 17 | } 18 | 19 | // MARK: - 请求协议 20 | 21 | /// 请求协议 22 | /// 每一个域名一个 23 | public protocol APIRequest { 24 | // MARK: - 回调实体 25 | 26 | /// 回调实体 27 | associatedtype Response: APIParsable 28 | 29 | // MARK: - 请求 30 | 31 | /// 基础地址 32 | var baseURL: URL { get } 33 | 34 | /// 接口路径 35 | var path: String { get } 36 | 37 | /// 方法 38 | var method: APIRequestMethod { get } 39 | 40 | /// 参数 41 | var parameters: [String: Any]? { get } 42 | 43 | /// header 44 | var headers: APIRequestHeaders? { get } 45 | 46 | /// 任务类型 47 | var taskType: APIRequestTaskType { get } 48 | 49 | /// 参数编码处理 50 | var encoding: APIParameterEncoding { get } 51 | 52 | // MARK: - 缓存相关 53 | 54 | /// 缓存 55 | /// 目前 taskType 为 request 才生效 56 | var cache: APICache? { get } 57 | 58 | /// 是否允许缓存 59 | /// 可根据业务实际情况控制:比如业务code为成功,业务数据不为空 60 | /// 这个闭包之所以不放入 APICache 内部的原因是 享受泛型的回调 61 | var cacheShouldWriteHandler: ((APIResponse) -> Bool)? { get } 62 | 63 | /// 过滤不参与缓存key生成的参数 64 | /// 如果一些业务场景不想统一参数参与缓存key生成,可在此配置 65 | var cacheFilterParameters: [String] { get } 66 | 67 | // MARK: - 方法 68 | 69 | /// 拦截参数,在参数编码之前 70 | /// 可以用于加上一些统一参数的场景 71 | /// 参数的相关操作最好都在这个位置进行处理,缓存key依赖于这个参数 72 | /// 73 | /// - Parameter parameters: 业务方传入的参数 74 | /// - Returns: 处理后的参数 75 | func intercept(parameters: [String: Any]?) -> [String: Any]? 76 | 77 | /// 拦截urlRequest,在传给client之前 78 | /// 可以用于添加统一Header等场景 79 | /// 80 | /// - Parameter urlRequest: 已经构造的 URLRequest 81 | /// - Returns: 处理之后的 URLRequest 82 | func intercept(urlRequest: URLRequest) throws -> URLRequest 83 | 84 | /// 拦截回调,在回调给接收方之前 85 | /// 86 | /// - Parameter request: 发送的URLRequest 87 | /// - Parameter response: 回调结果 88 | /// - Parameter replaceCompletionHandler: 替换返回给业务方的回调,如果不处理,将 response 回调即可 89 | /// - Returns: 处理之后的 URLRequest 90 | func intercept(request: U, response: APIResponse, replaceResponseHandler: @escaping APICompletionHandler) 91 | } 92 | 93 | // MARK: - 默认实现 94 | 95 | extension APIRequest { 96 | public var cache: APICache? { 97 | return nil 98 | } 99 | 100 | public var cacheShouldWriteHandler: ((APIResponse) -> Bool)? { 101 | return nil 102 | } 103 | 104 | public var cacheFilterParameters: [String] { 105 | return [] 106 | } 107 | 108 | public func intercept(parameters: [String: Any]?) -> [String: Any]? { 109 | return parameters 110 | } 111 | 112 | public func intercept(urlRequest: URLRequest) throws -> URLRequest { 113 | return urlRequest 114 | } 115 | 116 | public func intercept(request: U, response: APIResponse, replaceResponseHandler: @escaping APICompletionHandler) { 117 | replaceResponseHandler(response) 118 | } 119 | } 120 | 121 | extension APIRequest { 122 | /// 完整的URL 123 | /// 不包含参数,只是 baseURL 与 path 的拼接 124 | public var completeURL: URL { 125 | return path.isEmpty ? baseURL : baseURL.appendingPathComponent(path) 126 | } 127 | 128 | /// 最终的参数 129 | var resultParameters: [String: Any]? { 130 | return intercept(parameters: parameters) 131 | } 132 | 133 | /// 根据相关信息构造URLRequest 134 | func buildURLRequest() throws -> URLRequest { 135 | do { 136 | let originalRequest = try URLRequest(url: completeURL, method: method, headers: headers) 137 | let encodedURLRequest = try encoding.encode(originalRequest, with: resultParameters) 138 | return try intercept(urlRequest: encodedURLRequest) 139 | } catch { 140 | throw APIRequestError.invalidURLRequest 141 | } 142 | } 143 | } 144 | 145 | // MARK: - 缓存相关 146 | 147 | extension APIRequest { 148 | /// 缓存key 149 | var cacheKey: String { 150 | /// 参数字符串 151 | var paramString = "" 152 | if var resultParameters = resultParameters, !resultParameters.isEmpty { 153 | resultParameters = resultParameters.filter { !cacheFilterParameters.contains($0.key) } 154 | let paramKeys = resultParameters.keys.sorted() 155 | paramKeys.forEach { 156 | let value = resultParameters[$0] ?? "" 157 | paramString.append(contentsOf: "\(paramString.isEmpty ? "?" : "&")\($0)=\(value)") 158 | } 159 | } 160 | 161 | var cacheKey = "\(method.rawValue)-\(completeURL.absoluteString)\(paramString)" 162 | if let extraCacheKey = cache?.extraCacheKey, !extraCacheKey.isEmpty { 163 | cacheKey.append(contentsOf: "-\(extraCacheKey)") 164 | } 165 | 166 | if let customCacheKeyHandler = cache?.customCacheKeyHandler { 167 | return customCacheKeyHandler(cacheKey) 168 | } 169 | 170 | return cacheKey.md5 171 | } 172 | } 173 | 174 | extension String { 175 | /// md5 176 | fileprivate var md5: String { 177 | guard let data = data(using: .utf8) else { 178 | return self 179 | } 180 | var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) 181 | 182 | _ = data.withUnsafeBytes { buffer in 183 | CC_MD5(buffer.baseAddress, CC_LONG(buffer.count), &hash) 184 | } 185 | 186 | return hash.map { String(format: "%02x", $0) }.joined() 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIResponseModel.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2022/3/4. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 回调结果 11 | public struct APIResponse { 12 | /// 请求 13 | public var request: URLRequest? 14 | 15 | /// 回调 16 | public var response: HTTPURLResponse? 17 | 18 | /// 数据 19 | public var data: Data? 20 | 21 | /// 解析后数据 22 | public var result: APIResult 23 | 24 | /// 构造函数 25 | /// - Parameters: 26 | /// - request: request 27 | /// - response: response 28 | /// - data: data 29 | /// - result: result 30 | public init(request: URLRequest?, 31 | response: HTTPURLResponse?, 32 | data: Data?, 33 | result: APIResult) { 34 | self.request = request 35 | self.response = response 36 | self.data = data 37 | self.result = result 38 | } 39 | } 40 | 41 | extension APIResponse { 42 | /// 状态码 43 | public var statusCode: Int? { 44 | return response?.statusCode 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIResult.swift 3 | // CSAPIService 4 | // 5 | // Created by CoderStar on 2022/4/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 结果 11 | public enum APIResult { 12 | /// 成功 13 | case success(T) 14 | /// 失败 15 | case failure(APIError) 16 | } 17 | 18 | extension APIResult { 19 | /// 是否成功 20 | public var isSuccess: Bool { 21 | switch self { 22 | case .success: 23 | return true 24 | case .failure: 25 | return false 26 | } 27 | } 28 | 29 | /// 是否失败 30 | public var isFailure: Bool { 31 | return !isSuccess 32 | } 33 | 34 | /// 值 35 | public var value: T? { 36 | switch self { 37 | case let .success(value): 38 | return value 39 | case .failure: 40 | return nil 41 | } 42 | } 43 | 44 | /// 错误 45 | public var error: APIError? { 46 | switch self { 47 | case .success: 48 | return nil 49 | case .failure(let error): 50 | return error 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /APIService/Classes/Core/APIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIService.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2021/11/28. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 回调来源类型 11 | public enum APICompletionHandlerSourceType { 12 | /// 网络 13 | case network 14 | /// 缓存 15 | case cache 16 | } 17 | 18 | /// APICompletionSourceHandler 19 | public typealias APICompletionSourceHandler = (APIResponse, APICompletionHandlerSourceType) -> Void 20 | 21 | /// APICompletionHandler 22 | public typealias APICompletionHandler = (APIResponse) -> Void 23 | 24 | /// 请求命中缓存回调 25 | public typealias APICacheCompletionHandler = (APIResponse) -> Void 26 | 27 | /// 网络状态 28 | public enum NetworkStatus { 29 | /// 未知 30 | case unknown 31 | /// 不可用 32 | case notReachable 33 | /// wifi 34 | case wifi 35 | /// 数据 36 | case wwan 37 | } 38 | 39 | // MARK: - APIService 40 | 41 | /// API服务 42 | open class APIService { 43 | private let reachabilityManager = APINetworkReachabilityManager() 44 | 45 | /// 发送者 46 | public let client: APIClient 47 | 48 | /// 构造方法 49 | /// - Parameter client: 发送者实现 50 | public init(client: APIClient) { 51 | self.client = client 52 | } 53 | 54 | private static let `default` = APIService(client: AlamofireAPIClient()) 55 | } 56 | 57 | // MARK: - 公开属性 58 | 59 | extension APIService { 60 | /// 网络状态 61 | public var networkStatus: NetworkStatus { 62 | guard let status = reachabilityManager?.status else { 63 | return .unknown 64 | } 65 | switch status { 66 | case .unknown: 67 | return .unknown 68 | case .notReachable: 69 | return .notReachable 70 | case let .reachable(type): 71 | switch type { 72 | case .ethernetOrWiFi: 73 | return .wifi 74 | case .cellular: 75 | return .wwan 76 | } 77 | } 78 | } 79 | 80 | /// 网络是否可用 81 | public var isNetworkReachable: Bool { 82 | return networkStatus == .wifi || networkStatus == .wwan 83 | } 84 | } 85 | 86 | // MARK: - 公开方法 87 | 88 | extension APIService { 89 | /// 创建数据请求 90 | /// 这种方式使用为 Alamofire 作为底层实现 91 | /// 92 | /// - Parameters: 93 | /// - request: 请求 94 | /// - plugins: 插件 95 | /// - progressHandler: 进度回调 96 | /// - cacheHandler: 缓存回调 97 | /// - completionHandler: 网络回调 98 | /// - Returns: 请求任务 99 | @discardableResult 100 | public static func sendRequest( 101 | _ request: T, 102 | plugins: [APIPlugin] = [], 103 | queue: DispatchQueue = .main, 104 | progressHandler: APIProgressHandler? = nil, 105 | cacheHandler: APICacheCompletionHandler? = nil, 106 | completionHandler: APICompletionHandler? 107 | ) -> APIRequestTask? { 108 | `default`.sendRequest( 109 | request, 110 | plugins: plugins, 111 | queue: queue, 112 | progressHandler: progressHandler, 113 | cacheHandler: cacheHandler, 114 | completionHandler: completionHandler 115 | ) 116 | } 117 | 118 | /// 创建数据请求 119 | /// 这种方式使用为 Alamofire 作为底层实现 120 | /// 121 | /// - Parameters: 122 | /// - request: 请求 123 | /// - plugins: 插件 124 | /// - progressHandler: 进度回调 125 | /// - completionHandler: 结果回调,包含多种类型 126 | /// - Returns: 请求任务 127 | @discardableResult 128 | public static func sendRequest( 129 | _ request: T, 130 | plugins: [APIPlugin] = [], 131 | queue: DispatchQueue = .main, 132 | progressHandler: APIProgressHandler? = nil, 133 | completionHandler: APICompletionSourceHandler? 134 | ) -> APIRequestTask? { 135 | `default`.sendRequest( 136 | request, 137 | plugins: plugins, 138 | queue: queue, 139 | progressHandler: progressHandler, 140 | cacheHandler: { response in 141 | completionHandler?(response, .cache) 142 | }, 143 | completionHandler: { response in 144 | completionHandler?(response, .network) 145 | } 146 | ) 147 | } 148 | } 149 | 150 | extension APIService { 151 | /// 创建数据请求 152 | /// 153 | /// - Parameters: 154 | /// - request: 请求 155 | /// - plugins: 插件 156 | /// - progressHandler: 进度回调 157 | /// - cacheHandler: 命中缓存回调 158 | /// - completionHandler: 网络回调 159 | /// - Returns: 请求任务 160 | @discardableResult 161 | public func sendRequest( 162 | _ request: T, 163 | plugins: [APIPlugin] = [], 164 | queue: DispatchQueue = .main, 165 | progressHandler: APIProgressHandler? = nil, 166 | cacheHandler: APICacheCompletionHandler? = nil, 167 | completionHandler: APICompletionHandler? 168 | ) -> APIRequestTask? { 169 | var resultPlugins = APIConfig.shared.defaultPlugins 170 | resultPlugins.append(contentsOf: plugins) 171 | 172 | /// 构建 URLRequest 173 | var urlRequest: URLRequest 174 | do { 175 | /// Request拦截器:构建网络请求 176 | urlRequest = try request.buildURLRequest() 177 | 178 | /// 插件拦截器:构造网络请求 179 | urlRequest = resultPlugins.reduce(urlRequest) { $1.prepare($0, targetRequest: request) } 180 | } catch { 181 | let apiResult: APIResult = .failure(.requestError(error)) 182 | let apiResponse = APIResponse(request: nil, response: nil, data: nil, result: apiResult) 183 | queue.async { completionHandler?(apiResponse) } 184 | return nil 185 | } 186 | 187 | 188 | var resultCompletionHandler = completionHandler 189 | 190 | /// 检查缓存 191 | if let cache = request.cache, cache.usageMode != .none { 192 | if let cacheTool = APIConfig.shared.cacheTool { 193 | do { 194 | let cachePackage = try cacheTool.getValidObject(byKey: request.cacheKey) 195 | 196 | let responseModel = try T.Response.parse(data: cachePackage.data) 197 | let apiResult: APIResult = .success(responseModel) 198 | let apiResponse = APIResponse(request: urlRequest, response: nil, data: cachePackage.data, result: apiResult) 199 | queue.async { cacheHandler?(apiResponse) } 200 | 201 | DebugUtils.log("\(request)命中缓存,缓存key为:\(request.cacheKey)") 202 | 203 | if cache.usageMode == .cancelNetwork { 204 | return nil 205 | } 206 | 207 | if cache.usageMode == .alsoNetwork { 208 | resultCompletionHandler = nil 209 | } 210 | } catch { 211 | let apiResult: APIResult = .failure(.cache(error)) 212 | let apiResponse = APIResponse(request: nil, response: nil, data: nil, result: apiResult) 213 | queue.async { cacheHandler?(apiResponse) } 214 | } 215 | } else { 216 | assertionFailure("please set cacheStore in APIConfig") 217 | } 218 | } 219 | 220 | return sendRequest( 221 | urlRequest: urlRequest, 222 | request: request, 223 | resultPlugins: resultPlugins, 224 | queue: queue, 225 | progressHandler: progressHandler, 226 | completionHandler: resultCompletionHandler 227 | ) 228 | } 229 | 230 | /// 发起网络请求,不读取缓存 231 | private func sendRequest( 232 | urlRequest: URLRequest, 233 | request: T, 234 | resultPlugins: [APIPlugin], 235 | queue: DispatchQueue, 236 | progressHandler: APIProgressHandler?, 237 | completionHandler: APICompletionHandler? 238 | ) -> APIRequestTask? { 239 | /// 拦截器:即将发送网络请求 240 | /// 有一个插件不允许发送,则整体不允许发送 241 | var allowSend = true 242 | resultPlugins.forEach { 243 | let result = $0.willSend(urlRequest, targetRequest: request) 244 | allowSend = result && allowSend 245 | } 246 | 247 | if !allowSend { 248 | return nil 249 | } 250 | 251 | /// 检查网络可达 252 | if !isNetworkReachable { 253 | let apiResult: APIResult = .failure(.networkError) 254 | let apiResponse = APIResponse(request: nil, response: nil, data: nil, result: apiResult) 255 | 256 | /// 插件拦截器:即将回调给业务方 257 | resultPlugins.forEach { $0.willReceive(apiResponse, targetRequest: request) } 258 | 259 | queue.async { completionHandler?(apiResponse) } 260 | 261 | /// 插件拦截器:回调给业务方之后 262 | resultPlugins.forEach { $0.didReceive(apiResponse, targetRequest: request) } 263 | 264 | return nil 265 | } 266 | 267 | let requestTask: APIRequestTask 268 | 269 | switch request.taskType { 270 | case .request: 271 | requestTask = client.createDataRequest(request: urlRequest, queue: queue, progressHandler: progressHandler) { [weak self] response in 272 | let apiResult: APIResult 273 | switch response.result { 274 | case let .success(data): 275 | do { 276 | let responseModel = try T.Response.parse(data: data) 277 | apiResult = .success(responseModel) 278 | 279 | /// 缓存存储 280 | if let cache = request.cache, cache.writeNode != .none { 281 | if let cacheTool = APIConfig.shared.cacheTool { 282 | let cacheAPIResponse = APIResponse(request: response.request, response: response.response, data: response.data, result: apiResult) 283 | 284 | let allowCache = request.cacheShouldWriteHandler == nil || request.cacheShouldWriteHandler!(cacheAPIResponse) 285 | if allowCache { 286 | let cachePackage = APICachePackage(creationDate: Date(), data: data) 287 | cacheTool.set(forKey: request.cacheKey, data: cachePackage, writeMode: cache.writeNode, expiry: cache.expiry, completion: nil) 288 | 289 | DebugUtils.log("\(request)缓存写入,缓存key为:\(request.cacheKey)") 290 | } 291 | 292 | } else { 293 | assertionFailure("please set cacheStore in APIConfig") 294 | } 295 | } 296 | 297 | } catch { 298 | apiResult = .failure(.responseError(error)) 299 | } 300 | case let .failure(error): 301 | apiResult = .failure(.connectionError(error)) 302 | } 303 | 304 | let apiResponse = APIResponse(request: response.request, response: response.response, data: response.data, result: apiResult) 305 | self?.performData(request: request, response: apiResponse, plugins: resultPlugins, completionHandler: completionHandler) 306 | } 307 | case let .download(apiDownloadDestination): 308 | requestTask = client.createDownloadRequest(request: urlRequest, to: apiDownloadDestination, queue: queue, progressHandler: progressHandler) { [weak self] response in 309 | let apiResult: APIResult 310 | switch response.result { 311 | case let .success(data): 312 | do { 313 | let responseModel = try T.Response.parse(data: data) 314 | apiResult = .success(responseModel) 315 | } catch { 316 | apiResult = .failure(.responseError(error)) 317 | } 318 | case let .failure(error): 319 | apiResult = .failure(.connectionError(error)) 320 | } 321 | 322 | let apiResponse = APIResponse(request: response.request, response: response.response, data: response.value, result: apiResult) 323 | self?.performData(request: request, response: apiResponse, plugins: resultPlugins, completionHandler: completionHandler) 324 | } 325 | } 326 | 327 | requestTask.resume() 328 | 329 | return requestTask 330 | } 331 | } 332 | 333 | // MARK: - FormData Upload 334 | 335 | extension APIService {} 336 | 337 | // MARK: - 私有方法 338 | 339 | extension APIService { 340 | /// 回调数据给调用方 341 | /// 342 | /// - Parameters: 343 | /// - request: 请求 344 | /// - response: 上层回来的数据 345 | /// - result: 结果 346 | /// - plugins: 插件 347 | /// - completionHandler: 结果回调 348 | /// - Returns: 请求任务 349 | private func performData( 350 | request: T, 351 | response: APIResponse, 352 | plugins: [APIPlugin], 353 | completionHandler: APICompletionHandler? 354 | ) { 355 | /// 插件拦截器:即将回调给业务方 356 | plugins.forEach { $0.willReceive(response, targetRequest: request) } 357 | 358 | /// Request拦截器:在回调给业务方之前 359 | request.intercept(request: request, response: response) { replaceResponse in 360 | completionHandler?(replaceResponse) 361 | 362 | /// 插件拦截器:回调给业务方之后 363 | plugins.forEach { $0.didReceive(response, targetRequest: request) } 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /APIService/Classes/Core/DebugUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugUtils.swift 3 | // CSAPIService 4 | // 5 | // Created by CoderStar on 2022/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias LoggingFunctionType = (String) -> Void 11 | 12 | struct DebugUtils { 13 | public static var loggingFunction: LoggingFunctionType? { 14 | get { return _loggingFunction } 15 | set { _loggingFunction = newValue } 16 | } 17 | 18 | private static var _loggingFunction: LoggingFunctionType? = { print($0) } 19 | 20 | static func log(_ log: T, _ file: String = #file, _ function: String = #function, _ line: Int = #line) { 21 | if APIConfig.shared.debugLogEnabled { 22 | printLog(log, file: file, function: function, line: line) 23 | } 24 | } 25 | 26 | private static func printLog(_ log: T, file: String, function: String, line: Int) { 27 | let fileExtension = ((file as NSString).lastPathComponent as NSString).pathExtension // 文件名称 28 | let filename = ((file as NSString).lastPathComponent as NSString).deletingPathExtension // 文件扩展名 29 | let time = getCurrentTime() 30 | let informationPart = "\(time)-\(filename).\(fileExtension):\(line) \(function):" 31 | 32 | let message = "\(informationPart)\(log)" 33 | 34 | _loggingFunction?(message) 35 | } 36 | 37 | private static func getCurrentTime() -> String { 38 | let dateFormatter = DateFormatter() 39 | dateFormatter.calendar = Calendar.current 40 | dateFormatter.timeZone = TimeZone.current 41 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 42 | let dateString = dateFormatter.string(from: Date()) 43 | return dateString 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /APIService/Classes/Plugin/NetworkActivityPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkActivityPlugin.swift 3 | // CSAPIService 4 | // 5 | // Created by CoderStar on 2022/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum NetworkActivityChangeType { 11 | case began 12 | case ended 13 | } 14 | 15 | public struct NetworkActivityPlugin { 16 | public typealias NetworkActivityClosure = (_ change: NetworkActivityChangeType) -> Void 17 | let networkActivityClosure: NetworkActivityClosure 18 | 19 | public init(networkActivityClosure: @escaping NetworkActivityClosure) { 20 | self.networkActivityClosure = networkActivityClosure 21 | } 22 | } 23 | 24 | extension NetworkActivityPlugin: APIPlugin { 25 | public func willSend(_ request: URLRequest, targetRequest: T) -> Bool where T: APIRequest { 26 | networkActivityClosure(.began) 27 | return true 28 | } 29 | 30 | public func willReceive(_ result: APIResponse, targetRequest: T) where T: APIRequest { 31 | networkActivityClosure(.ended) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CSAPIService.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'CSAPIService' 3 | s.version = '0.1.0' 4 | s.summary = 'Swift 网络抽象层' 5 | s.description = <<-DESC 6 | Swift 网络抽象层,角色分明 7 | DESC 8 | 9 | s.homepage = 'https://github.com/Coder-Star/APIService' 10 | s.license = { :type => 'MIT', :file => 'LICENSE' } 11 | s.author = { 'CoderStar' => '1340529758@qq.com' } 12 | s.source = { :git => 'https://github.com/Coder-Star/APIService.git', :tag => s.version.to_s } 13 | 14 | s.swift_version = '5.0' 15 | s.module_name = 'APIService' 16 | s.ios.deployment_target = '11.0' 17 | 18 | # 第一层 19 | s.subspec 'Core' do |core| 20 | core.source_files = 'APIService/Classes/Core/**/*' 21 | core.dependency 'Alamofire','5.8.1' 22 | end 23 | 24 | # 第二层 25 | 26 | s.subspec 'Plugin' do |plugin| 27 | plugin.dependency 'CSAPIService/Core' 28 | plugin.source_files = 'APIService/Classes/Plugin/**/*' 29 | end 30 | 31 | s.subspec 'Cache' do |cache| 32 | cache.source_files = 'APIService/Classes/Cache/**/*' 33 | cache.dependency 'CSAPIService/Core' 34 | cache.dependency 'Cache','6.0.0' 35 | end 36 | 37 | 38 | 39 | end 40 | -------------------------------------------------------------------------------- /Documentation/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/Documentation/.gitkeep -------------------------------------------------------------------------------- /Example/.jazzy.yaml: -------------------------------------------------------------------------------- 1 | clean: true 2 | sdk: iphone 3 | author: CoderStar 4 | author_url: https://coder-star.github.io/ 5 | github_url: https://github.com/Coder-Star/APIService 6 | 7 | module: APIService # 模块名称 8 | module_version: 1.0.0 # 文档版本号 9 | 10 | readme: ../README.md # readme路径 11 | output: ../docs # 输出路径 12 | 13 | xcodebuild_arguments: [clean,build,-workspace,APIService.xcworkspace,-scheme,APIService-Example] 14 | 15 | min_acl: public # 最小权限 16 | 17 | theme: Doc/jazzy-theme 18 | 19 | 20 | -------------------------------------------------------------------------------- /Example/APIService.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/APIService.xcodeproj/xcshareddata/xcschemes/APIService-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Example/APIService.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/APIService.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/APIService/APIModelWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIModelWrapper.swift 3 | // LTXiOSUtils 4 | // 5 | // Created by CoderStar on 2022/3/27. 6 | // 7 | 8 | import Foundation 9 | import APIService 10 | 11 | /// 网络请求结果最外层Model 12 | public protocol APIModelWrapper { 13 | associatedtype DataType: APIDefaultJSONParsable 14 | 15 | var code: Int { get } 16 | 17 | var msg: String { get } 18 | 19 | var data: DataType? { get } 20 | } 21 | -------------------------------------------------------------------------------- /Example/APIService/APIResult+CS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIResult+CS.swift 3 | // APIService_Example 4 | // 5 | // Created by CoderStar on 2022/5/6. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import APIService 10 | 11 | public enum APIValidateResult { 12 | case success(T, String) 13 | case failure(String, APIError) 14 | } 15 | 16 | public enum CSDataError: Error { 17 | case invalidCode 18 | case invalidParseResponse 19 | } 20 | 21 | extension APIResult where T: APIModelWrapper { 22 | /// 业务校验器,可根据业务定制 23 | var validateResult: APIValidateResult { 24 | var message = "出现错误,请稍后重试" 25 | switch self { 26 | case let .success(response): 27 | if checkSuccessCode(code: response.code) { 28 | if let data = response.data { 29 | return .success(data, response.msg) 30 | } else { 31 | return .failure(message, APIError.responseError(CSDataError.invalidParseResponse)) 32 | } 33 | } else { 34 | return .failure(message, APIError.responseError(CSDataError.invalidCode)) 35 | } 36 | case let .failure(apiError): 37 | if apiError == APIError.networkError { 38 | message = apiError.localizedDescription 39 | } 40 | 41 | assertionFailure(apiError.localizedDescription) 42 | return .failure(message, apiError) 43 | } 44 | } 45 | 46 | var isSuccessCode: Bool { 47 | return checkSuccessCode(code: value?.code) 48 | } 49 | 50 | private func checkSuccessCode(code: Int?) -> Bool { 51 | return code == 200 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/APIService/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // APIService 4 | // 5 | // Created by CoderStar on 04/09/2022. 6 | // Copyright (c) 2022 CoderStar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SVProgressHUD 11 | import APIService 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | initLaunch() 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) {} 23 | 24 | func applicationDidEnterBackground(_ application: UIApplication) {} 25 | 26 | func applicationWillEnterForeground(_ application: UIApplication) {} 27 | 28 | func applicationDidBecomeActive(_ application: UIApplication) {} 29 | 30 | func applicationWillTerminate(_ application: UIApplication) {} 31 | } 32 | 33 | extension AppDelegate { 34 | private func initLaunch() { 35 | SVProgressHUD.setBackgroundColor(.black.withAlphaComponent(0.8)) 36 | SVProgressHUD.setForegroundColor(.white) 37 | SVProgressHUD.setMinimumDismissTimeInterval(0.8) 38 | SVProgressHUD.setMaximumDismissTimeInterval(1.6) 39 | SVProgressHUD.setMinimumSize(CGSize(width: 100, height: 40)) 40 | 41 | 42 | let configuration = URLSessionConfiguration.default 43 | configuration.timeoutIntervalForRequest = 100 44 | APIConfig.shared.urlSessionConfiguration = configuration 45 | APIConfig.shared.cacheTool = CacheTool.shared 46 | APIConfig.shared.debugLogEnabled = true 47 | 48 | debugPrint(NSHomeDirectory()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example/APIService/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/APIService/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/APIService/BetterCodaleExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BetterCodableExtensions.swift 3 | // APIService_Example 4 | // 5 | // Created by CoderStar on 2022/5/6. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import BetterCodable 11 | 12 | public typealias DefaultEmptyArray = DefaultCodable> where T: Decodable 13 | 14 | public typealias DefaultEmptyDictionary = DefaultCodable> where K: Decodable & Hashable, V: Decodable 15 | 16 | public typealias DefaultFalse = DefaultCodable 17 | 18 | public typealias DefaultTrue = DefaultCodable 19 | 20 | /// Double 21 | public struct DefaultDoubleZeroStrategy: DefaultCodableStrategy { 22 | public static var defaultValue: Double { 0.0 } 23 | } 24 | 25 | public typealias DefaultDoubleZero = DefaultCodable 26 | 27 | /// Float 28 | public struct DefaultFloatZeroStrategy: DefaultCodableStrategy { 29 | public static var defaultValue: Float { 0.0 } 30 | } 31 | 32 | public typealias DefaultFloatZero = DefaultCodable 33 | 34 | /// Int 35 | public struct DefaultIntZeroStrategy: DefaultCodableStrategy { 36 | public static var defaultValue: Int { 0 } 37 | } 38 | 39 | public typealias DefaultIntZero = DefaultCodable 40 | 41 | /// 空字符串 42 | public struct DefaultEmptyStringStrategy: DefaultCodableStrategy { 43 | public static var defaultValue: String { "" } 44 | } 45 | 46 | public typealias DefaultEmptyString = DefaultCodable 47 | -------------------------------------------------------------------------------- /Example/APIService/CSAPIRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSAPIRequest.swift 3 | // APIService_Example 4 | // 5 | // Created by CoderStar on 2022/11/6. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import APIService 10 | import Foundation 11 | 12 | class CSAPIRequest: APIRequest { 13 | typealias Response = CSBaseResponseModel 14 | 15 | var baseURL: URL { 16 | if isMock { 17 | return NetworkConstants.baseMockURL 18 | } 19 | switch NetworkConstants.env { 20 | case .prod: 21 | return NetworkConstants.baseProdURL 22 | case .dev: 23 | return NetworkConstants.baseDevURL 24 | } 25 | } 26 | 27 | var path: String { 28 | return "" 29 | } 30 | 31 | var method: APIRequestMethod { .get } 32 | 33 | var parameters: [String: Any]? { 34 | return nil 35 | } 36 | 37 | var headers: APIRequestHeaders? { 38 | return nil 39 | } 40 | 41 | var taskType: APIRequestTaskType { 42 | return .request 43 | } 44 | 45 | var encoding: APIParameterEncoding { 46 | return APIURLEncoding.default 47 | } 48 | 49 | public var cache: APICache? { 50 | return nil 51 | } 52 | 53 | public var cacheShouldWriteHandler: ((APIResponse) -> Bool)? { 54 | return nil 55 | } 56 | 57 | public var cacheFilterParameters: [String] { 58 | return [] 59 | } 60 | 61 | var isMock: Bool { 62 | return false 63 | } 64 | 65 | /// 验证码 66 | private var neteaseValidate = "" 67 | 68 | func intercept(parameters: [String: Any]?) -> [String: Any]? { 69 | if !neteaseValidate.isEmpty { 70 | var resultParameters = parameters ?? [:] 71 | resultParameters["neteaseValidate"] = neteaseValidate 72 | return resultParameters 73 | } else { 74 | return parameters 75 | } 76 | } 77 | 78 | func intercept(request: U, response: APIResponse, replaceResponseHandler: @escaping APICompletionHandler) { 79 | if response.result.value?.code == 1001, let csAPIRequest = request as? CSAPIRequest { 80 | csAPIRequest.neteaseValidate = "123" 81 | APIService.sendRequest(csAPIRequest, completionHandler: replaceResponseHandler) 82 | } else { 83 | replaceResponseHandler(response) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Example/APIService/CSAPIRequestProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSAPIRequest.swift 3 | // APIService_Example 4 | // 5 | // Created by CoderStar on 2022/4/29. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import APIService 10 | import Foundation 11 | 12 | protocol CSAPIRequestProtocol: APIRequest where Response == CSBaseResponseModel { 13 | associatedtype DataResponse: APIDefaultJSONParsable 14 | 15 | var isMock: Bool { get } 16 | } 17 | 18 | extension CSAPIRequestProtocol { 19 | var isMock: Bool { 20 | return false 21 | } 22 | 23 | var baseURL: URL { 24 | if isMock { 25 | return NetworkConstants.baseMockURL 26 | } 27 | switch NetworkConstants.env { 28 | case .prod: 29 | return NetworkConstants.baseProdURL 30 | case .dev: 31 | return NetworkConstants.baseDevURL 32 | } 33 | } 34 | 35 | var method: APIRequestMethod { .get } 36 | 37 | 38 | var parameters: [String: Any]? { 39 | return nil 40 | } 41 | 42 | var headers: APIRequestHeaders? { 43 | return nil 44 | } 45 | 46 | var taskType: APIRequestTaskType { 47 | return .request 48 | } 49 | 50 | var encoding: APIParameterEncoding { 51 | return APIURLEncoding.default 52 | } 53 | 54 | public func intercept(urlRequest: URLRequest) throws -> URLRequest { 55 | return urlRequest 56 | } 57 | 58 | public func intercept(request: U, response: APIResponse, replaceResponseHandler: @escaping APICompletionHandler) { 59 | replaceResponseHandler(response) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Example/APIService/CSBaseResponseModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DescBaseResponseModel.swift 3 | // LTXiOSUtilsDemo 4 | // 5 | // Created by CoderStar on 2022/3/16. 6 | // 7 | 8 | import APIService 9 | import Foundation 10 | 11 | public struct CSBaseResponseModel: APIModelWrapper, APIDefaultJSONParsable where T: APIDefaultJSONParsable { 12 | public var code: Int 13 | public var msg: String 14 | public var data: T? 15 | } 16 | 17 | extension CSBaseResponseModel { 18 | enum CodingKeys: String, CodingKey { 19 | case code 20 | case msg = "desc" 21 | case data 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/APIService/HomeBanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeBanner.swift 3 | // APIService_Example 4 | // 5 | // Created by CoderStar on 2022/5/6. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import APIService 10 | import BetterCodable 11 | import Foundation 12 | 13 | struct HomeBanner: APIDefaultJSONParsable { 14 | @DefaultIntZero 15 | var interval: Int 16 | 17 | @DefaultEmptyString 18 | var info: String 19 | 20 | @DefaultEmptyArray 21 | var imageList: [ImageList] 22 | } 23 | 24 | struct ImageList: Decodable { 25 | @DefaultEmptyString 26 | var imgUrl: String 27 | 28 | @DefaultEmptyString 29 | var actionUrl: String 30 | } 31 | 32 | struct LaunchAd: APIDefaultJSONParsable { 33 | @DefaultEmptyString 34 | var actionUrl: String = "" 35 | 36 | @DefaultIntZero 37 | var animationType: Int = 0 38 | 39 | @DefaultIntZero 40 | var duration: Int = 0 41 | 42 | @DefaultEmptyString 43 | var imgUrl: String = "" 44 | 45 | @DefaultIntZero 46 | var skipBtnType: Int = 0 47 | } 48 | -------------------------------------------------------------------------------- /Example/APIService/HomeBannerAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeBannerAPI.swift 3 | // APIService_Example 4 | // 5 | // Created by CoderStar on 2022/9/15. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import APIService 10 | import Foundation 11 | 12 | enum HomeBannerAPI { 13 | struct HomeBannerRequest: CSAPIRequestProtocol { 14 | typealias DataResponse = HomeBanner 15 | 16 | var parameters: [String: Any]? { 17 | return [ 18 | "param1": "张三", 19 | "param2": "18", 20 | ] 21 | } 22 | 23 | var path: String { 24 | return "/config/homeBanner" 25 | } 26 | 27 | var cache: APICache? { 28 | var cache = APICache() 29 | cache.usageMode = .alsoNetwork 30 | cache.writeNode = .memoryAndDisk 31 | cache.expiry = .seconds(10) 32 | return cache 33 | } 34 | 35 | var cacheFilterParameters: [String] { 36 | return ["param1"] 37 | } 38 | 39 | var cacheShouldWriteHandler: ((APIResponse>) -> Bool)? = { response in 40 | if response.result.isSuccessCode, response.result.value?.data != nil { 41 | return true 42 | } 43 | return false 44 | } 45 | } 46 | 47 | class LaunchAdRequest: CSAPIRequest { 48 | override var parameters: [String: Any]? { 49 | return [ 50 | "param1": "张三", 51 | "param2": "18", 52 | ] 53 | } 54 | 55 | override var path: String { 56 | return "/config/launchAd" 57 | } 58 | 59 | deinit { 60 | debugPrint("LaunchAdRequest deinit") 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Example/APIService/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/APIService/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | APIService 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/APIService/NetworkConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkConstants.swift 3 | // LTXiOSUtilsDemo 4 | // 5 | // Created by CoderStar on 2022/3/18. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Env { 11 | case dev 12 | case prod 13 | } 14 | 15 | struct NetworkConstants { 16 | static let baseProdURL = URL(string: "https://www.fastmock.site/mock/5abd18409d0a2270b34088a07457e68f/LTXMock")! 17 | 18 | // 不可用,显示使用 19 | static let baseDevURL = URL(string: "https://www.fastmock.site/mock/5abd18409d0a2270b34088a07457e68f/LTXMock1")! 20 | static let baseMockURL = URL(string: "https://www.fastmock.site/mock/5abd18409d0a2270b34088a07457e68f/LTXMock2")! 21 | 22 | static let env: Env = .prod 23 | } 24 | -------------------------------------------------------------------------------- /Example/APIService/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // APIService 4 | // 5 | // Created by CoderStar on 04/09/2022. 6 | // Copyright (c) 2022 CoderStar. All rights reserved. 7 | // 8 | 9 | import APIService 10 | import BetterCodable 11 | import Foundation 12 | import SVProgressHUD 13 | import UIKit 14 | 15 | class ViewController: UIViewController { 16 | lazy var networkActivityPlugin: NetworkActivityPlugin = { 17 | let networkActivityPlugin = NetworkActivityPlugin { change in 18 | switch change { 19 | case .began: 20 | SVProgressHUD.show() 21 | case .ended: 22 | SVProgressHUD.dismiss() 23 | } 24 | } 25 | return networkActivityPlugin 26 | }() 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | title = "首页" 31 | 32 | let rightBarButtonItem = UIBarButtonItem(title: "请求", style: .plain, target: self, action: #selector(getHomeBannerData)) 33 | navigationItem.rightBarButtonItem = rightBarButtonItem 34 | } 35 | 36 | @objc 37 | private func getHomeBannerData() { 38 | // let homeBannerRequest = HomeBannerAPI.HomeBannerRequest() 39 | // APIService.sendRequest(homeBannerRequest, plugins: [networkActivityPlugin], cacheHandler: { response in 40 | // debugPrint(response) 41 | // if response.result.isSuccess { 42 | // SVProgressHUD.showInfo(withStatus: "缓存") 43 | // } 44 | // }, completionHandler: { response in 45 | // switch response.result.validateResult { 46 | // case let .success(info, _): 47 | // SVProgressHUD.showInfo(withStatus: "网络结果") 48 | // debugPrint(info) 49 | // case let .failure(_, error): 50 | // debugPrint(error) 51 | // } 52 | // }) 53 | 54 | let launchAdRequest = HomeBannerAPI.LaunchAdRequest() 55 | 56 | APIService.sendRequest(launchAdRequest, plugins: [networkActivityPlugin]) { response, type in 57 | switch type { 58 | case .network: 59 | switch response.result.validateResult { 60 | case let .success(info, _): 61 | SVProgressHUD.showInfo(withStatus: "网络结果") 62 | debugPrint(info) 63 | case let .failure(_, error): 64 | debugPrint(error) 65 | } 66 | case .cache: 67 | debugPrint(response) 68 | if response.result.isSuccess { 69 | SVProgressHUD.showInfo(withStatus: "缓存") 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/assets/css/highlight.css.scss: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | 3 | .highlight { 4 | .c { color: #999988; font-style: italic } /* Comment */ 5 | .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 6 | .k { color: #000000; font-weight: bold } /* Keyword */ 7 | .o { color: #000000; font-weight: bold } /* Operator */ 8 | .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 9 | .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 10 | .c1 { color: #999988; font-style: italic } /* Comment.Single */ 11 | .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 12 | .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 13 | .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ 14 | .ge { color: #000000; font-style: italic } /* Generic.Emph */ 15 | .gr { color: #aa0000 } /* Generic.Error */ 16 | .gh { color: #999999 } /* Generic.Heading */ 17 | .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 18 | .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ 19 | .go { color: #888888 } /* Generic.Output */ 20 | .gp { color: #555555 } /* Generic.Prompt */ 21 | .gs { font-weight: bold } /* Generic.Strong */ 22 | .gu { color: #aaaaaa } /* Generic.Subheading */ 23 | .gt { color: #aa0000 } /* Generic.Traceback */ 24 | .kc { color: #000000; font-weight: bold } /* Keyword.Constant */ 25 | .kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ 26 | .kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ 27 | .kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ 28 | .kt { color: #445588; } /* Keyword.Type */ 29 | .m { color: #009999 } /* Literal.Number */ 30 | .s { color: #d14 } /* Literal.String */ 31 | .na { color: #008080 } /* Name.Attribute */ 32 | .nb { color: #0086B3 } /* Name.Builtin */ 33 | .nc { color: #445588; font-weight: bold } /* Name.Class */ 34 | .no { color: #008080 } /* Name.Constant */ 35 | .ni { color: #800080 } /* Name.Entity */ 36 | .ne { color: #990000; font-weight: bold } /* Name.Exception */ 37 | .nf { color: #990000; } /* Name.Function */ 38 | .nn { color: #555555 } /* Name.Namespace */ 39 | .nt { color: #000080 } /* Name.Tag */ 40 | .nv { color: #008080 } /* Name.Variable */ 41 | .ow { color: #000000; font-weight: bold } /* Operator.Word */ 42 | .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .mf { color: #009999 } /* Literal.Number.Float */ 44 | .mh { color: #009999 } /* Literal.Number.Hex */ 45 | .mi { color: #009999 } /* Literal.Number.Integer */ 46 | .mo { color: #009999 } /* Literal.Number.Oct */ 47 | .sb { color: #d14 } /* Literal.String.Backtick */ 48 | .sc { color: #d14 } /* Literal.String.Char */ 49 | .sd { color: #d14 } /* Literal.String.Doc */ 50 | .s2 { color: #d14 } /* Literal.String.Double */ 51 | .se { color: #d14 } /* Literal.String.Escape */ 52 | .sh { color: #d14 } /* Literal.String.Heredoc */ 53 | .si { color: #d14 } /* Literal.String.Interpol */ 54 | .sx { color: #d14 } /* Literal.String.Other */ 55 | .sr { color: #009926 } /* Literal.String.Regex */ 56 | .s1 { color: #d14 } /* Literal.String.Single */ 57 | .ss { color: #990073 } /* Literal.String.Symbol */ 58 | .bp { color: #999999 } /* Name.Builtin.Pseudo */ 59 | .vc { color: #008080 } /* Name.Variable.Class */ 60 | .vg { color: #008080 } /* Name.Variable.Global */ 61 | .vi { color: #008080 } /* Name.Variable.Instance */ 62 | .il { color: #009999 } /* Literal.Number.Integer.Long */ 63 | } 64 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/assets/css/jazzy.css.scss: -------------------------------------------------------------------------------- 1 | // =========================================================================== 2 | // 3 | // Variables 4 | // 5 | // =========================================================================== 6 | 7 | $body_background: #fff; 8 | $body_font: 16px/1.7 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | $text_color: #333; 10 | $gray_border: 1px solid #ddd; 11 | 12 | $heading_weight: 700; 13 | $light_heading_color: #777; 14 | 15 | $quote_color: #858585; 16 | $quote_border: 4px solid #e5e5e5; 17 | 18 | $link_color: #4183c4; 19 | 20 | $table_alt_row_color: #fbfbfb; 21 | $table_border_color: #ddd; 22 | 23 | $code_bg_color: #f7f7f7; 24 | $code_font: Consolas, "Liberation Mono", Menlo, Courier, monospace; 25 | 26 | 27 | // ----- Layout 28 | 29 | $gutter: 16px; 30 | $navigation_max_width: 300px; 31 | 32 | 33 | // ----- Header 34 | 35 | $header_bg_color: #444; 36 | $header_link_color: #fff; 37 | $doc_coverage_color: #999; 38 | 39 | 40 | // ----- Breadcrumbs 41 | 42 | $breadcrumbs_bg_color: #fbfbfb; 43 | $breadcrumbs_border_color: #ddd; 44 | 45 | 46 | // ----- Navigation 47 | 48 | $navigation_max_width: 300px; 49 | $navigation_bg_color: #fbfbfb; 50 | $navigation_border_color: #ddd; 51 | $navigation_title_color: #333; 52 | $navigation_task_color: #808080; 53 | 54 | // ----- Content 55 | 56 | $declaration_title_language_color: #4183c4; 57 | $declaration_language_border: 4px solid #cde9f4; 58 | $declaration_bg_color: #fff; 59 | $declaration_border_color: #ddd; 60 | 61 | $aside_color: #aaa; 62 | $aside_border: 4px solid lighten($aside_color, 20%); 63 | $aside_warning_color: #ff0000; 64 | $aside_warning_border: 4px solid lighten($aside_warning_color, 20%); 65 | 66 | // ----- Footer 67 | 68 | $footer_bg_color: #444; 69 | $footer_text_color: #ddd; 70 | $footer_link_color: #fff; 71 | 72 | // ----- Theme customisation 73 | 74 | $link_color: #CA3522; 75 | $doc_coverage_color: #ccc; 76 | $declaration_title_language_color: #CA3522; 77 | $declaration_language_border: 4px solid #CA3522; 78 | 79 | $footer_link_color: #FB9C44; 80 | $declaration_border: 4px solid $declaration_border_color; 81 | 82 | 83 | // =========================================================================== 84 | // 85 | // Base 86 | // 87 | // =========================================================================== 88 | 89 | *, *:before, *:after { 90 | box-sizing: inherit; 91 | } 92 | 93 | body { 94 | margin: 0; 95 | background: $body_background; 96 | color: $text_color; 97 | font: $body_font; 98 | letter-spacing: .2px; 99 | -webkit-font-smoothing: antialiased; 100 | box-sizing: border-box; 101 | } 102 | 103 | // ----- Block elements 104 | 105 | @mixin heading($font-size: 1rem, $margin: 1.275em 0 0.85em) { 106 | font-size: $font-size; 107 | font-weight: $heading_weight; 108 | margin: $margin; 109 | } 110 | 111 | h1 { 112 | @include heading(2rem, 1.275em 0 0.6em); 113 | } 114 | 115 | h2 { 116 | @include heading(1.75rem, 1.275em 0 0.3em); 117 | } 118 | 119 | h3 { 120 | @include heading(1.5rem, 1em 0 0.3em); 121 | } 122 | 123 | h4 { 124 | @include heading(1.25rem); 125 | } 126 | 127 | h5 { 128 | @include heading; 129 | } 130 | 131 | h6 { 132 | @include heading; 133 | color: $light_heading_color; 134 | } 135 | 136 | p { 137 | margin: 0 0 1em; 138 | } 139 | 140 | ul, ol { 141 | padding: 0 0 0 2em; 142 | margin: 0 0 0.85em; 143 | } 144 | 145 | blockquote { 146 | margin: 0 0 0.85em; 147 | padding: 0 15px; 148 | color: $quote_color; 149 | border-left: $quote_border; 150 | } 151 | 152 | 153 | // ----- Inline elements 154 | 155 | svg, img { 156 | max-width: 100%; 157 | } 158 | 159 | a { 160 | color: $link_color; 161 | text-decoration: none; 162 | 163 | &:hover, &:focus { 164 | outline: 0; 165 | text-decoration: underline; 166 | } 167 | } 168 | 169 | 170 | // ----- Tables 171 | 172 | table { 173 | background: $body_background; 174 | width: 100%; 175 | border-collapse: collapse; 176 | border-spacing: 0; 177 | overflow: auto; 178 | margin: 0 0 0.85em; 179 | } 180 | 181 | tr { 182 | &:nth-child(2n) { 183 | background-color: $table_alt_row_color; 184 | } 185 | } 186 | 187 | th, td { 188 | padding: 6px 13px; 189 | border: 1px solid $table_border_color; 190 | } 191 | 192 | 193 | // ----- Code 194 | 195 | pre { 196 | margin: 0 0 1.275em; 197 | padding: .85em 1em; 198 | overflow: auto; 199 | background: $code_bg_color; 200 | font-size: .85em; 201 | font-family: $code_font; 202 | } 203 | 204 | code { 205 | font-family: $code_font; 206 | } 207 | 208 | p, li { 209 | > code { 210 | background: $code_bg_color; 211 | padding: .2em; 212 | &:before, &:after { 213 | letter-spacing: -.2em; 214 | content: "\00a0"; 215 | } 216 | } 217 | } 218 | 219 | pre code { 220 | padding: 0; 221 | white-space: pre; 222 | } 223 | 224 | 225 | // =========================================================================== 226 | // 227 | // Layout 228 | // 229 | // =========================================================================== 230 | 231 | .content-wrapper { 232 | display: flex; 233 | flex-direction: column; 234 | @media (min-width: 768px) { 235 | flex-direction: row; 236 | } 237 | } 238 | 239 | 240 | // =========================================================================== 241 | // 242 | // Header 243 | // 244 | // =========================================================================== 245 | 246 | .header { 247 | display: flex; 248 | padding: $gutter/2; 249 | font-size: 0.875em; 250 | background: $header_bg_color; 251 | color: $doc_coverage_color; 252 | } 253 | 254 | .header-col { 255 | margin: 0; 256 | padding: 0 $gutter/2 257 | } 258 | 259 | .header-col--primary { 260 | flex: 1; 261 | } 262 | 263 | .header-link { 264 | color: $header_link_color; 265 | } 266 | 267 | .header-icon { 268 | padding-right: 6px; 269 | vertical-align: -4px; 270 | height: 16px; 271 | } 272 | 273 | 274 | 275 | // =========================================================================== 276 | // 277 | // Breadcrumbs 278 | // 279 | // =========================================================================== 280 | 281 | .breadcrumbs { 282 | font-size: 0.875em; 283 | padding: $gutter / 2 $gutter; 284 | margin: 0; 285 | background: $breadcrumbs_bg_color; 286 | border-bottom: 1px solid $breadcrumbs_border_color; 287 | } 288 | 289 | .carat { 290 | height: 10px; 291 | margin: 0 5px; 292 | } 293 | 294 | 295 | // =========================================================================== 296 | // 297 | // Navigation 298 | // 299 | // =========================================================================== 300 | 301 | .navigation { 302 | order: 2; 303 | 304 | @media (min-width: 768px) { 305 | order: 1; 306 | width: 25%; 307 | max-width: $navigation_max_width; 308 | padding-bottom: $gutter*4; 309 | overflow: hidden; 310 | word-wrap: normal; 311 | background: $navigation_bg_color; 312 | border-right: 1px solid $navigation_border_color; 313 | } 314 | } 315 | 316 | .nav-groups { 317 | list-style-type: none; 318 | padding-left: 0; 319 | } 320 | 321 | .nav-group-name { 322 | border-bottom: 1px solid $navigation_border_color; 323 | padding: $gutter/2 0 $gutter/2 $gutter; 324 | } 325 | 326 | .nav-group-name-link { 327 | color: $navigation_title_color; 328 | } 329 | 330 | .nav-group-tasks { 331 | margin: $gutter/2 0; 332 | padding: 0 0 0 $gutter/2; 333 | } 334 | 335 | .nav-group-task { 336 | font-size: 1em; 337 | list-style-type: none; 338 | } 339 | 340 | .nav-group-task-link { 341 | color: $navigation_task_color; 342 | } 343 | 344 | .current-page { 345 | color: $link_color; 346 | } 347 | 348 | // =========================================================================== 349 | // 350 | // Content 351 | // 352 | // =========================================================================== 353 | 354 | .main-content { 355 | order: 1; 356 | @media (min-width: 768px) { 357 | order: 2; 358 | flex: 1; 359 | padding-bottom: 60px; 360 | } 361 | } 362 | 363 | .section { 364 | padding: 0 $gutter * 2; 365 | border-bottom: 1px solid $navigation_border_color; 366 | } 367 | 368 | .section-content { 369 | max-width: 834px; 370 | margin: 0 auto; 371 | padding: $gutter 0; 372 | } 373 | 374 | .section-name { 375 | color: #666; 376 | display: block; 377 | } 378 | 379 | .declaration .highlight { 380 | overflow-x: initial; // This allows the scrollbar to show up inside declarations 381 | padding: $gutter/2 0; 382 | margin: 0; 383 | background-color: transparent; 384 | border: none; 385 | } 386 | 387 | .task-group-section { 388 | border-top: $gray_border; 389 | } 390 | 391 | .task-group { 392 | padding-top: 0px; 393 | } 394 | 395 | .task-name-container { 396 | a[name] { 397 | &:before { 398 | content: ""; 399 | display: block; 400 | } 401 | } 402 | } 403 | 404 | .item-container { 405 | padding: 0; 406 | } 407 | 408 | .item { 409 | padding-top: 8px; 410 | width: 100%; 411 | list-style-type: none; 412 | 413 | a[name] { 414 | &:before { 415 | content: ""; 416 | display: block; 417 | } 418 | } 419 | 420 | .token { 421 | padding-left: 3px; 422 | margin-left: 0px; 423 | font-size: 1.1rem; 424 | } 425 | 426 | .declaration-note { 427 | font-size: .85em; 428 | color: #808080; 429 | font-style: italic; 430 | } 431 | } 432 | 433 | .item-heading { 434 | padding: $gutter / 2 0; 435 | border-bottom: 1px solid #eee; 436 | 437 | position: relative; 438 | &:before { 439 | position: absolute; 440 | content: ''; 441 | width: 20px; 442 | height: 20px; 443 | left: -47px; 444 | top: 50%; 445 | margin-top: -10px; 446 | border-right: 4px solid #999; 447 | border-top: 4px solid #999; 448 | transform: rotate(45deg); 449 | transition: opacity 0.3s; 450 | opacity: 0; 451 | } 452 | } 453 | .item:hover .item-heading:before { 454 | opacity: 1; 455 | } 456 | 457 | .pointer-container, 458 | .pointer { 459 | display: none; 460 | } 461 | 462 | .height-container { 463 | .section { 464 | border: none; 465 | margin: $gutter / 2 $gutter / 4 $gutter * 2; 466 | padding: $gutter / 2 0; 467 | } 468 | } 469 | 470 | .aside, .language { 471 | padding: 6px 12px; 472 | margin: 12px 0; 473 | border-left: $aside_border; 474 | overflow-y: hidden; 475 | .aside-title { 476 | font-size: 9px; 477 | letter-spacing: 2px; 478 | text-transform: uppercase; 479 | padding-bottom: 0; 480 | margin: 0; 481 | color: $aside_color; 482 | -webkit-user-select: none; 483 | } 484 | p:last-child { 485 | margin-bottom: 0; 486 | } 487 | } 488 | 489 | .language { 490 | border-left: $declaration_language_border; 491 | .aside-title { 492 | color: $declaration_title_language_color; 493 | } 494 | } 495 | 496 | .aside-warning { 497 | border-left: $aside_warning_border; 498 | .aside-title { 499 | color: $aside_warning_color; 500 | } 501 | } 502 | 503 | .graybox { 504 | border-collapse: collapse; 505 | width: 100%; 506 | p { 507 | margin: 0; 508 | word-break: break-word; 509 | min-width: 50px; 510 | } 511 | td { 512 | border: $gray_border; 513 | padding: 5px 25px 5px 10px; 514 | vertical-align: middle; 515 | } 516 | tr td:first-of-type { 517 | text-align: right; 518 | padding: 7px; 519 | vertical-align: top; 520 | word-break: normal; 521 | width: 40px; 522 | } 523 | } 524 | 525 | .slightly-smaller { 526 | font-size: 0.9em; 527 | } 528 | 529 | .button { 530 | border: 1px solid #CA3522; 531 | display: inline-block; 532 | padding: 2px 10px; 533 | margin-right: 4px; 534 | transition: all 0.3s; 535 | &:hover, &:focus { 536 | text-decoration: none; 537 | border-color: #333; 538 | color: #333; 539 | } 540 | } 541 | 542 | 543 | // =========================================================================== 544 | // 545 | // Footer 546 | // 547 | // =========================================================================== 548 | 549 | .footer { 550 | padding: $gutter/2 $gutter; 551 | background: $footer_bg_color; 552 | color: $footer_text_color; 553 | font-size: 0.8em; 554 | 555 | p { 556 | margin: $gutter/2 0; 557 | } 558 | 559 | a { 560 | color: $footer_link_color; 561 | } 562 | } 563 | 564 | 565 | // =========================================================================== 566 | // 567 | // Dash 568 | // 569 | // =========================================================================== 570 | 571 | html.dash { 572 | 573 | .header, .breadcrumbs, .navigation, .footer { 574 | display: none; 575 | } 576 | 577 | .height-container { 578 | display: block; 579 | } 580 | } 581 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/assets/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/Example/Doc/jazzy-theme/assets/img/carat.png -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/assets/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/Example/Doc/jazzy-theme/assets/img/dash.png -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/assets/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/Example/Doc/jazzy-theme/assets/img/gh.png -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/assets/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false}; 2 | 3 | if (typeof window.dash != 'undefined') { 4 | document.documentElement.className += ' dash'; 5 | window.jazzy.docset = true; 6 | } 7 | 8 | if (navigator.userAgent.match(/xcode/i)) { 9 | document.documentElement.className += ' xcode'; 10 | window.jazzy.docset = true; 11 | } 12 | 13 | if (!window.jazzy.docset) { 14 | $(function() { 15 | var filename = window.location.pathname.split('/').pop(); 16 | $('a[href="Documentation.html"]').attr('href', 'index.html'); 17 | $('.navigation a[href$="/' + filename + '"]').addClass('current-page'); 18 | $('.navigation a[href="' + filename + '"]').addClass('current-page'); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/templates/doc.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{name}} {{kind}} Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{#dash_type}} 14 | 15 | {{/dash_type}} 16 | 17 | 18 | 19 | {{> header}} 20 | 21 | 26 | 27 |
28 | {{> nav}} 29 |
30 | 31 |
32 |
33 | {{^hide_name}}

{{name}}

{{/hide_name}} 34 | {{#declaration}} 35 |
36 |
37 | {{{declaration}}} 38 |
39 |
40 | {{/declaration}} 41 | {{{overview}}} 42 |
43 |
44 | 45 | {{> tasks}} 46 | 47 |
48 |
49 | {{> footer}} 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/templates/footer.mustache: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/templates/header.mustache: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | {{module_name}} Docs 5 | 6 | {{#doc_coverage}} ({{doc_coverage}}% documented){{/doc_coverage}} 7 |

8 | 9 | {{#github_url}} 10 |

11 | 12 | 13 | View on GitHub 14 | 15 |

16 | {{/github_url}} 17 | 18 | {{#dash_url}} 19 |

20 | 21 | 22 | Install in Dash 23 | 24 |

25 | {{/dash_url}} 26 |
27 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/templates/nav.mustache: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/templates/parameter.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{name}} 5 | 6 | 7 | 8 |
9 | {{{discussion}}} 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/templates/task.mustache: -------------------------------------------------------------------------------- 1 |
2 | {{#name}} 3 |
4 | 5 | 6 | 7 |

{{name}}

8 |
9 |
10 | {{/name}} 11 |
    12 | {{#items}} 13 |
  • 14 |
    15 | 16 | 17 | 18 | {{name}} 19 | 20 | {{#default_impl_abstract}} 21 | 22 | Default implementation 23 | 24 | {{/default_impl_abstract}} 25 | {{#from_protocol_extension}} 26 | 27 | Extension method 28 | 29 | {{/from_protocol_extension}} 30 |
    31 |
    32 |
    33 |
    34 |
    35 | {{#abstract}} 36 |
    37 | {{{abstract}}} 38 |
    39 | {{/abstract}} 40 | {{#default_impl_abstract}} 41 |
    Default Implementation
    42 |
    43 | {{{default_impl_abstract}}} 44 |
    45 | {{/default_impl_abstract}} 46 | {{#declaration}} 47 |
    48 |
    Declaration
    49 |
    50 |

    {{language}}

    51 | {{{declaration}}} 52 |
    53 |
    54 | {{/declaration}} 55 | {{#parameters.count}} 56 |
    57 |
    Parameters
    58 | 59 | 60 | {{#parameters}} 61 | {{> parameter}} 62 | {{/parameters}} 63 | 64 |
    65 |
    66 | {{/parameters.count}} 67 | {{#return}} 68 |
    69 |
    Return Value
    70 | {{{return}}} 71 |
    72 | {{/return}} 73 |
    74 | {{#abstract}}{{#url}} 75 | See more 76 | {{/url}}{{/abstract}} 77 | {{#github_token_url}} 78 | Show on GitHub 79 | {{/github_token_url}} 80 |
    81 |
    82 |
    83 |
  • 84 | {{/items}} 85 |
86 |
87 | -------------------------------------------------------------------------------- /Example/Doc/jazzy-theme/templates/tasks.mustache: -------------------------------------------------------------------------------- 1 | {{#tasks.count}} 2 |
3 |
4 | {{#tasks}} 5 | {{> task}} 6 | {{/tasks}} 7 |
8 |
9 | {{/tasks.count}} 10 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | 2 | install! 'cocoapods', :warn_for_unused_master_specs_repo => false 3 | use_frameworks! 4 | #inhibit_all_warnings! 5 | 6 | platform :ios, '11.0' 7 | 8 | target 'APIService_Example' do 9 | pod 'CSAPIService', :path => '../' 10 | pod 'BetterCodable', '0.4.0' 11 | pod 'SVProgressHUD', '2.2.5' 12 | end 13 | 14 | post_install do |installer| 15 | installer.pods_project.targets.each do |target| 16 | target.build_configurations.each do |config| 17 | # 消除编译警告 18 | if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 10.0 19 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0' 20 | end 21 | 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (5.8.1) 3 | - BetterCodable (0.4.0) 4 | - Cache (6.0.0) 5 | - CSAPIService (0.0.9): 6 | - CSAPIService/Cache (= 0.0.9) 7 | - CSAPIService/Core (= 0.0.9) 8 | - CSAPIService/Plugin (= 0.0.9) 9 | - CSAPIService/Cache (0.0.9): 10 | - Cache (= 6.0.0) 11 | - CSAPIService/Core 12 | - CSAPIService/Core (0.0.9): 13 | - Alamofire (= 5.8.1) 14 | - CSAPIService/Plugin (0.0.9): 15 | - CSAPIService/Core 16 | - SVProgressHUD (2.2.5) 17 | 18 | DEPENDENCIES: 19 | - BetterCodable (= 0.4.0) 20 | - CSAPIService (from `../`) 21 | - SVProgressHUD (= 2.2.5) 22 | 23 | SPEC REPOS: 24 | trunk: 25 | - Alamofire 26 | - BetterCodable 27 | - Cache 28 | - SVProgressHUD 29 | 30 | EXTERNAL SOURCES: 31 | CSAPIService: 32 | :path: "../" 33 | 34 | SPEC CHECKSUMS: 35 | Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7 36 | BetterCodable: 3f2a765c9f4089b19debd2a7b904a69fca0a5fda 37 | Cache: 4ca7e00363fca5455f26534e5607634c820ffc2d 38 | CSAPIService: 6031889eae8da5a6f15015722a4616f877fa98da 39 | SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 40 | 41 | PODFILE CHECKSUM: 62e45b7fc885a52c8f89dd91374dd9c00462aa1e 42 | 43 | COCOAPODS: 1.16.2 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 CoderStar <1340529758@qq.com> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSAPIService 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/CSAPIService.svg?style=flat)](https://cocoapods.org/pods/CSAPIService) 4 | [![License](https://img.shields.io/cocoapods/l/CSAPIService.svg?style=flat)](https://cocoapods.org/pods/CSAPIService) 5 | [![Platform](https://img.shields.io/cocoapods/p/CSAPIService.svg?style=flat)](https://cocoapods.org/pods/CSAPIService) 6 | [![Doc](https://img.shields.io/badge/doc-https%3A%2F%2Fcoder--star.github.io%2FAPIService%2F-lightgrey)](https://coder-star.github.io/APIService/) 7 | 8 | `CSAPIService` 是一个轻量的 Swift 网络抽象层框架,将请求、解析等流程工作分成几大角色去承担,完全面向协议实现,利于扩展。 9 | 10 | ## 使用方式 11 | 12 | ```ruby 13 | pod 'CSAPIService' 14 | ``` 15 | 16 | 其实原来的名称为`APIService`,但是因为该名在`CocoaPods`已经被占用了,就加了前缀,但是在使用时,模块名称依然是`APIService`。 17 | 18 | > 代码注释比较完备,部分细节可以直接查看代码。 19 | 20 | ## 特性 21 | 22 | - 层次清晰,按需引入; 23 | - 支持Request级别拦截器,并支持对响应进行异步替换; 24 | - 支持插件化拦截器; 25 | - 面向协议,角色清晰,方便功能扩展; 26 | - 缓存可拆卸,缓存包含内存、磁盘两级缓存; 27 | 28 | ## 框架组成 29 | 30 | ![APIService](./images/flow.png) 31 | > 箭头指的是发送流程,实心点指的是数据回调流程; 32 | > 高清图可见 [链接](https://www.processon.com/view/link/6274d2db1efad40df0236a83) 33 | 34 | 框架按照网络请求流程中涉及的步骤将其内部分为几个角色: 35 | 36 | - 请求者(APIRequest):更准确的叫法应该是**构建请求者**,其主要作用就是将外部传入的相关参数经过相关处理构造成一个`URLRequest`实例,并且提供请求拦截器以及回调拦截器两种拦截器; 37 | - 发送者(APIClient):实际的网络请求发送者,目前默认是`Alamofire`,你也可以实现协议,灵活的对发送者进行替换; 38 | - 解析者(APIParsable):一般与`Model`是同一个角色,由`Model`实现协议从而实现从数据到实体这一过程的映射; 39 | - 缓存(APICache、APICacheTool):缓存相关; 40 | - 服务提供者(APIService):整个框架的服务提供者,提供最外层的`API`,可以传入插件; 41 | 42 | 整个框架是按照`POP`的思想进行设计,将相关角色都尽量抽象成协议,方便扩展; 43 | 44 | ### APIRequest 协议 45 | 46 | 描述一个`URLRequest`实例需要的信息,并提供相应的拦截服务,在构造中我们可以设置返回的`ResponseModel`类型; 47 | 48 | 我们应当以`领域服务`为单位来提供对应的`APIRequest`,`领域服务`大部分会按照域名不同来划分,即 A 域名对应`AAPIRequest`,B 域名对应`BAPIRequest`。 49 | 50 | `APIRequest`的拦截器应用场景主要是整个`领域服务`级别的,一般添加的逻辑都是统一的逻辑。如: 51 | - 发送前加上统一参数,Header 等信息; 52 | - 数据回调到业务之前统一对一些 `code` 进行判断,如未登录自动弹出登录框等统一逻辑; 53 | 54 | ```swift 55 | /// 拦截参数,在参数编码之前 56 | func intercept(parameters: [String: Any]?) -> [String: Any]? 57 | 58 | /// 请求发送之前 59 | func intercept(urlRequest: URLRequest) throws -> URLRequest 60 | 61 | /// 数据回调给业务之前 62 | /// 利用 replaceResponseHandler 我们可以替换返回给业务的数据,还可以用作一些重试机制上等; 63 | /// 需要注意的是一旦实现该方法,需要及时使用 replaceResponseHandler 将 response 返回给业务方。 64 | func intercept(request: U, response: APIResponse, replaceResponseHandler: @escaping APICompletionHandler) 65 | ``` 66 | 67 | ### APICache 68 | 69 | `APICache`是一个`struct`,用来配置一个API请求时的**缓存**设置,相关设置包括: 70 | 71 | > 相关属性作用请看注释。 72 | 73 | ```swift 74 | public struct APICache { 75 | public init() { } 76 | 77 | /// 读取缓存模式 78 | public var usageMode: APICacheUsageMode = .none 79 | 80 | /// 写入缓存模式 81 | public var writeNode: APICacheWriteMode = .none 82 | 83 | /// 只有 writeNode 不为 .none 时,后面参数有效 84 | 85 | /// 额外的缓存key部分 86 | /// 可添加app版本号、用户id、缓存版本等 87 | public var extraCacheKey = "" 88 | 89 | /// 自定义缓存key 90 | public var customCacheKeyHandler: ((String) -> String)? 91 | 92 | /// 缓存过期策略类型 93 | public var expiry: APICacheExpiry = .seconds(0) 94 | } 95 | 96 | public protocol APIRequest { 97 | // MARK: - 缓存相关 98 | 99 | /// 缓存 100 | /// 目前 taskType 为 request 才生效 101 | var cache: APICache? { get } 102 | 103 | /// 是否允许缓存 104 | /// 可根据业务实际情况控制:比如业务code为成功,业务数据不为空 105 | /// 这个闭包之所以不放入 APICache 内部的原因是 享受泛型的回调 106 | var cacheShouldWriteHandler: ((APIResponse) -> Bool)? { get } 107 | 108 | /// 过滤不参与缓存key生成的参数 109 | /// 如果一些业务场景不想统一参数参与缓存key生成,可在此配置 110 | var cacheFilterParameters: [String] { get } 111 | } 112 | ``` 113 | 114 | 底层的缓存实现是可以通过设置`delegate`的方式进行替换。 115 | 116 | ```swift 117 | /// 可以通过调整 cacheTool 的实现来更换缓存底层实现 118 | APIConfig.shared.cacheTool = CacheTool.shared 119 | ``` 120 | 121 | 框架内部对于缓存的读写采用的是 **同步读,异步存** 的方式。 122 | 123 | 使用方式: 124 | ```swift 125 | 126 | enum HomeBannerAPI { 127 | struct HomeBannerRequest: CSAPIRequest { 128 | typealias DataResponse = HomeBanner 129 | 130 | var parameters: [String: Any]? { 131 | return nil 132 | } 133 | 134 | var path: String { 135 | return "/config/homeBanner" 136 | } 137 | 138 | /// 设置缓存 139 | var cache: APICache? { 140 | var cache = APICache() 141 | cache.readMode = .cancelNetwork 142 | cache.writeNode = .memoryAndDisk 143 | cache.expiry = .seconds(10) 144 | return cache 145 | } 146 | } 147 | } 148 | 149 | 150 | let request = HomeBannerAPI.HomeBannerRequest() 151 | APIService.sendRequest(request, plugins: [networkActivityPlugin], cacheHandler: { response in 152 | /// 缓存回调 153 | switch response.result.validateResult { 154 | case let .success(info, _): 155 | debugPrint(info) 156 | case let .failure(_, error): 157 | debugPrint(error) 158 | } 159 | }, completionHandler: { response in 160 | /// 网络回调 161 | switch response.result.validateResult { 162 | case let .success(info, _): 163 | debugPrint(info) 164 | case let .failure(_, error): 165 | debugPrint(error) 166 | } 167 | }) 168 | ``` 169 | 170 | 我们可以看到,`sendRequest` 为缓存单独加了一个回调,而不是和原来的`completionHandler`使用同一个,目的是想让业务方可以明确的感知到该次回调是来自网络还是缓存,也是呼应 `APICacheReadMode` 这个配置。 171 | 172 | ### APIClient 协议 173 | 174 | 负责发送一个`Request`请求,我们可以调整`APIClient`的实现方式; 目前默认实现方式为`Alamofire`,其中使用别名等方式做了隔离。 175 | 176 | ```swift 177 | /// 网络请求任务协议 178 | public protocol APIRequestTask { 179 | /// 恢复 180 | func resume() 181 | 182 | /// 取消 183 | func cancel() 184 | } 185 | 186 | /// 网络请求客户端协议 187 | public protocol APIClient { 188 | func createDataRequest( 189 | request: URLRequest, 190 | progressHandler: APIProgressHandler?, 191 | completionHandler: @escaping APIDataResponseCompletionHandler 192 | ) -> APIRequestTask 193 | 194 | func createDownloadRequest( 195 | request: URLRequest, 196 | to: @escaping APIDownloadDestination, 197 | progressHandler: APIProgressHandler?, 198 | completionHandler: @escaping APIDownloadResponseCompletionHandler 199 | ) -> APIRequestTask 200 | } 201 | ``` 202 | 203 | `APIClient` 协议比较简单,就是根据请求类型区分不同的方法,其返回值也是一个协议(`APIRequestTask`),支持`resume`以及`cancel`操作。 204 | 205 | > 目前操作只保留数据请求、下载请求两种方式,其他方式后续版本再补充; 206 | 207 | ### APIParsable 协议 208 | 209 | ```swift 210 | public protocol APIParsable { 211 | static func parse(data: Data) throws -> Self 212 | } 213 | ``` 214 | 215 | 如上所示,`APIParsable`协议其实很简单,实现者通常是`Model`,就是将`Data`类型的数据映射成实体类型。 216 | 217 | 这是最上层的协议,在该协议下方目前还有`APIJSONParsable`协议,其继承了`APIParsable`协议,如下所示: 218 | 219 | ```swift 220 | public protocol APIJSONParsable: APIParsable {} 221 | 222 | extension APIJSONParsable where Self: Decodable { 223 | public static func parse(data: Data) throws -> Self { 224 | do { 225 | let model = try JSONDecoder().decode(self, from: data) 226 | return model 227 | } catch { 228 | throw APIResponseError.invalidParseResponse(error) 229 | } 230 | } 231 | } 232 | ``` 233 | 234 | 目前协议的默认实现方式是通过`Decodable`的方式将`JSON`转为`Model`。 235 | 236 | 当然我们可以根据项目数据交换协议扩展对应的解析方式,如 XML、`Protobuf`等; 237 | 238 | ```swift 239 | public typealias APIDefaultJSONParsable = APIJSONParsable & Decodable 240 | ``` 241 | 242 | 同时为方便业务使用,添加了一个别名,如果使用默认方式 `Decodable` 进行解析,最外层 `Model` 就可以直接实现该协议。 243 | 244 | ### APIPlugin 245 | 246 | ```swift 247 | public protocol APIPlugin { 248 | /// 构造URLRequest 249 | func prepare(_ request: URLRequest, targetRequest: T) -> URLRequest 250 | 251 | /// 发送之前 252 | func willSend(_ request: URLRequest, targetRequest: T) 253 | 254 | /// 接收结果,时机在返回给调用方之前 255 | func willReceive(_ result: APIResponse, targetRequest: T) 256 | 257 | /// 接收结果,时机在返回给调用方之后 258 | func didReceive(_ result: APIResponse, targetRequest: T) 259 | } 260 | ``` 261 | 262 | 在具体网络请求层次上提供的拦截器协议,这样业务使用过程中可以感知到请求请求中的重要节点,从而完成一些逻辑,如`Loading`的加载与消失就可以通过构造一些对应的实例去完成。 263 | 264 | > 目前提供了一个默认的 `Plugin`,是: NetworkActivityPlugin 265 | 266 | ### APIService 267 | 268 | 这是最外层的供业务发起网络请求的`API`。 269 | 270 | ```swift 271 | open class APIService { 272 | private let reachabilityManager = APINetworkReachabilityManager() 273 | 274 | public let client: APIClient 275 | 276 | public init(client: APIClient) { 277 | self.client = client 278 | } 279 | 280 | /// 单例 281 | public static let `default` = APIService(client: AlamofireAPIClient()) 282 | } 283 | ``` 284 | 285 | `APIService`提供**类方法**以及**实例方法**,其中类方法就是使用的`default`实例,当然也可以其他的`APIClient`实现实例然后调用实例方法,等后续对底层实现进行替换是,只需要替换`default`实例的默认实现就可以了。 286 | 287 | ```swift 288 | public func sendRequest( 289 | _ request: T, 290 | plugins: [APIPlugin] = [], 291 | encoding: APIParameterEncoding? = nil, 292 | progressHandler: APIProgressHandler? = nil, 293 | completionHandler: @escaping APICompletionHandler 294 | ) -> APIRequestTask? { } 295 | ``` 296 | 297 | 实例方法定义如上,支持传入`APIPlugin` 实例数组。 298 | 299 | ## 业务使用实践 300 | 301 | > 相关代码在 Demo 工程里面可以看到。 302 | 303 | ### 最外层的 Model 304 | 305 | ```swift 306 | /// 网络请求结果最外层Model 307 | public protocol APIModelWrapper { 308 | associatedtype DataType: Decodable 309 | 310 | var code: Int { get } 311 | 312 | var msg: String { get } 313 | 314 | var data: DataType? { get } 315 | } 316 | ``` 317 | 318 | 对于大多数的网络请求而言,拿到的回调结果最外层肯定是最基础的 Model,也就是所谓的`BaseReponseBean`,其字段主要也是`code`、`msg`、`data`。 319 | 320 | 我们定义这样的一个协议方便后续使用,其实这个协议当时是直接放在库里面的,后来发现库内部对其没有依赖,其更多是一种更优的编程实践,就提出来放到了 Demo 工程里面去。 321 | 322 | 我们需要构造一个实体去实现该协议, 323 | 324 | ```swift 325 | public struct CSBaseResponseModel: APIModelWrapper, APIDefaultJSONParsable where T: Decodable { 326 | public var code: Int 327 | public var msg: String 328 | public var data: T? 329 | 330 | enum CodingKeys: String, CodingKey { 331 | case code 332 | case msg = "desc" 333 | case data 334 | } 335 | } 336 | ``` 337 | 338 | 因为最外层的 `CSBaseResponseModel` 已经满足了 `APIDefaultJSONParsable`协议,所以业务Model不需要再实现该协议了,而是直接实现`Decodable`就好。 339 | 340 | > 有的小伙伴可能会想不能直接使用实体吗?为什么还需要一个协议,这个协议在后面会用到。 341 | 342 | ### 业务 APIRequest 343 | 344 | ```swift 345 | /// 注意这个 CSBaseResponseModel 346 | protocol CSAPIRequest: APIRequest where Response == CSBaseResponseModel { 347 | associatedtype DataResponse: Decodable 348 | 349 | var isMock: Bool { get } 350 | } 351 | 352 | extension CSAPIRequest { 353 | var isMock: Bool { 354 | return false 355 | } 356 | 357 | var baseURL: URL { 358 | if isMock { 359 | return NetworkConstants.baseMockURL 360 | } 361 | switch NetworkConstants.env { 362 | case .prod: 363 | return NetworkConstants.baseProdURL 364 | case .dev: 365 | return NetworkConstants.baseDevURL 366 | } 367 | } 368 | 369 | var method: APIRequestMethod { .get } 370 | 371 | 372 | var parameters: [String: Any]? { 373 | return nil 374 | } 375 | 376 | var headers: APIRequestHeaders? { 377 | return nil 378 | } 379 | 380 | var taskType: APIRequestTaskType { 381 | return .request 382 | } 383 | 384 | var encoding: APIParameterEncoding { 385 | return APIURLEncoding.default 386 | } 387 | 388 | public func intercept(urlRequest: URLRequest) throws -> URLRequest { 389 | /// 我们可以在这个位置添加统一的参数、header的信息; 390 | return urlRequest 391 | } 392 | 393 | public func intercept(request: U, response: APIResponse, replaceResponseHandler: @escaping APICompletionHandler) { 394 | /// 我们在这里位置可以处理统一的回调判断相关逻辑 395 | replaceResponseHandler(response) 396 | } 397 | } 398 | ``` 399 | 400 | ### APIResult 扩展 401 | 402 | 我们通过`APIResult`最终获得的是最外层的`Model`,那对于大部分业务方而言,他们拿到数据后还会有一些通用逻辑,如: 403 | 404 | - 根据`code`值判断请求是否成功; 405 | - 错误本地化; 406 | - 获取实际的`data`数据; 407 | - ... 408 | 409 | 而这些逻辑在每一个`域名服务`又可能是不同的,属于业务逻辑,所以不宜放入库内部。 410 | 411 | 那对于这些逻辑,我们就可以对 `APIResult` 进行扩展,将这些逻辑收进去,业务方可以根据自己的需求决定在拿到`APIResult`之后是否还调用这个扩展。 412 | 413 | 如果有多种逻辑,可以考虑增加一些特定前缀去区别,如下面的`validateResult`我们可以扩展为多个 -- `csValidateResult`,`cfValidateResult`等等。 414 | 415 | ```swift 416 | public enum APIValidateResult { 417 | case success(T, String) 418 | case failure(String, APIError) 419 | } 420 | 421 | public enum CSDataError: Error { 422 | case invalidParseResponse 423 | } 424 | 425 | /// APIModelWrapper 在这个地方用到了 426 | extension APIResult where T: APIModelWrapper { 427 | var validateResult: APIValidateResult { 428 | var message = "出现错误,请稍后重试" 429 | switch self { 430 | case let .success(reponse): 431 | if reponse.code == 200, let data = reponse.data { 432 | return .success(data, reponse.msg) 433 | } else { 434 | return .failure(message, APIError.responseError(APIResponseError.invalidParseResponse(CSDataError.invalidParseResponse))) 435 | } 436 | case let .failure(apiError): 437 | if apiError == APIError.networkError { 438 | message = apiError.localizedDescription 439 | } 440 | 441 | assertionFailure(apiError.localizedDescription) 442 | return .failure(message, apiError) 443 | } 444 | } 445 | } 446 | 447 | ``` 448 | 449 | 450 | 451 | ### 业务使用 452 | 453 | 基础使用方式 454 | 455 | ```swift 456 | enum HomeBannerAPI { 457 | struct HomeBannerRequest: CSAPIRequest { 458 | typealias DataResponse = HomeBanner 459 | 460 | var parameters: [String: Any]? { 461 | return nil 462 | } 463 | 464 | var path: String { 465 | return "/config/homeBanner" 466 | } 467 | } 468 | } 469 | 470 | APIService.sendRequest(HomeBannerAPI.HomeBannerRequest()) { response in 471 | switch response.result.validateResult { 472 | case let .success(info, _): 473 | /// 这个 Info 就是上面我们传入的 HomeBanner 类型 474 | print(info) 475 | case let .failure(_, error): 476 | print(error) 477 | } 478 | } 479 | ``` 480 | 481 | > 项目示例中有两种使用形式,一种是 协议默认实现,一种是类继承,比较推荐协议默认实现,但是当有重试逻辑时,协议默认实现无法cover,增加了类继承方式; 482 | 483 | ## 未来规划 484 | 485 | - 重试机制 486 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /docs/Extensions/Data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Data Extension Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 20 | APIService Docs 21 | 22 | (90% documented) 23 |

24 | 25 |

26 | 27 | 28 | View on GitHub 29 | 30 |

31 | 32 |
33 | 34 | 39 | 40 |
41 | 219 |
220 | 221 |
222 |
223 |

Data

224 |
225 |
226 |
extension Data: APIParsable
227 | 228 |
229 |
230 | 231 |
232 |
233 | 234 |
235 |
236 |
237 |
    238 |
  • 239 |
    240 | 241 | 242 | 243 | parse(data:) 244 | 245 |
    246 |
    247 |
    248 |
    249 |
    250 |
    251 |

    Data默认实现解析协议 252 | 使请求时可以请求Data数据,业务方自己根据实际情况进行解析

    253 | 254 |
    255 |
    256 |
    Declaration
    257 |
    258 |

    Swift

    259 |
    public static func parse(data: Data) throws -> Data
    260 | 261 |
    262 |
    263 |
    264 |
    Parameters
    265 | 266 | 267 | 268 | 273 | 278 | 279 | 280 |
    269 | 270 | data 271 | 272 | 274 |
    275 |

    data

    276 |
    277 |
    281 |
    282 |
    283 |
    Return Value
    284 |

    还是data

    285 |
    286 |
    287 | 288 |
    289 |
    290 |
    291 |
  • 292 |
293 |
294 |
295 |
296 | 297 |
298 |
299 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /docs/Protocols/APIParsable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | APIParsable Protocol Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 20 | APIService Docs 21 | 22 | (90% documented) 23 |

24 | 25 |

26 | 27 | 28 | View on GitHub 29 | 30 |

31 | 32 |
33 | 34 | 39 | 40 |
41 | 219 |
220 | 221 |
222 |
223 |

APIParsable

224 |
225 |
226 |
public protocol APIParsable
227 | 228 |
229 |
230 |

解析角色

231 | 232 |
233 |
234 | 235 |
236 |
237 |
238 |
    239 |
  • 240 |
    241 | 242 | 243 | 244 | parse(data:) 245 | 246 |
    247 |
    248 |
    249 |
    250 |
    251 |
    252 |

    将 Data 解析成 Model

    253 | 254 |
    255 |
    256 |
    Declaration
    257 |
    258 |

    Swift

    259 |
    static func parse(data: Data) throws -> Self
    260 | 261 |
    262 |
    263 |
    264 |
    Parameters
    265 | 266 | 267 | 268 | 273 | 278 | 279 | 280 |
    269 | 270 | data 271 | 272 | 274 |
    275 |

    data

    276 |
    277 |
    281 |
    282 |
    283 |
    Return Value
    284 |

    Model

    285 |
    286 |
    287 | 288 |
    289 |
    290 |
    291 |
  • 292 |
293 |
294 |
295 |
296 | 297 |
298 |
299 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 90% 23 | 24 | 25 | 90% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; } 3 | 4 | body { 5 | margin: 0; 6 | background: #fff; 7 | color: #333; 8 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | letter-spacing: .2px; 10 | -webkit-font-smoothing: antialiased; 11 | box-sizing: border-box; } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | font-weight: 700; 16 | margin: 1.275em 0 0.6em; } 17 | 18 | h2 { 19 | font-size: 1.75rem; 20 | font-weight: 700; 21 | margin: 1.275em 0 0.3em; } 22 | 23 | h3 { 24 | font-size: 1.5rem; 25 | font-weight: 700; 26 | margin: 1em 0 0.3em; } 27 | 28 | h4 { 29 | font-size: 1.25rem; 30 | font-weight: 700; 31 | margin: 1.275em 0 0.85em; } 32 | 33 | h5 { 34 | font-size: 1rem; 35 | font-weight: 700; 36 | margin: 1.275em 0 0.85em; } 37 | 38 | h6 { 39 | font-size: 1rem; 40 | font-weight: 700; 41 | margin: 1.275em 0 0.85em; 42 | color: #777; } 43 | 44 | p { 45 | margin: 0 0 1em; } 46 | 47 | ul, ol { 48 | padding: 0 0 0 2em; 49 | margin: 0 0 0.85em; } 50 | 51 | blockquote { 52 | margin: 0 0 0.85em; 53 | padding: 0 15px; 54 | color: #858585; 55 | border-left: 4px solid #e5e5e5; } 56 | 57 | svg, img { 58 | max-width: 100%; } 59 | 60 | a { 61 | color: #CA3522; 62 | text-decoration: none; } 63 | a:hover, a:focus { 64 | outline: 0; 65 | text-decoration: underline; } 66 | 67 | table { 68 | background: #fff; 69 | width: 100%; 70 | border-collapse: collapse; 71 | border-spacing: 0; 72 | overflow: auto; 73 | margin: 0 0 0.85em; } 74 | 75 | tr:nth-child(2n) { 76 | background-color: #fbfbfb; } 77 | 78 | th, td { 79 | padding: 6px 13px; 80 | border: 1px solid #ddd; } 81 | 82 | pre { 83 | margin: 0 0 1.275em; 84 | padding: .85em 1em; 85 | overflow: auto; 86 | background: #f7f7f7; 87 | font-size: .85em; 88 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 89 | 90 | code { 91 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 92 | 93 | p > code, li > code { 94 | background: #f7f7f7; 95 | padding: .2em; } 96 | p > code:before, p > code:after, li > code:before, li > code:after { 97 | letter-spacing: -.2em; 98 | content: "\00a0"; } 99 | 100 | pre code { 101 | padding: 0; 102 | white-space: pre; } 103 | 104 | .content-wrapper { 105 | display: flex; 106 | flex-direction: column; } 107 | @media (min-width: 768px) { 108 | .content-wrapper { 109 | flex-direction: row; } } 110 | .header { 111 | display: flex; 112 | padding: 8px; 113 | font-size: 0.875em; 114 | background: #444; 115 | color: #ccc; } 116 | 117 | .header-col { 118 | margin: 0; 119 | padding: 0 8px; } 120 | 121 | .header-col--primary { 122 | flex: 1; } 123 | 124 | .header-link { 125 | color: #fff; } 126 | 127 | .header-icon { 128 | padding-right: 6px; 129 | vertical-align: -4px; 130 | height: 16px; } 131 | 132 | .breadcrumbs { 133 | font-size: 0.875em; 134 | padding: 8px 16px; 135 | margin: 0; 136 | background: #fbfbfb; 137 | border-bottom: 1px solid #ddd; } 138 | 139 | .carat { 140 | height: 10px; 141 | margin: 0 5px; } 142 | 143 | .navigation { 144 | order: 2; } 145 | @media (min-width: 768px) { 146 | .navigation { 147 | order: 1; 148 | width: 25%; 149 | max-width: 300px; 150 | padding-bottom: 64px; 151 | overflow: hidden; 152 | word-wrap: normal; 153 | background: #fbfbfb; 154 | border-right: 1px solid #ddd; } } 155 | .nav-groups { 156 | list-style-type: none; 157 | padding-left: 0; } 158 | 159 | .nav-group-name { 160 | border-bottom: 1px solid #ddd; 161 | padding: 8px 0 8px 16px; } 162 | 163 | .nav-group-name-link { 164 | color: #333; } 165 | 166 | .nav-group-tasks { 167 | margin: 8px 0; 168 | padding: 0 0 0 8px; } 169 | 170 | .nav-group-task { 171 | font-size: 1em; 172 | list-style-type: none; } 173 | 174 | .nav-group-task-link { 175 | color: #808080; } 176 | 177 | .current-page { 178 | color: #CA3522; } 179 | 180 | .main-content { 181 | order: 1; } 182 | @media (min-width: 768px) { 183 | .main-content { 184 | order: 2; 185 | flex: 1; 186 | padding-bottom: 60px; } } 187 | .section { 188 | padding: 0 32px; 189 | border-bottom: 1px solid #ddd; } 190 | 191 | .section-content { 192 | max-width: 834px; 193 | margin: 0 auto; 194 | padding: 16px 0; } 195 | 196 | .section-name { 197 | color: #666; 198 | display: block; } 199 | 200 | .declaration .highlight { 201 | overflow-x: initial; 202 | padding: 8px 0; 203 | margin: 0; 204 | background-color: transparent; 205 | border: none; } 206 | 207 | .task-group-section { 208 | border-top: 1px solid #ddd; } 209 | 210 | .task-group { 211 | padding-top: 0px; } 212 | 213 | .task-name-container a[name]:before { 214 | content: ""; 215 | display: block; } 216 | 217 | .item-container { 218 | padding: 0; } 219 | 220 | .item { 221 | padding-top: 8px; 222 | width: 100%; 223 | list-style-type: none; } 224 | .item a[name]:before { 225 | content: ""; 226 | display: block; } 227 | .item .token { 228 | padding-left: 3px; 229 | margin-left: 0px; 230 | font-size: 1.1rem; } 231 | .item .declaration-note { 232 | font-size: .85em; 233 | color: #808080; 234 | font-style: italic; } 235 | 236 | .item-heading { 237 | padding: 8px 0; 238 | border-bottom: 1px solid #eee; 239 | position: relative; } 240 | .item-heading:before { 241 | position: absolute; 242 | content: ''; 243 | width: 20px; 244 | height: 20px; 245 | left: -47px; 246 | top: 50%; 247 | margin-top: -10px; 248 | border-right: 4px solid #999; 249 | border-top: 4px solid #999; 250 | transform: rotate(45deg); 251 | transition: opacity 0.3s; 252 | opacity: 0; } 253 | 254 | .item:hover .item-heading:before { 255 | opacity: 1; } 256 | 257 | .pointer-container, 258 | .pointer { 259 | display: none; } 260 | 261 | .height-container .section { 262 | border: none; 263 | margin: 8px 4px 32px; 264 | padding: 8px 0; } 265 | 266 | .aside, .language { 267 | padding: 6px 12px; 268 | margin: 12px 0; 269 | border-left: 4px solid #dddddd; 270 | overflow-y: hidden; } 271 | .aside .aside-title, .language .aside-title { 272 | font-size: 9px; 273 | letter-spacing: 2px; 274 | text-transform: uppercase; 275 | padding-bottom: 0; 276 | margin: 0; 277 | color: #aaa; 278 | -webkit-user-select: none; } 279 | .aside p:last-child, .language p:last-child { 280 | margin-bottom: 0; } 281 | 282 | .language { 283 | border-left: 4px solid #CA3522; } 284 | .language .aside-title { 285 | color: #CA3522; } 286 | 287 | .aside-warning { 288 | border-left: 4px solid #ff6666; } 289 | .aside-warning .aside-title { 290 | color: #ff0000; } 291 | 292 | .graybox { 293 | border-collapse: collapse; 294 | width: 100%; } 295 | .graybox p { 296 | margin: 0; 297 | word-break: break-word; 298 | min-width: 50px; } 299 | .graybox td { 300 | border: 1px solid #ddd; 301 | padding: 5px 25px 5px 10px; 302 | vertical-align: middle; } 303 | .graybox tr td:first-of-type { 304 | text-align: right; 305 | padding: 7px; 306 | vertical-align: top; 307 | word-break: normal; 308 | width: 40px; } 309 | 310 | .slightly-smaller { 311 | font-size: 0.9em; } 312 | 313 | .button { 314 | border: 1px solid #CA3522; 315 | display: inline-block; 316 | padding: 2px 10px; 317 | margin-right: 4px; 318 | transition: all 0.3s; } 319 | .button:hover, .button:focus { 320 | text-decoration: none; 321 | border-color: #333; 322 | color: #333; } 323 | 324 | .footer { 325 | padding: 8px 16px; 326 | background: #444; 327 | color: #ddd; 328 | font-size: 0.8em; } 329 | .footer p { 330 | margin: 8px 0; } 331 | .footer a { 332 | color: #FB9C44; } 333 | 334 | html.dash .header, html.dash .breadcrumbs, html.dash .navigation, html.dash .footer { 335 | display: none; } 336 | 337 | html.dash .height-container { 338 | display: block; } 339 | -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.apiservice 7 | CFBundleName 8 | APIService 9 | DocSetPlatformFamily 10 | apiservice 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/Documents/Extensions/Data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Data Extension Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 20 | APIService Docs 21 | 22 | (90% documented) 23 |

24 | 25 |

26 | 27 | 28 | View on GitHub 29 | 30 |

31 | 32 |
33 | 34 | 39 | 40 |
41 | 219 |
220 | 221 |
222 |
223 |

Data

224 |
225 |
226 |
extension Data: APIParsable
227 | 228 |
229 |
230 | 231 |
232 |
233 | 234 |
235 |
236 |
237 |
    238 |
  • 239 |
    240 | 241 | 242 | 243 | parse(data:) 244 | 245 |
    246 |
    247 |
    248 |
    249 |
    250 |
    251 |

    Data默认实现解析协议 252 | 使请求时可以请求Data数据,业务方自己根据实际情况进行解析

    253 | 254 |
    255 |
    256 |
    Declaration
    257 |
    258 |

    Swift

    259 |
    public static func parse(data: Data) throws -> Data
    260 | 261 |
    262 |
    263 |
    264 |
    Parameters
    265 | 266 | 267 | 268 | 273 | 278 | 279 | 280 |
    269 | 270 | data 271 | 272 | 274 |
    275 |

    data

    276 |
    277 |
    281 |
    282 |
    283 |
    Return Value
    284 |

    还是data

    285 |
    286 |
    287 | 288 |
    289 |
    290 |
    291 |
  • 292 |
293 |
294 |
295 |
296 | 297 |
298 |
299 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/Documents/Protocols/APIParsable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | APIParsable Protocol Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 20 | APIService Docs 21 | 22 | (90% documented) 23 |

24 | 25 |

26 | 27 | 28 | View on GitHub 29 | 30 |

31 | 32 |
33 | 34 | 39 | 40 |
41 | 219 |
220 | 221 |
222 |
223 |

APIParsable

224 |
225 |
226 |
public protocol APIParsable
227 | 228 |
229 |
230 |

解析角色

231 | 232 |
233 |
234 | 235 |
236 |
237 |
238 |
    239 |
  • 240 |
    241 | 242 | 243 | 244 | parse(data:) 245 | 246 |
    247 |
    248 |
    249 |
    250 |
    251 |
    252 |

    将 Data 解析成 Model

    253 | 254 |
    255 |
    256 |
    Declaration
    257 |
    258 |

    Swift

    259 |
    static func parse(data: Data) throws -> Self
    260 | 261 |
    262 |
    263 |
    264 |
    Parameters
    265 | 266 | 267 | 268 | 273 | 278 | 279 | 280 |
    269 | 270 | data 271 | 272 | 274 |
    275 |

    data

    276 |
    277 |
    281 |
    282 |
    283 |
    Return Value
    284 |

    Model

    285 |
    286 |
    287 | 288 |
    289 |
    290 |
    291 |
  • 292 |
293 |
294 |
295 |
296 | 297 |
298 |
299 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/Documents/css/jazzy.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; } 3 | 4 | body { 5 | margin: 0; 6 | background: #fff; 7 | color: #333; 8 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | letter-spacing: .2px; 10 | -webkit-font-smoothing: antialiased; 11 | box-sizing: border-box; } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | font-weight: 700; 16 | margin: 1.275em 0 0.6em; } 17 | 18 | h2 { 19 | font-size: 1.75rem; 20 | font-weight: 700; 21 | margin: 1.275em 0 0.3em; } 22 | 23 | h3 { 24 | font-size: 1.5rem; 25 | font-weight: 700; 26 | margin: 1em 0 0.3em; } 27 | 28 | h4 { 29 | font-size: 1.25rem; 30 | font-weight: 700; 31 | margin: 1.275em 0 0.85em; } 32 | 33 | h5 { 34 | font-size: 1rem; 35 | font-weight: 700; 36 | margin: 1.275em 0 0.85em; } 37 | 38 | h6 { 39 | font-size: 1rem; 40 | font-weight: 700; 41 | margin: 1.275em 0 0.85em; 42 | color: #777; } 43 | 44 | p { 45 | margin: 0 0 1em; } 46 | 47 | ul, ol { 48 | padding: 0 0 0 2em; 49 | margin: 0 0 0.85em; } 50 | 51 | blockquote { 52 | margin: 0 0 0.85em; 53 | padding: 0 15px; 54 | color: #858585; 55 | border-left: 4px solid #e5e5e5; } 56 | 57 | svg, img { 58 | max-width: 100%; } 59 | 60 | a { 61 | color: #CA3522; 62 | text-decoration: none; } 63 | a:hover, a:focus { 64 | outline: 0; 65 | text-decoration: underline; } 66 | 67 | table { 68 | background: #fff; 69 | width: 100%; 70 | border-collapse: collapse; 71 | border-spacing: 0; 72 | overflow: auto; 73 | margin: 0 0 0.85em; } 74 | 75 | tr:nth-child(2n) { 76 | background-color: #fbfbfb; } 77 | 78 | th, td { 79 | padding: 6px 13px; 80 | border: 1px solid #ddd; } 81 | 82 | pre { 83 | margin: 0 0 1.275em; 84 | padding: .85em 1em; 85 | overflow: auto; 86 | background: #f7f7f7; 87 | font-size: .85em; 88 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 89 | 90 | code { 91 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 92 | 93 | p > code, li > code { 94 | background: #f7f7f7; 95 | padding: .2em; } 96 | p > code:before, p > code:after, li > code:before, li > code:after { 97 | letter-spacing: -.2em; 98 | content: "\00a0"; } 99 | 100 | pre code { 101 | padding: 0; 102 | white-space: pre; } 103 | 104 | .content-wrapper { 105 | display: flex; 106 | flex-direction: column; } 107 | @media (min-width: 768px) { 108 | .content-wrapper { 109 | flex-direction: row; } } 110 | .header { 111 | display: flex; 112 | padding: 8px; 113 | font-size: 0.875em; 114 | background: #444; 115 | color: #ccc; } 116 | 117 | .header-col { 118 | margin: 0; 119 | padding: 0 8px; } 120 | 121 | .header-col--primary { 122 | flex: 1; } 123 | 124 | .header-link { 125 | color: #fff; } 126 | 127 | .header-icon { 128 | padding-right: 6px; 129 | vertical-align: -4px; 130 | height: 16px; } 131 | 132 | .breadcrumbs { 133 | font-size: 0.875em; 134 | padding: 8px 16px; 135 | margin: 0; 136 | background: #fbfbfb; 137 | border-bottom: 1px solid #ddd; } 138 | 139 | .carat { 140 | height: 10px; 141 | margin: 0 5px; } 142 | 143 | .navigation { 144 | order: 2; } 145 | @media (min-width: 768px) { 146 | .navigation { 147 | order: 1; 148 | width: 25%; 149 | max-width: 300px; 150 | padding-bottom: 64px; 151 | overflow: hidden; 152 | word-wrap: normal; 153 | background: #fbfbfb; 154 | border-right: 1px solid #ddd; } } 155 | .nav-groups { 156 | list-style-type: none; 157 | padding-left: 0; } 158 | 159 | .nav-group-name { 160 | border-bottom: 1px solid #ddd; 161 | padding: 8px 0 8px 16px; } 162 | 163 | .nav-group-name-link { 164 | color: #333; } 165 | 166 | .nav-group-tasks { 167 | margin: 8px 0; 168 | padding: 0 0 0 8px; } 169 | 170 | .nav-group-task { 171 | font-size: 1em; 172 | list-style-type: none; } 173 | 174 | .nav-group-task-link { 175 | color: #808080; } 176 | 177 | .current-page { 178 | color: #CA3522; } 179 | 180 | .main-content { 181 | order: 1; } 182 | @media (min-width: 768px) { 183 | .main-content { 184 | order: 2; 185 | flex: 1; 186 | padding-bottom: 60px; } } 187 | .section { 188 | padding: 0 32px; 189 | border-bottom: 1px solid #ddd; } 190 | 191 | .section-content { 192 | max-width: 834px; 193 | margin: 0 auto; 194 | padding: 16px 0; } 195 | 196 | .section-name { 197 | color: #666; 198 | display: block; } 199 | 200 | .declaration .highlight { 201 | overflow-x: initial; 202 | padding: 8px 0; 203 | margin: 0; 204 | background-color: transparent; 205 | border: none; } 206 | 207 | .task-group-section { 208 | border-top: 1px solid #ddd; } 209 | 210 | .task-group { 211 | padding-top: 0px; } 212 | 213 | .task-name-container a[name]:before { 214 | content: ""; 215 | display: block; } 216 | 217 | .item-container { 218 | padding: 0; } 219 | 220 | .item { 221 | padding-top: 8px; 222 | width: 100%; 223 | list-style-type: none; } 224 | .item a[name]:before { 225 | content: ""; 226 | display: block; } 227 | .item .token { 228 | padding-left: 3px; 229 | margin-left: 0px; 230 | font-size: 1.1rem; } 231 | .item .declaration-note { 232 | font-size: .85em; 233 | color: #808080; 234 | font-style: italic; } 235 | 236 | .item-heading { 237 | padding: 8px 0; 238 | border-bottom: 1px solid #eee; 239 | position: relative; } 240 | .item-heading:before { 241 | position: absolute; 242 | content: ''; 243 | width: 20px; 244 | height: 20px; 245 | left: -47px; 246 | top: 50%; 247 | margin-top: -10px; 248 | border-right: 4px solid #999; 249 | border-top: 4px solid #999; 250 | transform: rotate(45deg); 251 | transition: opacity 0.3s; 252 | opacity: 0; } 253 | 254 | .item:hover .item-heading:before { 255 | opacity: 1; } 256 | 257 | .pointer-container, 258 | .pointer { 259 | display: none; } 260 | 261 | .height-container .section { 262 | border: none; 263 | margin: 8px 4px 32px; 264 | padding: 8px 0; } 265 | 266 | .aside, .language { 267 | padding: 6px 12px; 268 | margin: 12px 0; 269 | border-left: 4px solid #dddddd; 270 | overflow-y: hidden; } 271 | .aside .aside-title, .language .aside-title { 272 | font-size: 9px; 273 | letter-spacing: 2px; 274 | text-transform: uppercase; 275 | padding-bottom: 0; 276 | margin: 0; 277 | color: #aaa; 278 | -webkit-user-select: none; } 279 | .aside p:last-child, .language p:last-child { 280 | margin-bottom: 0; } 281 | 282 | .language { 283 | border-left: 4px solid #CA3522; } 284 | .language .aside-title { 285 | color: #CA3522; } 286 | 287 | .aside-warning { 288 | border-left: 4px solid #ff6666; } 289 | .aside-warning .aside-title { 290 | color: #ff0000; } 291 | 292 | .graybox { 293 | border-collapse: collapse; 294 | width: 100%; } 295 | .graybox p { 296 | margin: 0; 297 | word-break: break-word; 298 | min-width: 50px; } 299 | .graybox td { 300 | border: 1px solid #ddd; 301 | padding: 5px 25px 5px 10px; 302 | vertical-align: middle; } 303 | .graybox tr td:first-of-type { 304 | text-align: right; 305 | padding: 7px; 306 | vertical-align: top; 307 | word-break: normal; 308 | width: 40px; } 309 | 310 | .slightly-smaller { 311 | font-size: 0.9em; } 312 | 313 | .button { 314 | border: 1px solid #CA3522; 315 | display: inline-block; 316 | padding: 2px 10px; 317 | margin-right: 4px; 318 | transition: all 0.3s; } 319 | .button:hover, .button:focus { 320 | text-decoration: none; 321 | border-color: #333; 322 | color: #333; } 323 | 324 | .footer { 325 | padding: 8px 16px; 326 | background: #444; 327 | color: #ddd; 328 | font-size: 0.8em; } 329 | .footer p { 330 | margin: 8px 0; } 331 | .footer a { 332 | color: #FB9C44; } 333 | 334 | html.dash .header, html.dash .breadcrumbs, html.dash .navigation, html.dash .footer { 335 | display: none; } 336 | 337 | html.dash .height-container { 338 | display: block; } 339 | -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/docs/docsets/APIService.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/docs/docsets/APIService.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/docs/docsets/APIService.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false}; 2 | 3 | if (typeof window.dash != 'undefined') { 4 | document.documentElement.className += ' dash'; 5 | window.jazzy.docset = true; 6 | } 7 | 8 | if (navigator.userAgent.match(/xcode/i)) { 9 | document.documentElement.className += ' xcode'; 10 | window.jazzy.docset = true; 11 | } 12 | 13 | if (!window.jazzy.docset) { 14 | $(function() { 15 | var filename = window.location.pathname.split('/').pop(); 16 | $('a[href="Documentation.html"]').attr('href', 'index.html'); 17 | $('.navigation a[href$="/' + filename + '"]').addClass('current-page'); 18 | $('.navigation a[href="' + filename + '"]').addClass('current-page'); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /docs/docsets/APIService.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/docs/docsets/APIService.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/APIService.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/docs/docsets/APIService.tgz -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/docs/img/gh.png -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false}; 2 | 3 | if (typeof window.dash != 'undefined') { 4 | document.documentElement.className += ' dash'; 5 | window.jazzy.docset = true; 6 | } 7 | 8 | if (navigator.userAgent.match(/xcode/i)) { 9 | document.documentElement.className += ' xcode'; 10 | window.jazzy.docset = true; 11 | } 12 | 13 | if (!window.jazzy.docset) { 14 | $(function() { 15 | var filename = window.location.pathname.split('/').pop(); 16 | $('a[href="Documentation.html"]').attr('href', 'index.html'); 17 | $('.navigation a[href$="/' + filename + '"]').addClass('current-page'); 18 | $('.navigation a[href="' + filename + '"]').addClass('current-page'); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | { 4 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Cache/CacheTool.swift", 5 | "line": 119, 6 | "symbol": "CacheTool.set(forKey:data:writeMode:expiry:completion:)", 7 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 8 | "warning": "undocumented" 9 | }, 10 | { 11 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Cache/CacheTool.swift", 12 | "line": 166, 13 | "symbol": "CacheTool.set(forKey:data:writeMode:expiry:)", 14 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 15 | "warning": "undocumented" 16 | }, 17 | { 18 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Core/APICache.swift", 19 | "line": 51, 20 | "symbol": "APICache.init()", 21 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 22 | "warning": "undocumented" 23 | }, 24 | { 25 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Core/APICache.swift", 26 | "line": 82, 27 | "symbol": "APICacheTool", 28 | "symbol_kind": "source.lang.swift.decl.protocol", 29 | "warning": "undocumented" 30 | }, 31 | { 32 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Core/APICache.swift", 33 | "line": 85, 34 | "symbol": "APICacheTool.set(forKey:data:writeMode:expiry:completion:)", 35 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 36 | "warning": "undocumented" 37 | }, 38 | { 39 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Core/APICache.swift", 40 | "line": 93, 41 | "symbol": "APICacheTool.set(forKey:data:writeMode:expiry:)", 42 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 43 | "warning": "undocumented" 44 | }, 45 | { 46 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Core/APIConfig.swift", 47 | "line": 10, 48 | "symbol": "APIConfig", 49 | "symbol_kind": "source.lang.swift.decl.class", 50 | "warning": "undocumented" 51 | }, 52 | { 53 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Core/APIConfig.swift", 54 | "line": 13, 55 | "symbol": "APIConfig.shared", 56 | "symbol_kind": "source.lang.swift.decl.var.static", 57 | "warning": "undocumented" 58 | }, 59 | { 60 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Core/APIConfig.swift", 61 | "line": 17, 62 | "symbol": "APIConfig.debugLogEnabled", 63 | "symbol_kind": "source.lang.swift.decl.var.instance", 64 | "warning": "undocumented" 65 | }, 66 | { 67 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Core/APIConfig.swift", 68 | "line": 31, 69 | "symbol": "APIConfig.cacheTool", 70 | "symbol_kind": "source.lang.swift.decl.var.instance", 71 | "warning": "undocumented" 72 | }, 73 | { 74 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Plugin/NetworkActivityPlugin.swift", 75 | "line": 10, 76 | "symbol": "NetworkActivityChangeType", 77 | "symbol_kind": "source.lang.swift.decl.enum", 78 | "warning": "undocumented" 79 | }, 80 | { 81 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Plugin/NetworkActivityPlugin.swift", 82 | "line": 11, 83 | "symbol": "NetworkActivityChangeType.began", 84 | "symbol_kind": "source.lang.swift.decl.enumelement", 85 | "warning": "undocumented" 86 | }, 87 | { 88 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Plugin/NetworkActivityPlugin.swift", 89 | "line": 12, 90 | "symbol": "NetworkActivityChangeType.ended", 91 | "symbol_kind": "source.lang.swift.decl.enumelement", 92 | "warning": "undocumented" 93 | }, 94 | { 95 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Plugin/NetworkActivityPlugin.swift", 96 | "line": 15, 97 | "symbol": "NetworkActivityPlugin", 98 | "symbol_kind": "source.lang.swift.decl.struct", 99 | "warning": "undocumented" 100 | }, 101 | { 102 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Plugin/NetworkActivityPlugin.swift", 103 | "line": 16, 104 | "symbol": "NetworkActivityPlugin.NetworkActivityClosure", 105 | "symbol_kind": "source.lang.swift.decl.typealias", 106 | "warning": "undocumented" 107 | }, 108 | { 109 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Plugin/NetworkActivityPlugin.swift", 110 | "line": 19, 111 | "symbol": "NetworkActivityPlugin.init(networkActivityClosure:)", 112 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 113 | "warning": "undocumented" 114 | }, 115 | { 116 | "file": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/APIService/Classes/Plugin/NetworkActivityPlugin.swift", 117 | "line": 24, 118 | "symbol": "NetworkActivityPlugin", 119 | "symbol_kind": "source.lang.swift.decl.extension", 120 | "warning": "undocumented" 121 | } 122 | ], 123 | "source_directory": "/Users/coderstar/CoderStar/Projects/iOS_Projects/APIService/Example" 124 | } -------------------------------------------------------------------------------- /images/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Star/APIService/4dca6b3d7cae60408c52354a874fa279aaed777f/images/flow.png -------------------------------------------------------------------------------- /uploadPod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | POD_NAME="CSAPIService" 3 | # 新版本号 4 | NEW_VERSION_NUMBER=$1 5 | # 提交信息 6 | COMMIT_MESSAGE=$2 7 | 8 | CURRENT_PATH=$(cd `dirname $0`; pwd) 9 | cd ${CURRENT_PATH} 10 | 11 | if [ $NEW_VERSION_NUMBER ]; then 12 | echo ${NEW_VERSION_NUMBER} 13 | fi 14 | if [ $COMMIT_MESSAGE ]; then 15 | echo ${COMMIT_MESSAGE} 16 | fi 17 | 18 | 19 | VERSION_NUMBER=`grep -E 's.version.*=' ${POD_NAME}.podspec | awk -F \' '{print $2}'` 20 | 21 | LINE_NUMBER=`grep -nE 's.version.*=' ${POD_NAME}.podspec | cut -d : -f1` 22 | sed -i "" "${LINE_NUMBER}s/${VERSION_NUMBER}/${NEW_VERSION_NUMBER}/g" ${POD_NAME}.podspec 23 | 24 | git add . 25 | git commit -m ${COMMIT_MESSAGE} 26 | git tag ${NEW_VERSION_NUMBER} 27 | git push --tags 28 | 29 | pod trunk push ${POD_NAME}.podspec 30 | --------------------------------------------------------------------------------