123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- //
- // Cache.swift
- // Haneke
- //
- // Created by Luis Ascorbe on 23/07/14.
- // Copyright (c) 2014 Haneke. All rights reserved.
- //
- import UIKit
- // Used to add T to NSCache
- class ObjectWrapper : NSObject {
- let hnk_value: Any
-
- init(value: Any) {
- self.hnk_value = value
- }
- }
- extension HanekeGlobals {
-
- // It'd be better to define this in the Cache class but Swift doesn't allow statics in a generic type
- public struct Cache {
-
- public static let OriginalFormatName = "original"
- public enum ErrorCode : Int {
- case objectNotFound = -100
- case formatNotFound = -101
- }
-
- }
-
- }
- open class Cache<T: DataConvertible> where T.Result == T, T : DataRepresentable {
-
- let name: String
-
- var memoryWarningObserver : NSObjectProtocol!
-
- public init(name: String) {
- self.name = name
-
- let notifications = NotificationCenter.default
- // Using block-based observer to avoid subclassing NSObject
- memoryWarningObserver = notifications.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification,
- object: nil,
- queue: OperationQueue.main,
- using: { [unowned self] (notification : Notification!) -> Void in
- self.onMemoryWarning()
- }
- )
-
- let originalFormat = Format<T>(name: HanekeGlobals.Cache.OriginalFormatName)
- self.addFormat(originalFormat)
- }
-
- deinit {
- let notifications = NotificationCenter.default
- notifications.removeObserver(memoryWarningObserver, name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
- }
-
- open func set(value: T, key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, success succeed: ((T) -> ())? = nil) {
- if let (format, memoryCache, diskCache) = self.formats[formatName] {
- self.format(value: value, format: format) { formattedValue in
- let wrapper = ObjectWrapper(value: formattedValue)
- memoryCache.setObject(wrapper, forKey: key as AnyObject)
- // Value data is sent as @autoclosure to be executed in the disk cache queue.
- diskCache.setData(self.dataFromValue(formattedValue, format: format), key: key)
- succeed?(formattedValue)
- }
- } else {
- assertionFailure("Can't set value before adding format")
- }
- }
-
- @discardableResult open func fetch(key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
- let fetch = Cache.buildFetch(failure: fail, success: succeed)
- if let (format, memoryCache, diskCache) = self.formats[formatName] {
- if let wrapper = memoryCache.object(forKey: key as AnyObject) as? ObjectWrapper, let result = wrapper.hnk_value as? T {
- fetch.succeed(result)
- diskCache.updateAccessDate(self.dataFromValue(result, format: format), key: key)
- return fetch
- }
- self.fetchFromDiskCache(diskCache, key: key, memoryCache: memoryCache, failure: { error in
- fetch.fail(error)
- }) { value in
- fetch.succeed(value)
- }
- } else {
- let localizedFormat = NSLocalizedString("Format %@ not found", comment: "Error description")
- let description = String(format:localizedFormat, formatName)
- let error = errorWithCode(HanekeGlobals.Cache.ErrorCode.formatNotFound.rawValue, description: description)
- fetch.fail(error)
- }
- return fetch
- }
-
- @discardableResult open func fetch(fetcher : Fetcher<T>, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
- let key = fetcher.key
- let fetch = Cache.buildFetch(failure: fail, success: succeed)
- self.fetch(key: key, formatName: formatName, failure: { error in
- if (error as NSError?)?.code == HanekeGlobals.Cache.ErrorCode.formatNotFound.rawValue {
- fetch.fail(error)
- }
-
- if let (format, _, _) = self.formats[formatName] {
- self.fetchAndSet(fetcher, format: format, failure: { error in
- fetch.fail(error)
- }) {value in
- fetch.succeed(value)
- }
- }
-
- // Unreachable code. Formats can't be removed from Cache.
- }) { value in
- fetch.succeed(value)
- }
- return fetch
- }
- open func remove(key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName) {
- if let (_, memoryCache, diskCache) = self.formats[formatName] {
- memoryCache.removeObject(forKey: key as AnyObject)
- diskCache.removeData(with: key)
- }
- }
-
- open func removeAll(_ completion: (() -> ())? = nil) {
- let group = DispatchGroup()
- for (_, (_, memoryCache, diskCache)) in self.formats {
- memoryCache.removeAllObjects()
- group.enter()
- diskCache.removeAllData {
- group.leave()
- }
- }
- DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
- let timeout = DispatchTime.now() + Double(Int64(60 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)
- if group.wait(timeout: timeout) != .success {
- Log.error(message: "removeAll timed out waiting for disk caches")
- }
- let path = self.cachePath
- do {
- try FileManager.default.removeItem(atPath: path)
- } catch {
- Log.error(message: "Failed to remove path \(path)", error: error)
- }
- if let completion = completion {
- DispatchQueue.main.async {
- completion()
- }
- }
- }
- }
- // MARK: Size
- open var size: UInt64 {
- var size: UInt64 = 0
- for (_, (_, _, diskCache)) in self.formats {
- diskCache.cacheQueue.sync { size += diskCache.size }
- }
- return size
- }
- // MARK: Notifications
-
- func onMemoryWarning() {
- for (_, (_, memoryCache, _)) in self.formats {
- memoryCache.removeAllObjects()
- }
- }
-
- // MARK: Formats
- public var formats : [String : (Format<T>, NSCache<AnyObject, AnyObject>, DiskCache)] = [:]
-
- open func addFormat(_ format : Format<T>) {
- let name = format.name
- let formatPath = self.formatPath(withFormatName: name)
- let memoryCache = NSCache<AnyObject, AnyObject>()
- let diskCache = DiskCache(path: formatPath, capacity : format.diskCapacity)
- self.formats[name] = (format, memoryCache, diskCache)
- }
-
- // MARK: Internal
-
- lazy var cachePath: String = {
- let basePath = DiskCache.basePath()
- let cachePath = (basePath as NSString).appendingPathComponent(self.name)
- return cachePath
- }()
-
- func formatPath(withFormatName formatName: String) -> String {
- let formatPath = (self.cachePath as NSString).appendingPathComponent(formatName)
- do {
- try FileManager.default.createDirectory(atPath: formatPath, withIntermediateDirectories: true, attributes: nil)
- } catch {
- Log.error(message: "Failed to create directory \(formatPath)", error: error)
- }
- return formatPath
- }
-
- // MARK: Private
-
- func dataFromValue(_ value : T, format : Format<T>) -> Data? {
- if let data = format.convertToData?(value) {
- return data as Data
- }
- return value.asData()
- }
-
- fileprivate func fetchFromDiskCache(_ diskCache : DiskCache, key: String, memoryCache : NSCache<AnyObject, AnyObject>, failure fail : ((Error?) -> ())?, success succeed : @escaping (T) -> ()) {
- diskCache.fetchData(key: key, failure: { error in
- if let block = fail {
- if (error as NSError?)?.code == NSFileReadNoSuchFileError {
- let localizedFormat = NSLocalizedString("Object not found for key %@", comment: "Error description")
- let description = String(format:localizedFormat, key)
- let error = errorWithCode(HanekeGlobals.Cache.ErrorCode.objectNotFound.rawValue, description: description)
- block(error)
- } else {
- block(error)
- }
- }
- }) { data in
- DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async(execute: {
- let value = T.convertFromData(data)
- if let value = value {
- let descompressedValue = self.decompressedImageIfNeeded(value)
- DispatchQueue.main.async(execute: {
- succeed(descompressedValue)
- let wrapper = ObjectWrapper(value: descompressedValue)
- memoryCache.setObject(wrapper, forKey: key as AnyObject)
- })
- }
- })
- }
- }
-
- fileprivate func fetchAndSet(_ fetcher : Fetcher<T>, format : Format<T>, failure fail : ((Error?) -> ())?, success succeed : @escaping (T) -> ()) {
- fetcher.fetch(failure: { error in
- let _ = fail?(error)
- }) { value in
- self.set(value: value, key: fetcher.key, formatName: format.name, success: succeed)
- }
- }
-
- fileprivate func format(value : T, format : Format<T>, success succeed : @escaping (T) -> ()) {
- // HACK: Ideally Cache shouldn't treat images differently but I can't think of any other way of doing this that doesn't complicate the API for other types.
- if format.isIdentity && !(value is UIImage) {
- succeed(value)
- } else {
- DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
- var formatted = format.apply(value)
-
- if let formattedImage = formatted as? UIImage {
- let originalImage = value as? UIImage
- if formattedImage === originalImage {
- formatted = self.decompressedImageIfNeeded(formatted)
- }
- }
-
- DispatchQueue.main.async {
- succeed(formatted)
- }
- }
- }
- }
-
- fileprivate func decompressedImageIfNeeded(_ value : T) -> T {
- if let image = value as? UIImage {
- let decompressedImage = image.hnk_decompressedImage() as? T
- return decompressedImage!
- }
- return value
- }
-
- fileprivate class func buildFetch(failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
- let fetch = Fetch<T>()
- if let succeed = succeed {
- fetch.onSuccess(succeed)
- }
- if let fail = fail {
- fetch.onFailure(fail)
- }
- return fetch
- }
-
- // MARK: Convenience fetch
- // Ideally we would put each of these in the respective fetcher file as a Cache extension. Unfortunately, this fails to link when using the framework in a project as of Xcode 6.1.
-
- open func fetch(key: String, value getValue : @autoclosure @escaping () -> T.Result, formatName: String = HanekeGlobals.Cache.OriginalFormatName, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
- let fetcher = SimpleFetcher<T>(key: key, value: getValue())
- return self.fetch(fetcher: fetcher, formatName: formatName, success: succeed)
- }
-
- open func fetch(path: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
- let fetcher = DiskFetcher<T>(path: path)
- return self.fetch(fetcher: fetcher, formatName: formatName, failure: fail, success: succeed)
- }
-
- open func fetch(URL : Foundation.URL, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
- let fetcher = NetworkFetcher<T>(URL: URL)
- return self.fetch(fetcher: fetcher, formatName: formatName, failure: fail, success: succeed)
- }
-
- }
|