protocol CustomHTTPProtocolDelegate: class {
+
func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve response: URLResponse)
func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve data: Data)
func customHTTPProtocolDidFinishLoading(_ proto: CustomHTTPProtocol)
func customHTTPProtocol(_ proto: CustomHTTPProtocol, didFailWithError error: Error)
}
-class CustomHTTPProtocol: URLProtocol {
- fileprivate static let requestProperty = "com.masakih.KCD.requestProperty"
- static var classDelegate: CustomHTTPProtocolDelegate? = nil
- fileprivate static let cachedExtensions = ["swf", "flv", "png", "jpg", "jpeg", "mp3"]
- fileprivate static let cacheFileURL: URL = ApplicationDirecrories.support.appendingPathComponent("Caches")
- fileprivate static let kcdURLCache = URLCache(memoryCapacity: 32 * 1024 * 1024,
- diskCapacity: 1024 * 1024 * 1024,
- diskPath: cacheFileURL.path)
-
- class func clearCache() { kcdURLCache.removeAllCachedResponses() }
- class func start() { URLProtocol.registerClass(CustomHTTPProtocol.self) }
+
+/// 生成されたスレッド上でoprationを実行する
+private final class ThreadOperator: NSObject {
+
+ private let thread: Thread
+ private let modes: [String]
+ private var operation: (() -> Void)?
+
+ override init() {
+
+ thread = Thread.current
+ if let mode = RunLoop.current.currentMode {
+
+ modes = [mode, .defaultRunLoopMode].map { $0.rawValue }
+
+ } else {
+
+ modes = [RunLoopMode.defaultRunLoopMode.rawValue]
+ }
+
+ super.init()
+ }
+
+ func execute(_ operation: @escaping () -> Void) {
+
+ self.operation = operation
+ perform(#selector(ThreadOperator.operate),
+ on: thread,
+ with: nil,
+ waitUntilDone: true,
+ modes: modes)
+ self.operation = nil
+ }
+
+ @objc private func operate() {
+
+ operation?()
+ }
+}
+
+extension HTTPURLResponse {
+
+ private var httpDateFormatter: DateFormatter {
+
+ let formatter = DateFormatter()
+ formatter.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
+ formatter.locale = Locale(identifier: "en_US")
+
+ return formatter
+ }
+
+ func expires() -> Date? {
+
+ if let cc = (allHeaderFields["Cache-Control"] as? String)?.lowercased(),
+ let range = cc.range(of: "max-age="),
+ let s = cc[range.upperBound...]
+ .components(separatedBy: ",")
+ .first,
+ let age = TimeInterval(s) {
+
+ return Date(timeIntervalSinceNow: age)
+ }
+
+ if let ex = (allHeaderFields["Expires"] as? String)?.lowercased(),
+ let exp = httpDateFormatter.date(from: ex) {
+
+ return exp
+ }
+
+ return nil
+ }
+}
+
+extension URLCache {
+
+ static let kcd = URLCache(memoryCapacity: 32 * 1024 * 1024,
+ diskCapacity: 1024 * 1024 * 1024,
+ diskPath: ApplicationDirecrories.support.appendingPathComponent("Caches").path)
+ static let cachedExtensions = ["swf", "flv", "png", "jpg", "jpeg", "mp3"]
+
+ func storeIfNeeded(for task: URLSessionTask, data: Data) {
+
+ if let request = task.originalRequest,
+ let response = task.response as? HTTPURLResponse,
+ let ext = request.url?.pathExtension,
+ URLCache.cachedExtensions.contains(ext),
+ let expires = response.expires() {
+
+ let cache = CachedURLResponse(response: response,
+ data: data,
+ userInfo: ["Expires": expires],
+ storagePolicy: .allowed)
+ storeCachedResponse(cache, for: request)
+ }
+ }
+
+ func validCach(for request: URLRequest) -> CachedURLResponse? {
+
+ if let cache = cachedResponse(for: request),
+ let info = cache.userInfo,
+ let expires = info["Expires"] as? Date,
+ Date().compare(expires) == .orderedAscending {
+
+ return cache
+ }
+
+ return nil
+ }
+}
+
+final class CustomHTTPProtocol: URLProtocol {
+
+ private static let requestProperty = "com.masakih.KCD.requestProperty"
+ static var classDelegate: CustomHTTPProtocolDelegate?
+
+ class func clearCache() {
+
+ URLCache.kcd.removeAllCachedResponses()
+ }
+
+ class func start() {
+
+ URLProtocol.registerClass(CustomHTTPProtocol.self)
+ }
override class func canInit(with request: URLRequest) -> Bool {
+
if let _ = property(forKey: requestProperty, in: request) { return false }
+
if let scheme = request.url?.scheme?.lowercased(),
- (scheme == "http" || scheme == "https")
- { return true }
+ (scheme == "http" || scheme == "https") {
+
+ return true
+ }
+
return false
}
+
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+
return request
}
- fileprivate var delegate: CustomHTTPProtocolDelegate? { return CustomHTTPProtocol.classDelegate }
+ private var delegate: CustomHTTPProtocolDelegate? { return CustomHTTPProtocol.classDelegate }
+
+ private var session: URLSession?
+ private var dataTask: URLSessionDataTask?
+ private var cachePolicy: URLCache.StoragePolicy = .notAllowed
+ private var data: Data = Data()
+ private var didRetry: Bool = false
+ private var didRecieveData: Bool = false
- fileprivate var session: URLSession? = nil
- fileprivate var dataTask: URLSessionDataTask? = nil
- fileprivate var cachePolicy: URLCache.StoragePolicy = .notAllowed
- fileprivate var data: Data = Data()
- fileprivate var didRetry: Bool = false
- fileprivate var didRecieveData: Bool = false
+ private var threadOperator: ThreadOperator?
private func use(_ cache: CachedURLResponse) {
+
delegate?.customHTTPProtocol(self, didRecieve: cache.response)
client?.urlProtocol(self, didReceive: cache.response, cacheStoragePolicy: .allowed)
delegate?.customHTTPProtocolDidFinishLoading(self)
client?.urlProtocolDidFinishLoading(self)
}
+
override func startLoading() {
- guard let newRequest = (request as NSObject).mutableCopy() as? NSMutableURLRequest
- else { fatalError("Can not convert to NSMutableURLRequest") }
- CustomHTTPProtocol.setProperty(true,
- forKey: CustomHTTPProtocol.requestProperty,
- in: newRequest)
- if let cache = CustomHTTPProtocol.kcdURLCache.cachedResponse(for: request),
- let info = cache.userInfo,
- let expires = info["Expires"] as? Date,
- Date().compare(expires) == .orderedAscending
- {
+ guard let newRequest = (request as NSObject).mutableCopy() as? NSMutableURLRequest else {
+
+ fatalError("Can not convert to NSMutableURLRequest")
+ }
+
+ URLProtocol.setProperty(true,
+ forKey: CustomHTTPProtocol.requestProperty,
+ in: newRequest)
+
+ if let cache = URLCache.kcd.validCach(for: request) {
+
use(cache)
- if let name = request.url?.lastPathComponent {
- Debug.print("Use cache for", name, level: .full)
- } else {
- Debug.print("Use cache", level: .full)
+
+ Debug.excute(level: .full) {
+
+ if let name = request.url?.lastPathComponent {
+
+ print("Use cache for", name)
+
+ } else {
+
+ print("Use cache")
+ }
}
return
}
+ threadOperator = ThreadOperator()
+
let config = URLSessionConfiguration.default
- config.protocolClasses = [type(of: self)]
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
dataTask = session?.dataTask(with: newRequest as URLRequest)
dataTask?.resume()
}
+
override func stopLoading() {
+
dataTask?.cancel()
}
}
extension CustomHTTPProtocol: URLSessionDataDelegate {
+
func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void) {
- Debug.print("willPerformHTTPRedirection", level: .full)
- client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
- completionHandler(request)
+
+ threadOperator?.execute { [weak self] in
+
+ guard let `self` = self else {
+
+ return
+ }
+
+ Debug.print("willPerformHTTPRedirection", level: .full)
+
+ self.client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
+
+ completionHandler(request)
+ }
}
+
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
- Debug.print("didReceive response", level: .full)
- if let response = response as? HTTPURLResponse,
- let request = dataTask.originalRequest {
- cachePolicy = CacheStoragePolicy(for: request, response: response)
+ threadOperator?.execute { [weak self] in
+
+ guard let `self` = self else {
+
+ return
+ }
+
+ Debug.print("didReceive response", level: .full)
+
+ if let response = response as? HTTPURLResponse,
+ let request = dataTask.originalRequest {
+
+ self.cachePolicy = cacheStoragePolicy(for: request, response: response)
+ }
+
+ self.delegate?.customHTTPProtocol(self, didRecieve: response)
+ self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: self.cachePolicy)
+
+ completionHandler(.allow)
}
-
- delegate?.customHTTPProtocol(self, didRecieve: response)
- client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: cachePolicy)
- completionHandler(.allow)
}
+
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
- Debug.print("didReceive data", level: .full)
- if cachePolicy == .allowed {
- self.data.append(data)
+
+ threadOperator?.execute { [weak self] in
+
+ guard let `self` = self else {
+
+ return
+ }
+
+ Debug.print("didReceive data", level: .full)
+ if self.cachePolicy == .allowed {
+
+ self.data.append(data)
+ }
+
+ self.delegate?.customHTTPProtocol(self, didRecieve: data)
+ self.client?.urlProtocol(self, didLoad: data)
+ self.didRecieveData = true
}
- delegate?.customHTTPProtocol(self, didRecieve: data)
- client?.urlProtocol(self, didLoad: data)
- didRecieveData = true
}
// cfurlErrorNetworkConnectionLost の場合はもう一度試す
private func canRetry(error: NSError) -> Bool {
+
guard error.code == Int(CFNetworkErrors.cfurlErrorNetworkConnectionLost.rawValue),
!didRetry,
- !didRecieveData
- else { return false }
+ !didRecieveData else {
+
+ return false
+ }
+
print("Retry download...")
+
return true
}
+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
- if let error = error {
- if canRetry(error: error as NSError),
- let request = task.originalRequest
- {
- didRetry = true
- dataTask = session.dataTask(with: request)
- dataTask?.resume()
+
+ threadOperator?.execute { [weak self] in
+
+ guard let `self` = self else {
+
return
}
- Debug.print("didCompleteWithError ERROR", level: .full)
- delegate?.customHTTPProtocol(self, didFailWithError: error)
- client?.urlProtocol(self, didFailWithError: error)
- return
- }
- Debug.print("didCompleteWithError SUCCESS", level: .full)
- delegate?.customHTTPProtocolDidFinishLoading(self)
- client?.urlProtocolDidFinishLoading(self)
-
- if cachePolicy == .allowed,
- let ext = task.originalRequest?.url?.pathExtension,
- CustomHTTPProtocol.cachedExtensions.contains(ext),
- let request = task.originalRequest,
- let response = task.response as? HTTPURLResponse,
- let expires = response.expires()
- {
- let cache = CachedURLResponse(response: response,
- data: data,
- userInfo: ["Expires": expires],
- storagePolicy: .allowed)
- CustomHTTPProtocol.kcdURLCache.storeCachedResponse(cache, for: request)
- }
- }
-}
-
-extension HTTPURLResponse {
- private var httpDateFormatter: DateFormatter {
- let formatter = DateFormatter()
- formatter.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
- formatter.locale = Locale(identifier: "en_US")
- return formatter
- }
- func expires() -> Date? {
- if let cc = (allHeaderFields["Cache-Control"] as? String)?.lowercased(),
- let range = cc.range(of: "max-age="),
- let s = cc[range.upperBound..<cc.endIndex]
- .components(separatedBy: ",")
- .first,
- let age = TimeInterval(s)
- {
- return Date(timeIntervalSinceNow: age)
- }
- if let ex = (allHeaderFields["Expires"] as? String)?.lowercased(),
- let exp = httpDateFormatter.date(from: ex)
- {
- return exp
+
+ if let error = error {
+
+ if self.canRetry(error: error as NSError),
+ let request = task.originalRequest {
+
+ self.didRetry = true
+ self.dataTask = session.dataTask(with: request)
+ self.dataTask?.resume()
+
+ return
+ }
+
+ Debug.print("didCompleteWithError ERROR", level: .full)
+
+ self.delegate?.customHTTPProtocol(self, didFailWithError: error)
+ self.client?.urlProtocol(self, didFailWithError: error)
+
+ return
+ }
+
+ Debug.print("didCompleteWithError SUCCESS", level: .full)
+
+ self.delegate?.customHTTPProtocolDidFinishLoading(self)
+ self.client?.urlProtocolDidFinishLoading(self)
+
+ if self.cachePolicy == .allowed {
+
+ URLCache.kcd.storeIfNeeded(for: task, data: self.data)
+ }
}
- return nil
}
}