|
@@ -9,6 +9,8 @@
|
|
|
import Foundation
|
|
|
import UIKit
|
|
|
import SDWebImage
|
|
|
+import ImageIO
|
|
|
+import MobileCoreServices
|
|
|
|
|
|
extension Date {
|
|
|
|
|
@@ -223,6 +225,56 @@ extension UIViewController {
|
|
|
}
|
|
|
|
|
|
extension UIImage {
|
|
|
+ class func gifImageWithData(_ data: Data) -> UIImage? {
|
|
|
+ guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ let frameCount = CGImageSourceGetCount(source)
|
|
|
+ var images: [UIImage] = []
|
|
|
+
|
|
|
+ for i in 0..<frameCount {
|
|
|
+ if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
|
|
|
+ let image = UIImage(cgImage: cgImage)
|
|
|
+ images.append(image)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return UIImage.animatedImage(with: images, duration: 0.0)
|
|
|
+ }
|
|
|
+
|
|
|
+ func gifData() -> Data? {
|
|
|
+ guard let cgImages = self.images?.compactMap({ $0.cgImage }) else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: 0.1]] // Adjust delay time if necessary
|
|
|
+
|
|
|
+ let destinationData = NSMutableData()
|
|
|
+ guard let destination = CGImageDestinationCreateWithData(destinationData as CFMutableData, kUTTypeGIF, cgImages.count, nil) else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ let framePropertiesPointer = UnsafeMutablePointer<NSDictionary>.allocate(capacity: cgImages.count)
|
|
|
+ defer {
|
|
|
+ framePropertiesPointer.deallocate()
|
|
|
+ }
|
|
|
+ for i in 0..<cgImages.count {
|
|
|
+ framePropertiesPointer[i] = frameProperties as NSDictionary
|
|
|
+ }
|
|
|
+
|
|
|
+ CGImageDestinationSetProperties(destination, [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]] as CFDictionary)
|
|
|
+ for image in cgImages {
|
|
|
+ CGImageDestinationAddImage(destination, image, framePropertiesPointer.pointee)
|
|
|
+ }
|
|
|
+
|
|
|
+ guard CGImageDestinationFinalize(destination) else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return destinationData as Data
|
|
|
+ }
|
|
|
+
|
|
|
func resize(target: CGSize) -> UIImage {
|
|
|
// Determine the scale factor that preserves aspect ratio
|
|
|
let widthRatio = target.width / size.width
|
|
@@ -1100,7 +1152,7 @@ extension UIImageView {
|
|
|
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
|
|
}
|
|
|
|
|
|
- func loadImageAsync(with urlString: String?) {
|
|
|
+ func loadImageAsync(with urlString: String?, isGif: Bool = false) {
|
|
|
// cancel prior task, if any
|
|
|
|
|
|
weak var oldTask = currentTask
|
|
@@ -1116,6 +1168,15 @@ extension UIImageView {
|
|
|
guard let urlString = urlString else { return }
|
|
|
|
|
|
// check cache
|
|
|
+
|
|
|
+ if isGif, let cachedImageGif = ImageCache.shared.imageGif(forKey: urlString) {
|
|
|
+ guard let gifImage = UIImage.gifImageWithData(Data(referencing: cachedImageGif)) else {
|
|
|
+ print("Failed to create the GIF image.")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ self.image = gifImage
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
if let cachedImage = ImageCache.shared.image(forKey: urlString) {
|
|
|
self.image = cachedImage
|
|
@@ -1145,16 +1206,31 @@ extension UIImageView {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- guard let data = data, let downloadedImage = UIImage(data: data) else {
|
|
|
+ guard let data = data else {
|
|
|
//print("unable to extract image")
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
- ImageCache.shared.save(image: downloadedImage, forKey: urlString)
|
|
|
+
|
|
|
+ let downloadedImage = UIImage(data: data)
|
|
|
+ if isGif {
|
|
|
+ ImageCache.shared.saveGif(data: NSData(data: data), forKey: urlString)
|
|
|
+ } else {
|
|
|
+ if downloadedImage != nil {
|
|
|
+ ImageCache.shared.save(image: downloadedImage!, forKey: urlString)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
if url == self?.currentURL {
|
|
|
DispatchQueue.main.async {
|
|
|
- self?.image = downloadedImage
|
|
|
+ if isGif {
|
|
|
+ guard let gifImage = UIImage.gifImageWithData(data) else {
|
|
|
+ print("Failed to create the GIF image.")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ self?.image = gifImage
|
|
|
+ } else {
|
|
|
+ self?.image = downloadedImage
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -1227,29 +1303,67 @@ extension UITextField {
|
|
|
}
|
|
|
|
|
|
public class ImageCache {
|
|
|
- private let cache = NSCache<NSString, UIImage>()
|
|
|
- private var observer: NSObjectProtocol!
|
|
|
-
|
|
|
public static let shared = ImageCache()
|
|
|
+ private let cache = NSCache<NSString, UIImage>()
|
|
|
+ private let cacheGif = NSCache<NSString, NSData>()
|
|
|
|
|
|
private init() {
|
|
|
- // make sure to purge cache on memory pressure
|
|
|
-
|
|
|
- observer = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: nil) { [weak self] notification in
|
|
|
- self?.cache.removeAllObjects()
|
|
|
- }
|
|
|
+ loadCache()
|
|
|
+// UserDefaults.standard.removeObject(forKey: "imageCache")
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(saveCache), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
|
}
|
|
|
|
|
|
- deinit {
|
|
|
- NotificationCenter.default.removeObserver(observer as Any)
|
|
|
+ public func save(image: UIImage, forKey key: String) {
|
|
|
+ cache.setObject(image, forKey: key as NSString)
|
|
|
+ cacheKeys.append(key)
|
|
|
+ saveCache()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func saveGif(data: NSData, forKey key: String) {
|
|
|
+ cacheGif.setObject(data, forKey: key as NSString)
|
|
|
+ cacheKeys.append(key)
|
|
|
+ saveCache()
|
|
|
}
|
|
|
|
|
|
public func image(forKey key: String) -> UIImage? {
|
|
|
return cache.object(forKey: key as NSString)
|
|
|
}
|
|
|
+ public func imageGif(forKey key: String) -> NSData? {
|
|
|
+ return cacheGif.object(forKey: key as NSString)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var cacheKeys: [String] = []
|
|
|
+ private func loadCache() {
|
|
|
+ if let cachedData = UserDefaults.standard.object(forKey: "imageCache") as? Data {
|
|
|
+ if let decodedCache = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(cachedData) as? [String: Data] {
|
|
|
+ for (key, valueData) in decodedCache {
|
|
|
+ if let image = UIImage(data: valueData) {
|
|
|
+ cache.setObject(image, forKey: key as NSString)
|
|
|
+ cacheKeys.append(key)
|
|
|
+ }
|
|
|
+ let dataGif = NSData(data: valueData)
|
|
|
+ cacheGif.setObject(dataGif, forKey: key as NSString)
|
|
|
+ cacheKeys.append(key)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- public func save(image: UIImage, forKey key: String) {
|
|
|
- cache.setObject(image, forKey: key as NSString)
|
|
|
+ @objc private func saveCache() {
|
|
|
+ var cacheDictionary = [String: Data]()
|
|
|
+ for key in cacheKeys {
|
|
|
+ if let dataGif = cacheGif.object(forKey: key as NSString) {
|
|
|
+ cacheDictionary[key] = Data(referencing: dataGif)
|
|
|
+ } else {
|
|
|
+ if let image = cache.object(forKey: key as NSString) {
|
|
|
+ if let imageData = image.pngData() {
|
|
|
+ cacheDictionary[key] = imageData
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let encodedData = try? NSKeyedArchiver.archivedData(withRootObject: cacheDictionary, requiringSecureCoding: false)
|
|
|
+ UserDefaults.standard.set(encodedData, forKey: "imageCache")
|
|
|
}
|
|
|
}
|
|
|
|