-
Notifications
You must be signed in to change notification settings - Fork 554
/
Copy pathImageDownloader.swift
144 lines (107 loc) · 3.18 KB
/
ImageDownloader.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//
// ImageDownloader.swift
// NetNewsWire
//
// Created by Brent Simmons on 11/25/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import os.log
import RSCore
import RSWeb
extension Notification.Name {
static let ImageDidBecomeAvailable = Notification.Name("ImageDidBecomeAvailableNotification") // UserInfoKey.url
}
final class ImageDownloader {
public static let shared = ImageDownloader()
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ImageDownloader")
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.
init() {
let folder = AppConfig.cacheSubfolder(named: "Images")
self.diskCache = BinaryDiskCache(folder: folder.path)
self.queue = DispatchQueue(label: "ImageDownloader serial queue - \(folder.path)")
}
@discardableResult
func image(for url: String) -> Data? {
if let data = imageCache[url] {
return data
}
findImage(url)
return nil
}
}
private extension ImageDownloader {
func cacheImage(_ url: String, _ image: Data) {
imageCache[url] = image
postImageDidBecomeAvailableNotification(url)
}
func findImage(_ url: String) {
guard !urlsInProgress.contains(url) && !badURLs.contains(url) else {
return
}
urlsInProgress.insert(url)
readFromDisk(url) { (image) in
if let image = image {
self.cacheImage(url, image)
self.urlsInProgress.remove(url)
return
}
self.downloadImage(url) { (image) in
if let image = image {
self.cacheImage(url, image)
}
self.urlsInProgress.remove(url)
}
}
}
func readFromDisk(_ url: String, _ completion: @escaping (Data?) -> Void) {
queue.async {
if let data = self.diskCache[self.diskKey(url)], !data.isEmpty {
DispatchQueue.main.async {
completion(data)
}
return
}
DispatchQueue.main.async {
completion(nil)
}
}
}
func downloadImage(_ url: String, _ completion: @escaping (Data?) -> Void) {
guard let imageURL = URL(string: url) else {
completion(nil)
return
}
Downloader.shared.download(imageURL) { (data, response, error) in
if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil {
self.saveToDisk(url, data)
completion(data)
return
}
if let response = response as? HTTPURLResponse, response.statusCode >= HTTPResponseCode.badRequest && response.statusCode <= HTTPResponseCode.notAcceptable {
self.badURLs.insert(url)
}
if let error = error {
os_log(.info, log: self.log, "Error downloading image at %@: %@.", url, error.localizedDescription)
}
completion(nil)
}
}
func saveToDisk(_ url: String, _ data: Data) {
queue.async {
self.diskCache[self.diskKey(url)] = data
}
}
func diskKey(_ url: String) -> String {
return url.md5String
}
func postImageDidBecomeAvailableNotification(_ url: String) {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .ImageDidBecomeAvailable, object: self, userInfo: [UserInfoKey.url: url])
}
}
}