Created
January 10, 2020 13:56
-
-
Save podkovyrin/2583dcf250cd8d1502f4dc4b6776521a to your computer and use it in GitHub Desktop.
Simple Image Cache (memory & disk)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ImageCache.swift | |
// TwoFAuth | |
// | |
// Created by Andrew Podkovyrin on 8/8/19. | |
// Copyright © 2019 2FAuth. All rights reserved. | |
// | |
// Based on https://github.com/SDWebImage/SDWebImage/blob/master/SDWebImage/Core/SDImageCache.m | |
import UIKit | |
import typealias CommonCrypto.CC_LONG | |
import func CommonCrypto.CC_MD5 | |
import var CommonCrypto.CC_MD5_DIGEST_LENGTH | |
final class ImageCache { | |
private let name = "2fauth.imagecache" | |
private let memoryCache = AutoPurgeCache() | |
private let ioQueue = DispatchQueue(label: "2fauth.imagecache.queue", qos: .default) | |
private var fileManager: FileManager | |
private var diskCacheURL: URL | |
init() { | |
memoryCache.name = name | |
fileManager = FileManager() | |
guard let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first else { | |
fatalError("Caches directory doesn't exist") | |
} | |
diskCacheURL = cachesDirectory.appendingPathComponent(name) | |
} | |
func image(for key: String, completion: @escaping (UIImage?) -> Void) { | |
guard let cacheKey = key.md5() else { | |
completion(nil) | |
return | |
} | |
if let image = memoryCache.object(forKey: cacheKey as NSString) { | |
completion(image) | |
} | |
else { | |
ioQueue.async { | |
if !self.fileManager.fileExists(atPath: self.diskCacheURL.path) { | |
try? self.fileManager.createDirectory(at: self.diskCacheURL, | |
withIntermediateDirectories: true) | |
} | |
let url = self.cachePath(for: cacheKey) | |
if let data = try? Data(contentsOf: url), let image = UIImage.image(with: data) { | |
self.memoryCache.setObject(image, forKey: cacheKey as NSString) | |
DispatchQueue.main.async { | |
completion(image) | |
} | |
} | |
else { | |
DispatchQueue.main.async { | |
completion(nil) | |
} | |
} | |
} | |
} | |
} | |
func storeImage(_ image: UIImage, for key: String) { | |
guard let cacheKey = key.md5() else { | |
return | |
} | |
memoryCache.setObject(image, forKey: cacheKey as NSString) | |
ioQueue.async { | |
guard let data = image.imageData() else { | |
return | |
} | |
let url = self.cachePath(for: cacheKey) | |
try? data.write(to: url, options: .atomic) | |
} | |
} | |
private func cachePath(for key: String) -> URL { | |
return diskCacheURL.appendingPathComponent(key) | |
} | |
} | |
// MARK: - AutoPurgeCache | |
private final class AutoPurgeCache: NSCache<NSString, UIImage> { | |
override init() { | |
super.init() | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(removeAllObjects), | |
name: UIApplication.didReceiveMemoryWarningNotification, | |
object: nil) | |
} | |
} | |
// MARK: - FileManager Extension | |
private extension FileManager { | |
func getOrCreateFolderInCaches(folderName: String) -> URL? { | |
if let cachesDirectory = urls(for: .cachesDirectory, in: .userDomainMask).first { | |
let folderURL = cachesDirectory.appendingPathComponent(folderName) | |
if !fileExists(atPath: folderURL.path) { | |
do { | |
try createDirectory(atPath: folderURL.path, | |
withIntermediateDirectories: true, | |
attributes: nil) | |
} | |
catch { | |
logVerbose(error.localizedDescription) | |
return nil | |
} | |
} | |
return folderURL | |
} | |
return nil | |
} | |
} | |
// MARK: - UIImage Extension | |
private extension UIImage { | |
func imageData() -> Data? { | |
guard let alphaInfo = cgImage?.alphaInfo else { | |
return nil | |
} | |
let hasAlpha = !(alphaInfo == .none || alphaInfo == .noneSkipFirst || alphaInfo == .noneSkipLast) | |
if hasAlpha { | |
return pngData() | |
} | |
else { | |
return jpegData(compressionQuality: 1.0) | |
} | |
} | |
class func image(with data: Data) -> UIImage? { | |
var result = UIImage(data: data) | |
if let image = result, let cgImage = image.cgImage, image.imageOrientation != .up { | |
let orientation = imageOrientationFromImageData(imageData: data) | |
result = UIImage(cgImage: cgImage, scale: image.scale, orientation: orientation) | |
} | |
return result | |
} | |
static func imageOrientationFromImageData(imageData: Data) -> UIImage.Orientation { | |
guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil), | |
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: AnyObject], | |
let exifOrientation = properties[kCGImagePropertyOrientation as String] as? Int else { | |
return .up | |
} | |
return exifOrientationToiOSOrientation(exifOrientation) | |
} | |
static func exifOrientationToiOSOrientation(_ exifOrientation: Int) -> UIImage.Orientation { | |
switch exifOrientation { | |
case 1: return .up | |
case 3: return .down | |
case 8: return .left | |
case 6: return .right | |
case 2: return .upMirrored | |
case 4: return .downMirrored | |
case 5: return .leftMirrored | |
case 7: return .rightMirrored | |
default: return .up | |
} | |
} | |
} | |
// MARK: - String Extension | |
private extension String { | |
func md5() -> String? { | |
let length = Int(CC_MD5_DIGEST_LENGTH) | |
guard let messageData = data(using: .utf8) else { | |
return nil | |
} | |
var digestData = Data(count: length) | |
_ = digestData.withUnsafeMutableBytes { digestBytes -> UInt8 in | |
messageData.withUnsafeBytes { messageBytes -> UInt8 in | |
if let messageBytesBaseAddress = messageBytes.baseAddress, | |
let digestBytesBlindMemory = digestBytes.bindMemory(to: UInt8.self).baseAddress { | |
let messageLength = CC_LONG(messageData.count) | |
CC_MD5(messageBytesBaseAddress, messageLength, digestBytesBlindMemory) | |
} | |
return 0 | |
} | |
} | |
return digestData.map { String(format: "%02hhx", $0) }.joined() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment