Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Simple Image Cache (memory & disk)
//
// 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