2 // CustomHTTPProtocol.swift
5 // Created by Hori,Masaki on 2017/02/10.
6 // Copyright © 2017年 Hori,Masaki. All rights reserved.
12 protocol CustomHTTPProtocolDelegate: class {
14 func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve response: URLResponse)
15 func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve data: Data)
16 func customHTTPProtocolDidFinishLoading(_ proto: CustomHTTPProtocol)
17 func customHTTPProtocol(_ proto: CustomHTTPProtocol, didFailWithError error: Error)
20 private final class ThreadOperator: NSObject {
22 private let thread: Thread
23 private let modes: [String]
24 private var operation: (() -> Void)?
28 thread = Thread.current
29 let mode = RunLoop.current.currentMode ?? .defaultRunLoopMode
31 if mode == .defaultRunLoopMode {
33 modes = [mode.rawValue]
37 modes = [mode, .defaultRunLoopMode].map { $0.rawValue }
43 func execute(_ operation: @escaping () -> Void) {
45 self.operation = operation
46 perform(#selector(ThreadOperator.operate),
54 @objc func operate() {
60 extension HTTPURLResponse {
62 private var httpDateFormatter: DateFormatter {
64 let formatter = DateFormatter()
65 formatter.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
66 formatter.locale = Locale(identifier: "en_US")
71 func expires() -> Date? {
73 if let cc = (allHeaderFields["Cache-Control"] as? String)?.lowercased(),
74 let range = cc.range(of: "max-age="),
75 let s = cc[range.upperBound...]
76 .components(separatedBy: ",")
78 let age = TimeInterval(s) {
80 return Date(timeIntervalSinceNow: age)
83 if let ex = (allHeaderFields["Expires"] as? String)?.lowercased(),
84 let exp = httpDateFormatter.date(from: ex) {
95 static let kcd = URLCache(memoryCapacity: 32 * 1024 * 1024,
96 diskCapacity: 1024 * 1024 * 1024,
97 diskPath: ApplicationDirecrories.support.appendingPathComponent("Caches").path)
98 static let cachedExtensions = ["swf", "flv", "png", "jpg", "jpeg", "mp3"]
100 func storeIfNeeded(for task: URLSessionTask, data: Data) {
102 if let request = task.originalRequest,
103 let response = task.response as? HTTPURLResponse,
104 let ext = request.url?.pathExtension,
105 URLCache.cachedExtensions.contains(ext),
106 let expires = response.expires() {
108 let cache = CachedURLResponse(response: response,
110 userInfo: ["Expires": expires],
111 storagePolicy: .allowed)
112 storeCachedResponse(cache, for: request)
116 func validCach(for request: URLRequest) -> CachedURLResponse? {
118 if let cache = cachedResponse(for: request),
119 let info = cache.userInfo,
120 let expires = info["Expires"] as? Date,
121 Date().compare(expires) == .orderedAscending {
130 final class CustomHTTPProtocol: URLProtocol {
132 private static let requestProperty = "com.masakih.KCD.requestProperty"
133 static var classDelegate: CustomHTTPProtocolDelegate?
135 class func clearCache() { URLCache.kcd.removeAllCachedResponses() }
136 class func start() { URLProtocol.registerClass(CustomHTTPProtocol.self) }
138 override class func canInit(with request: URLRequest) -> Bool {
140 if let _ = property(forKey: requestProperty, in: request) { return false }
142 if let scheme = request.url?.scheme?.lowercased(),
143 (scheme == "http" || scheme == "https") {
151 override class func canonicalRequest(for request: URLRequest) -> URLRequest {
156 private var delegate: CustomHTTPProtocolDelegate? { return CustomHTTPProtocol.classDelegate }
158 private var session: URLSession?
159 private var dataTask: URLSessionDataTask?
160 private var cachePolicy: URLCache.StoragePolicy = .notAllowed
161 private var data: Data = Data()
162 private var didRetry: Bool = false
163 private var didRecieveData: Bool = false
165 private var threadOperator: ThreadOperator?
167 private func use(_ cache: CachedURLResponse) {
169 delegate?.customHTTPProtocol(self, didRecieve: cache.response)
170 client?.urlProtocol(self, didReceive: cache.response, cacheStoragePolicy: .allowed)
172 delegate?.customHTTPProtocol(self, didRecieve: cache.data)
173 client?.urlProtocol(self, didLoad: cache.data)
175 delegate?.customHTTPProtocolDidFinishLoading(self)
176 client?.urlProtocolDidFinishLoading(self)
179 override func startLoading() {
181 guard let newRequest = (request as NSObject).mutableCopy() as? NSMutableURLRequest else {
183 fatalError("Can not convert to NSMutableURLRequest")
186 URLProtocol.setProperty(true,
187 forKey: CustomHTTPProtocol.requestProperty,
190 if let cache = URLCache.kcd.validCach(for: request) {
194 Debug.excute(level: .full) {
196 if let name = request.url?.lastPathComponent {
198 print("Use cache for", name)
209 threadOperator = ThreadOperator()
211 let config = URLSessionConfiguration.default
212 session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
213 dataTask = session?.dataTask(with: newRequest as URLRequest)
217 override func stopLoading() {
223 extension CustomHTTPProtocol: URLSessionDataDelegate {
225 func urlSession(_ session: URLSession,
226 task: URLSessionTask,
227 willPerformHTTPRedirection response: HTTPURLResponse,
228 newRequest request: URLRequest,
229 completionHandler: @escaping (URLRequest?) -> Void) {
231 threadOperator?.execute { [weak self] in
233 guard let `self` = self else { return }
235 Debug.print("willPerformHTTPRedirection", level: .full)
237 self.client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
239 completionHandler(request)
243 func urlSession(_ session: URLSession,
244 dataTask: URLSessionDataTask,
245 didReceive response: URLResponse,
246 completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
248 threadOperator?.execute { [weak self] in
250 guard let `self` = self else { return }
252 Debug.print("didReceive response", level: .full)
254 if let response = response as? HTTPURLResponse,
255 let request = dataTask.originalRequest {
257 self.cachePolicy = cacheStoragePolicy(for: request, response: response)
260 self.delegate?.customHTTPProtocol(self, didRecieve: response)
261 self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: self.cachePolicy)
263 completionHandler(.allow)
267 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
269 threadOperator?.execute { [weak self] in
271 guard let `self` = self else { return }
273 Debug.print("didReceive data", level: .full)
274 if self.cachePolicy == .allowed {
276 self.data.append(data)
279 self.delegate?.customHTTPProtocol(self, didRecieve: data)
280 self.client?.urlProtocol(self, didLoad: data)
281 self.didRecieveData = true
285 // cfurlErrorNetworkConnectionLost の場合はもう一度試す
286 private func canRetry(error: NSError) -> Bool {
288 guard error.code == Int(CFNetworkErrors.cfurlErrorNetworkConnectionLost.rawValue),
290 !didRecieveData else {
295 print("Retry download...")
300 func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
302 threadOperator?.execute { [weak self] in
304 guard let `self` = self else { return }
306 if let error = error {
308 if self.canRetry(error: error as NSError),
309 let request = task.originalRequest {
312 self.dataTask = session.dataTask(with: request)
313 self.dataTask?.resume()
318 Debug.print("didCompleteWithError ERROR", level: .full)
320 self.delegate?.customHTTPProtocol(self, didFailWithError: error)
321 self.client?.urlProtocol(self, didFailWithError: error)
326 Debug.print("didCompleteWithError SUCCESS", level: .full)
328 self.delegate?.customHTTPProtocolDidFinishLoading(self)
329 self.client?.urlProtocolDidFinishLoading(self)
331 if self.cachePolicy == .allowed {
333 URLCache.kcd.storeIfNeeded(for: task, data: self.data)