NetNewsWire/Shared/Images/ImageDownloader.swift

144 lines
3.1 KiB
Swift
Raw Normal View History

2017-11-26 05:12:53 +01:00
//
// ImageDownloader.swift
2018-08-29 07:18:24 +02:00
// NetNewsWire
2017-11-26 05:12:53 +01:00
//
// Created by Brent Simmons on 11/25/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import os.log
2017-11-26 05:12:53 +01:00
import RSCore
import RSWeb
extension Notification.Name {
static let ImageDidBecomeAvailable = Notification.Name("ImageDidBecomeAvailableNotification") // UserInfoKey.url
2017-11-26 05:12:53 +01:00
}
final class ImageDownloader {
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ImageDownloader")
2017-11-26 05:12:53 +01:00
private let folder: String
private var diskCache: BinaryDiskCache
private let queue: DispatchQueue
private var imageCache = [String: Data]() // url: image
private var urlsInProgress = Set<String>()
private var badURLs = Set<String>() // That return a 404 or whatever. Just skip them in the future.
2017-11-26 05:12:53 +01:00
init(folder: String) {
self.folder = folder
self.diskCache = BinaryDiskCache(folder: folder)
self.queue = DispatchQueue(label: "ImageDownloader serial queue - \(folder)")
}
@discardableResult
func image(for url: String) -> Data? {
2017-11-26 05:12:53 +01:00
if let data = imageCache[url] {
return data
2017-11-26 05:12:53 +01:00
}
findImage(url)
return nil
}
}
private extension ImageDownloader {
func cacheImage(_ url: String, _ image: Data) {
2017-11-26 05:12:53 +01:00
imageCache[url] = image
postImageDidBecomeAvailableNotification(url)
}
func findImage(_ url: String) {
guard !urlsInProgress.contains(url) && !badURLs.contains(url) else {
return
}
urlsInProgress.insert(url)
2017-11-26 05:12:53 +01:00
readFromDisk(url) { (image) in
if let image = image {
self.cacheImage(url, image)
self.urlsInProgress.remove(url)
2017-11-26 05:12:53 +01:00
return
}
self.downloadImage(url) { (image) in
if let image = image {
self.cacheImage(url, image)
}
self.urlsInProgress.remove(url)
2017-11-26 05:12:53 +01:00
}
}
}
2019-12-15 02:01:34 +01:00
func readFromDisk(_ url: String, _ completion: @escaping (Data?) -> Void) {
2017-11-26 05:12:53 +01:00
queue.async {
if let data = self.diskCache[self.diskKey(url)], !data.isEmpty {
DispatchQueue.main.async {
2019-12-15 02:01:34 +01:00
completion(data)
}
2017-11-26 05:12:53 +01:00
return
}
DispatchQueue.main.async {
2019-12-15 02:01:34 +01:00
completion(nil)
2017-11-26 05:12:53 +01:00
}
}
}
2019-12-15 02:01:34 +01:00
func downloadImage(_ url: String, _ completion: @escaping (Data?) -> Void) {
2017-11-26 05:12:53 +01:00
guard let imageURL = URL(string: url) else {
2019-12-15 02:01:34 +01:00
completion(nil)
2017-11-26 05:12:53 +01:00
return
}
downloadUsingCache(imageURL) { (data, response, error) in
2017-11-26 05:12:53 +01:00
if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil {
self.saveToDisk(url, data)
2019-12-15 02:01:34 +01:00
completion(data)
2017-11-26 05:12:53 +01:00
return
}
if let response = response as? HTTPURLResponse, response.statusCode >= HTTPResponseCode.badRequest && response.statusCode <= HTTPResponseCode.notAcceptable {
self.badURLs.insert(url)
}
2017-11-26 05:12:53 +01:00
if let error = error {
os_log(.info, log: self.log, "Error downloading image at %@: %@.", url, error.localizedDescription)
2017-11-26 05:12:53 +01:00
}
2019-12-15 02:01:34 +01:00
completion(nil)
2017-11-26 05:12:53 +01:00
}
}
func saveToDisk(_ url: String, _ data: Data) {
queue.async {
self.diskCache[self.diskKey(url)] = data
}
}
func diskKey(_ url: String) -> String {
2020-01-18 08:00:56 +01:00
return url.md5String
2017-11-26 05:12:53 +01:00
}
func postImageDidBecomeAvailableNotification(_ url: String) {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .ImageDidBecomeAvailable, object: self, userInfo: [UserInfoKey.url: url])
}
2017-11-26 05:12:53 +01:00
}
}