2 // CustomHTTPProtocol.swift
5 // Created by Hori,Masaki on 2017/02/10.
6 // Copyright © 2017年 Hori,Masaki. All rights reserved.
12 protocol CustomHTTPProtocolDelegate: class {
13 func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve response: URLResponse)
14 func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve data: Data)
15 func customHTTPProtocolDidFinishLoading(_ proto: CustomHTTPProtocol)
16 func customHTTPProtocol(_ proto: CustomHTTPProtocol, didFailWithError error: Error)
19 fileprivate class ThreadOperator: NSObject {
20 private let thread: Thread
21 private let modes: [String]
22 private var operation: (() -> Void)?
25 thread = Thread.current
26 let mode = RunLoop.current.currentMode ?? .defaultRunLoopMode
27 if mode == .defaultRunLoopMode {
28 modes = [mode.rawValue]
30 modes = [mode, .defaultRunLoopMode].map { $0.rawValue }
34 func execute(_ operation: @escaping () -> Void) {
35 self.operation = operation
36 perform(#selector(ThreadOperator.operate),
48 extension HTTPURLResponse {
49 private var httpDateFormatter: DateFormatter {
50 let formatter = DateFormatter()
51 formatter.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
52 formatter.locale = Locale(identifier: "en_US")
55 func expires() -> Date? {
56 if let cc = (allHeaderFields["Cache-Control"] as? String)?.lowercased(),
57 let range = cc.range(of: "max-age="),
58 let s = cc[range.upperBound..<cc.endIndex]
59 .components(separatedBy: ",")
61 let age = TimeInterval(s) {
62 return Date(timeIntervalSinceNow: age)
64 if let ex = (allHeaderFields["Expires"] as? String)?.lowercased(),
65 let exp = httpDateFormatter.date(from: ex) {
73 static let kcd = URLCache(memoryCapacity: 32 * 1024 * 1024,
74 diskCapacity: 1024 * 1024 * 1024,
75 diskPath: ApplicationDirecrories.support.appendingPathComponent("Caches").path)
76 static let cachedExtensions = ["swf", "flv", "png", "jpg", "jpeg", "mp3"]
78 func storeIfNeeded(for task: URLSessionTask, data: Data) {
79 if let request = task.originalRequest,
80 let response = task.response as? HTTPURLResponse,
81 let ext = request.url?.pathExtension,
82 URLCache.cachedExtensions.contains(ext),
83 let expires = response.expires() {
84 let cache = CachedURLResponse(response: response,
86 userInfo: ["Expires": expires],
87 storagePolicy: .allowed)
88 storeCachedResponse(cache, for: request)
91 func validCach(for request: URLRequest) -> CachedURLResponse? {
92 if let cache = cachedResponse(for: request),
93 let info = cache.userInfo,
94 let expires = info["Expires"] as? Date,
95 Date().compare(expires) == .orderedAscending {
102 class CustomHTTPProtocol: URLProtocol {
103 fileprivate static let requestProperty = "com.masakih.KCD.requestProperty"
104 static var classDelegate: CustomHTTPProtocolDelegate?
106 class func clearCache() { URLCache.kcd.removeAllCachedResponses() }
107 class func start() { URLProtocol.registerClass(CustomHTTPProtocol.self) }
109 override class func canInit(with request: URLRequest) -> Bool {
110 if let _ = property(forKey: requestProperty, in: request) { return false }
111 if let scheme = request.url?.scheme?.lowercased(),
112 (scheme == "http" || scheme == "https") {
117 override class func canonicalRequest(for request: URLRequest) -> URLRequest {
121 fileprivate var delegate: CustomHTTPProtocolDelegate? { return CustomHTTPProtocol.classDelegate }
123 fileprivate var session: URLSession?
124 fileprivate var dataTask: URLSessionDataTask?
125 fileprivate var cachePolicy: URLCache.StoragePolicy = .notAllowed
126 fileprivate var data: Data = Data()
127 fileprivate var didRetry: Bool = false
128 fileprivate var didRecieveData: Bool = false
130 fileprivate var threadOperator: ThreadOperator?
132 private func use(_ cache: CachedURLResponse) {
133 delegate?.customHTTPProtocol(self, didRecieve: cache.response)
134 client?.urlProtocol(self, didReceive: cache.response, cacheStoragePolicy: .allowed)
136 delegate?.customHTTPProtocol(self, didRecieve: cache.data)
137 client?.urlProtocol(self, didLoad: cache.data)
139 delegate?.customHTTPProtocolDidFinishLoading(self)
140 client?.urlProtocolDidFinishLoading(self)
142 override func startLoading() {
143 guard let newRequest = (request as NSObject).mutableCopy() as? NSMutableURLRequest
144 else { fatalError("Can not convert to NSMutableURLRequest") }
145 URLProtocol.setProperty(true,
146 forKey: CustomHTTPProtocol.requestProperty,
149 if let cache = URLCache.kcd.validCach(for: request) {
151 if let name = request.url?.lastPathComponent {
152 Debug.print("Use cache for", name, level: .full)
154 Debug.print("Use cache", level: .full)
160 threadOperator = ThreadOperator()
162 let config = URLSessionConfiguration.default
163 session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
164 dataTask = session?.dataTask(with: newRequest as URLRequest)
167 override func stopLoading() {
172 extension CustomHTTPProtocol: URLSessionDataDelegate {
173 func urlSession(_ session: URLSession,
174 task: URLSessionTask,
175 willPerformHTTPRedirection response: HTTPURLResponse,
176 newRequest request: URLRequest,
177 completionHandler: @escaping (URLRequest?) -> Void) {
178 threadOperator?.execute { [weak self] in
179 guard let `self` = self else { return }
180 Debug.print("willPerformHTTPRedirection", level: .full)
181 self.client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
182 completionHandler(request)
185 func urlSession(_ session: URLSession,
186 dataTask: URLSessionDataTask,
187 didReceive response: URLResponse,
188 completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
189 threadOperator?.execute { [weak self] in
190 guard let `self` = self else { return }
191 Debug.print("didReceive response", level: .full)
193 if let response = response as? HTTPURLResponse,
194 let request = dataTask.originalRequest {
195 self.cachePolicy = CacheStoragePolicy(for: request, response: response)
198 self.delegate?.customHTTPProtocol(self, didRecieve: response)
199 self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: self.cachePolicy)
200 completionHandler(.allow)
203 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
204 threadOperator?.execute { [weak self] in
205 guard let `self` = self else { return }
206 Debug.print("didReceive data", level: .full)
207 if self.cachePolicy == .allowed {
208 self.data.append(data)
210 self.delegate?.customHTTPProtocol(self, didRecieve: data)
211 self.client?.urlProtocol(self, didLoad: data)
212 self.didRecieveData = true
216 // cfurlErrorNetworkConnectionLost の場合はもう一度試す
217 private func canRetry(error: NSError) -> Bool {
218 guard error.code == Int(CFNetworkErrors.cfurlErrorNetworkConnectionLost.rawValue),
221 else { return false }
222 print("Retry download...")
225 func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
226 threadOperator?.execute { [weak self] in
227 guard let `self` = self else { return }
228 if let error = error {
229 if self.canRetry(error: error as NSError),
230 let request = task.originalRequest {
232 self.dataTask = session.dataTask(with: request)
233 self.dataTask?.resume()
236 Debug.print("didCompleteWithError ERROR", level: .full)
237 self.delegate?.customHTTPProtocol(self, didFailWithError: error)
238 self.client?.urlProtocol(self, didFailWithError: error)
241 Debug.print("didCompleteWithError SUCCESS", level: .full)
242 self.delegate?.customHTTPProtocolDidFinishLoading(self)
243 self.client?.urlProtocolDidFinishLoading(self)
245 if self.cachePolicy == .allowed {
246 URLCache.kcd.storeIfNeeded(for: task, data: self.data)