OLD | NEW |
1 // | 1 // |
2 // FavIcon | 2 // FavIcon |
3 // Copyright © 2016 Leon Breedt | 3 // Copyright © 2016 Leon Breedt |
4 // | 4 // |
5 // Licensed under the Apache License, Version 2.0 (the "License"); | 5 // Licensed under the Apache License, Version 2.0 (the "License"); |
6 // you may not use this file except in compliance with the License. | 6 // you may not use this file except in compliance with the License. |
7 // You may obtain a copy of the License at | 7 // You may obtain a copy of the License at |
8 // | 8 // |
9 // http://www.apache.org/licenses/LICENSE-2.0 | 9 // http://www.apache.org/licenses/LICENSE-2.0 |
10 // | 10 // |
11 // Unless required by applicable law or agreed to in writing, software | 11 // Unless required by applicable law or agreed to in writing, software |
12 // distributed under the License is distributed on an "AS IS" BASIS, | 12 // distributed under the License is distributed on an "AS IS" BASIS, |
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 // See the License for the specific language governing permissions and | 14 // See the License for the specific language governing permissions and |
15 // limitations under the License. | 15 // limitations under the License. |
16 // | 16 // |
17 | 17 |
| 18 // swiftlint:disable sorted_imports |
| 19 // swiftlint:disable file_length |
| 20 |
18 import Foundation | 21 import Foundation |
19 | |
20 #if os(iOS) | 22 #if os(iOS) |
21 import UIKit | 23 import UIKit |
22 /// Alias for the iOS image type (`UIImage`). | 24 /// Alias for the iOS image type (`UIImage`). |
23 public typealias ImageType = UIImage | 25 public typealias ImageType = UIImage |
24 #elseif os(OSX) | 26 #elseif os(OSX) |
25 import Cocoa | 27 import Cocoa |
26 /// Alias for the OS X image type (`NSImage`). | 28 /// Alias for the OS X image type (`NSImage`). |
27 public typealias ImageType = NSImage | 29 public typealias ImageType = NSImage |
28 #endif | 30 #endif |
29 | 31 |
30 /// Represents the result of attempting to download an icon. | 32 /// Represents the result of attempting to download an icon. |
31 public enum IconDownloadResult { | 33 public enum IconDownloadResult { |
32 | 34 |
33 /// Download successful. | 35 /// Download successful. |
34 /// | 36 /// |
35 /// - parameter image: The `ImageType` for the downloaded icon. | 37 /// - parameter image: The `ImageType` for the downloaded icon. |
36 case success(image: ImageType) | 38 case success(image: ImageType) |
37 | 39 |
38 /// Download failed for some reason. | 40 /// Download failed for some reason. |
39 /// | 41 /// |
40 /// - parameter error: The error which can be consulted to determine the roo
t cause. | 42 /// - parameter error: The error which can be consulted to determine the roo
t cause. |
41 case failure(error: Error) | 43 case failure(error: Error) |
42 | 44 |
43 } | 45 } |
44 | 46 |
45 /// Responsible for detecting all of the different icons supported by a given si
te. | 47 /// Responsible for detecting all of the different icons supported by a given si
te. |
46 @objc public final class FavIcon : NSObject { | 48 @objc |
| 49 public final class FavIcon: NSObject { |
47 | 50 |
48 // swiftlint:disable function_body_length | 51 // swiftlint:disable function_body_length |
49 | 52 |
50 /// Scans a base URL, attempting to determine all of the supported icons tha
t can | 53 /// Scans a base URL, attempting to determine all of the supported icons tha
t can |
51 /// be used for favicon purposes. | 54 /// be used for favicon purposes. |
52 /// | 55 /// |
53 /// It will do the following to determine possible icons that can be used: | 56 /// It will do the following to determine possible icons that can be used: |
54 /// | 57 /// |
55 /// - Check whether or not `/favicon.ico` exists. | 58 /// - Check whether or not `/favicon.ico` exists. |
56 /// - If the base URL returns an HTML page, parse the `<head>` section and c
heck for `<link>` | 59 /// - If the base URL returns an HTML page, parse the `<head>` section and c
heck for `<link>` |
57 /// and `<meta>` tags that reference icons using Apple, Microsoft and Goog
le | 60 /// and `<meta>` tags that reference icons using Apple, Microsoft and Goog
le |
58 /// conventions. | 61 /// conventions. |
59 /// - If _Web Application Manifest JSON_ (`manifest.json`) files are referen
ced, or | 62 /// - If _Web Application Manifest JSON_ (`manifest.json`) files are referen
ced, or |
60 /// _Microsoft browser configuration XML_ (`browserconfig.xml`) files | 63 /// _Microsoft browser configuration XML_ (`browserconfig.xml`) files |
61 /// are referenced, download and parse them to check if they reference ico
ns. | 64 /// are referenced, download and parse them to check if they reference ico
ns. |
62 /// | 65 /// |
63 /// All of this work is performed in a background queue. | 66 /// All of this work is performed in a background queue. |
64 /// | 67 /// |
65 /// - parameter url: The base URL to scan. | 68 /// - parameter url: The base URL to scan. |
66 /// - parameter completion: A closure to call when the scan has completed. T
he closure will be call | 69 /// - parameter completion: A closure to call when the scan has completed. T
he closure will be call |
67 /// on the main queue. | 70 /// on the main queue. |
68 @objc public static func scan(_ url: URL, completion: @escaping ([DetectedIc
on], [String:String]) -> Void) { | 71 @objc |
| 72 public static func scan(_ url: URL, completion: @escaping ([DetectedIcon], [
String:String]) -> Void) { |
69 let queue = DispatchQueue(label: "org.bitserf.FavIcon", attributes: []) | 73 let queue = DispatchQueue(label: "org.bitserf.FavIcon", attributes: []) |
70 var icons: [DetectedIcon] = [] | 74 var icons: [DetectedIcon] = [] |
71 var additionalDownloads: [URLRequestWithCallback] = [] | 75 var additionalDownloads: [URLRequestWithCallback] = [] |
72 let urlSession = urlSessionProvider() | 76 let urlSession = urlSessionProvider() |
73 var meta: [String:String] = [:] | 77 var meta: [String:String] = [:] |
74 | 78 |
75 let downloadHTMLOperation = DownloadTextOperation(url: url, session: url
Session) | 79 let downloadHTMLOperation = DownloadTextOperation(url: url, session: url
Session) |
76 let downloadHTML = urlRequestOperation(downloadHTMLOperation) { result i
n | 80 let downloadHTML = urlRequestOperation(downloadHTMLOperation) { result i
n |
77 if case let .textDownloaded(actualURL, text, contentType) = result { | 81 if case let .textDownloaded(actualURL, text, contentType) = result { |
78 if contentType == "text/html" { | 82 if contentType == "text/html" { |
79 let document = HTMLDocument(string: text) | 83 let document = HTMLDocument(string: text) |
80 | 84 |
81 let htmlIcons = extractHTMLHeadIcons(document, baseURL: actu
alURL) | 85 let htmlIcons = extractHTMLHeadIcons(document, baseURL: actu
alURL) |
82 let htmlMeta = examineHTMLMeta(document, baseURL: actualURL) | 86 let htmlMeta = examineHTMLMeta(document, baseURL: actualURL) |
83 queue.sync { | 87 queue.sync { |
84 icons.append(contentsOf: htmlIcons) | 88 icons.append(contentsOf: htmlIcons) |
85 meta = htmlMeta | 89 meta = htmlMeta |
86 } | 90 } |
87 | 91 |
88 for manifestURL in extractWebAppManifestURLs(document, baseU
RL: url) { | 92 for manifestURL in extractWebAppManifestURLs(document, baseU
RL: url) { |
89 let downloadOperation = DownloadTextOperation(url: manif
estURL, | 93 let downloadOperation = DownloadTextOperation(url: manif
estURL, |
90 se
ssion: urlSession) | 94 session: u
rlSession) |
91 let download = urlRequestOperation(downloadOperation) {
result in | 95 let download = urlRequestOperation(downloadOperation) {
result in |
92 if case .textDownloaded(_, let manifestJSON, _) = re
sult { | 96 if case .textDownloaded(_, let manifestJSON, _) = re
sult { |
93 let jsonIcons = extractManifestJSONIcons( | 97 let jsonIcons = extractManifestJSONIcons( |
94 manifestJSON, | 98 manifestJSON, |
95 baseURL: actualURL | 99 baseURL: actualURL |
96 ) | 100 ) |
97 queue.sync { | 101 queue.sync { |
98 icons.append(contentsOf: jsonIcons) | 102 icons.append(contentsOf: jsonIcons) |
99 } | 103 } |
100 } | 104 } |
(...skipping 16 matching lines...) Expand all Loading... |
117 icons.append(contentsOf: xmlIcons) | 121 icons.append(contentsOf: xmlIcons) |
118 } | 122 } |
119 } | 123 } |
120 } | 124 } |
121 additionalDownloads.append(download) | 125 additionalDownloads.append(download) |
122 } | 126 } |
123 } | 127 } |
124 } | 128 } |
125 } | 129 } |
126 | 130 |
127 | |
128 let favIconURL = URL(string: "/favicon.ico", relativeTo: url as URL)!.ab
soluteURL | 131 let favIconURL = URL(string: "/favicon.ico", relativeTo: url as URL)!.ab
soluteURL |
129 let checkFavIconOperation = CheckURLExistsOperation(url: favIconURL, ses
sion: urlSession) | 132 let checkFavIconOperation = CheckURLExistsOperation(url: favIconURL, ses
sion: urlSession) |
130 let checkFavIcon = urlRequestOperation(checkFavIconOperation) { result i
n | 133 let checkFavIcon = urlRequestOperation(checkFavIconOperation) { result i
n |
131 if case let .success(actualURL) = result { | 134 if case let .success(actualURL) = result { |
132 queue.sync { | 135 queue.sync { |
133 icons.append(DetectedIcon(url: actualURL, type: .classic)) | 136 icons.append(DetectedIcon(url: actualURL, type: .classic)) |
134 } | 137 } |
135 } | 138 } |
136 } | 139 } |
137 | 140 |
138 let touchIconURL = URL(string: "/apple-touch-icon.png", relativeTo: url
as URL)!.absoluteURL | 141 let touchIconURL = URL(string: "/apple-touch-icon.png", relativeTo: url
as URL)!.absoluteURL |
139 let checkTouchIconOperation = CheckURLExistsOperation(url: touchIconURL,
session: urlSession) | 142 let checkTouchIconOperation = CheckURLExistsOperation(url: touchIconURL,
session: urlSession) |
140 let checkTouchIcon = urlRequestOperation(checkTouchIconOperation) { resu
lt in | 143 let checkTouchIcon = urlRequestOperation(checkTouchIconOperation) { resu
lt in |
141 if case let .success(actualURL) = result { | 144 if case let .success(actualURL) = result { |
142 queue.sync { | 145 queue.sync { |
143 icons.append(DetectedIcon(url: actualURL, type: .appleIOSWebClip,
width: 60, height: 60)) | 146 icons.append(DetectedIcon(url: actualURL, type: .appleIOSWebClip,
width: 60, height: 60)) |
144 } | 147 } |
145 } | 148 } |
146 } | 149 } |
147 | 150 |
(...skipping 12 matching lines...) Expand all Loading... |
160 } | 163 } |
161 } | 164 } |
162 // swiftlint:enable function_body_length | 165 // swiftlint:enable function_body_length |
163 | 166 |
164 /// Downloads an array of detected icons in the background. | 167 /// Downloads an array of detected icons in the background. |
165 /// | 168 /// |
166 /// - parameter icons: The icons to download. | 169 /// - parameter icons: The icons to download. |
167 /// - parameter completion: A closure to call when all download tasks have | 170 /// - parameter completion: A closure to call when all download tasks have |
168 /// results available (successful or otherwise). The
closure | 171 /// results available (successful or otherwise). The
closure |
169 /// will be called on the main queue. | 172 /// will be called on the main queue. |
170 @objc public static func download(_ icons: [DetectedIcon], completion: @esca
ping ([ImageType]) -> Void) { | 173 @objc |
| 174 public static func download(_ icons: [DetectedIcon], completion: @escaping (
[ImageType]) -> Void) { |
171 let urlSession = urlSessionProvider() | 175 let urlSession = urlSessionProvider() |
172 let operations: [DownloadImageOperation] = | 176 let operations: [DownloadImageOperation] = |
173 icons.map { DownloadImageOperation(url: $0.url, session: urlSession)
} | 177 icons.map { DownloadImageOperation(url: $0.url, session: urlSession)
} |
174 | 178 |
175 executeURLOperations(operations) { results in | 179 executeURLOperations(operations) { results in |
176 let downloadResults: [ImageType] = results.flatMap { result in | 180 let downloadResults: [ImageType] = results.flatMap { result in |
177 switch result { | 181 switch result { |
178 case .imageDownloaded(_, let image): | 182 case .imageDownloaded(_, let image): |
179 return image; | 183 return image |
180 case .failed(_): | 184 case .failed: |
181 return nil; | 185 return nil |
182 default: | 186 default: |
183 return nil; | 187 return nil |
184 } | 188 } |
185 } | 189 } |
186 | 190 |
187 DispatchQueue.main.async { | 191 DispatchQueue.main.async { |
188 completion(downloadResults) | 192 completion(downloadResults) |
189 } | 193 } |
190 } | 194 } |
191 } | 195 } |
192 | 196 |
193 /// Downloads all available icons by calling `scan(url:)` to discover the av
ailable icons, and then | 197 /// Downloads all available icons by calling `scan(url:)` to discover the av
ailable icons, and then |
194 /// performing background downloads of each icon. | 198 /// performing background downloads of each icon. |
195 /// | 199 /// |
196 /// - parameter url: The URL to scan for icons. | 200 /// - parameter url: The URL to scan for icons. |
197 /// - parameter completion: A closure to call when all download tasks have r
esults available | 201 /// - parameter completion: A closure to call when all download tasks have r
esults available |
198 /// (successful or otherwise). The closure will be c
alled on the main queue. | 202 /// (successful or otherwise). The closure will be c
alled on the main queue. |
199 @objc public static func downloadAll(_ url: URL, completion: @escaping ([Ima
geType]) -> Void) { | 203 @objc |
200 scan(url) { icons, meta in | 204 public static func downloadAll(_ url: URL, completion: @escaping ([ImageType
]) -> Void) { |
| 205 scan(url) { icons, _ in |
201 download(icons, completion: completion) | 206 download(icons, completion: completion) |
202 } | 207 } |
203 } | 208 } |
204 | 209 |
205 /// Downloads the most preferred icon, by calling `scan(url:)` to discover a
vailable icons, and then choosing | 210 /// Downloads the most preferred icon, by calling `scan(url:)` to discover a
vailable icons, and then choosing |
206 /// the most preferable available icon. If both `width` and `height` are sup
plied, the icon closest to the | 211 /// the most preferable available icon. If both `width` and `height` are sup
plied, the icon closest to the |
207 /// preferred size is chosen. Otherwise, the largest icon is chosen, if dime
nsions are known. If no icon | 212 /// preferred size is chosen. Otherwise, the largest icon is chosen, if dime
nsions are known. If no icon |
208 /// has dimensions, the icons are chosen by order of their `DetectedIconType
` enumeration raw value. | 213 /// has dimensions, the icons are chosen by order of their `DetectedIconType
` enumeration raw value. |
209 /// | 214 /// |
210 /// - parameter url: The URL to scan for icons. | 215 /// - parameter url: The URL to scan for icons. |
211 /// - parameter width: The preferred icon width, in pixels, or `nil`. | 216 /// - parameter width: The preferred icon width, in pixels, or `nil`. |
212 /// - parameter height: The preferred icon height, in pixels, or `nil`. | 217 /// - parameter height: The preferred icon height, in pixels, or `nil`. |
213 /// - parameter completion: A closure to call when the download task has pro
duced results. The closure will | 218 /// - parameter completion: A closure to call when the download task has pro
duced results. The closure will |
214 /// be called on the main queue. | 219 /// be called on the main queue. |
215 /// - throws: An appropriate `IconError` if downloading was not successful. | 220 /// - throws: An appropriate `IconError` if downloading was not successful. |
216 @objc public static func downloadPreferred(_ url: URL, | 221 @objc |
| 222 public static func downloadPreferred(_ url: URL, |
217 width: Int, | 223 width: Int, |
218 height: Int, | 224 height: Int, |
219 completion: @escaping (ImageType?) -> V
oid) { | 225 completion: @escaping (ImageType?) -> V
oid) { |
220 scan(url) { icons, meta in | 226 scan(url) { icons, _ in |
221 guard let icon = chooseIcon(icons, width: width, height: height) els
e { | 227 guard let icon = chooseIcon(icons, width: width, height: height) els
e { |
222 DispatchQueue.main.async { | 228 DispatchQueue.main.async { |
223 completion(ImageType()); | 229 completion(ImageType()) |
224 } | 230 } |
225 return | 231 return |
226 } | 232 } |
227 | 233 |
228 let urlSession = urlSessionProvider() | 234 let urlSession = urlSessionProvider() |
229 | 235 |
230 let operations = [DownloadImageOperation(url: icon.url, session: url
Session)] | 236 let operations = [DownloadImageOperation(url: icon.url, session: url
Session)] |
231 executeURLOperations(operations) { results in | 237 executeURLOperations(operations) { results in |
232 let downloadResults: [ImageType] = results.flatMap { result in | 238 let downloadResults: [ImageType] = results.flatMap { result in |
233 switch result { | 239 switch result { |
234 case let .imageDownloaded(_, image): | 240 case let .imageDownloaded(_, image): |
235 return image; | 241 return image |
236 case .failed(_): | 242 case .failed: |
237 return nil; | 243 return nil |
238 default: | 244 default: |
239 return nil; | 245 return nil |
240 } | 246 } |
241 } | 247 } |
242 | 248 |
243 DispatchQueue.main.async { | 249 DispatchQueue.main.async { |
244 completion(downloadResults.first) | 250 completion(downloadResults.first) |
245 } | 251 } |
246 } | 252 } |
247 } | 253 } |
248 } | 254 } |
249 | 255 |
250 @objc public static func chooseLargestIconSmallerThan(_ icons: [DetectedIcon],
width: Int, height: Int) -> DetectedIcon? { | 256 @objc |
251 var filteredIcons = icons; | 257 public static func chooseLargestIconSmallerThan(_ icons: [DetectedIcon], wid
th: Int, height: Int) -> DetectedIcon? { |
252 if (width > 0 && height > 0) { | 258 var filteredIcons = icons |
253 filteredIcons = icons.filter { (icon) -> Bool in | 259 if width > 0 && height > 0 { |
| 260 filteredIcons = icons.filter { icon -> Bool in |
254 if let iconWidth = icon.width, | 261 if let iconWidth = icon.width, |
255 let iconHeight = icon.height { | 262 let iconHeight = icon.height { |
256 return iconWidth <= width && iconHeight <= height; | 263 return iconWidth <= width && iconHeight <= height |
257 } else { return true; } | 264 } else { return true; } |
258 } | 265 } |
259 } | 266 } |
260 return chooseIcon(filteredIcons, width: 0, height: 0); | 267 return chooseIcon(filteredIcons, width: 0, height: 0) |
261 | 268 |
262 } | 269 } |
263 | 270 |
264 @objc public static func choseIconLargerThan(_ icons: [DetectedIcon], width: I
nt, height: Int) -> DetectedIcon? { | 271 @objc |
265 var filteredIcons = icons; | 272 public static func choseIconLargerThan(_ icons: [DetectedIcon], width: Int,
height: Int) -> DetectedIcon? { |
266 if (width > 0 && height > 0) { | 273 var filteredIcons = icons |
267 filteredIcons = icons.filter { (icon) -> Bool in | 274 if width > 0 && height > 0 { |
| 275 filteredIcons = icons.filter { icon -> Bool in |
268 if let iconWidth = icon.width, | 276 if let iconWidth = icon.width, |
269 let iconHeight = icon.height { | 277 let iconHeight = icon.height { |
270 return iconWidth >= width && iconHeight >= height; | 278 return iconWidth >= width && iconHeight >= height |
271 } else { return true; } | 279 } else { return true; } |
272 } | 280 } |
273 } | 281 } |
274 return chooseIcon(filteredIcons, width: 0, height: 0); | 282 return chooseIcon(filteredIcons, width: 0, height: 0) |
275 | |
276 } | 283 } |
277 // MARK: Test hooks | 284 // MARK: Test hooks |
278 | 285 |
279 typealias URLSessionProvider = () -> URLSession | 286 typealias URLSessionProvider = () -> URLSession |
| 287 |
280 @objc static var urlSessionProvider: URLSessionProvider = FavIcon.createDefa
ultURLSession | 288 @objc static var urlSessionProvider: URLSessionProvider = FavIcon.createDefa
ultURLSession |
281 | 289 |
282 // MARK: Internal | 290 // MARK: Internal |
283 | 291 |
284 @objc static func createDefaultURLSession() -> URLSession { | 292 @objc |
| 293 static func createDefaultURLSession() -> URLSession { |
285 return URLSession.shared | 294 return URLSession.shared |
286 } | 295 } |
287 | 296 |
288 /// Helper function to choose an icon to use out of a set of available icons
. If preferred | 297 /// Helper function to choose an icon to use out of a set of available icons
. If preferred |
289 /// width or height is supplied, the icon closest to the preferred size is c
hosen. If no | 298 /// width or height is supplied, the icon closest to the preferred size is c
hosen. If no |
290 /// preferred width or height is supplied, the largest icon (if known) is ch
osen. | 299 /// preferred width or height is supplied, the largest icon (if known) is ch
osen. |
291 /// | 300 /// |
292 /// - parameter icons: The icons to choose from. | 301 /// - parameter icons: The icons to choose from. |
293 /// - parameter width: The preferred icon width. | 302 /// - parameter width: The preferred icon width. |
294 /// - parameter height: The preferred icon height. | 303 /// - parameter height: The preferred icon height. |
295 /// - returns: The chosen icon, or `nil`, if `icons` is empty. | 304 /// - returns: The chosen icon, or `nil`, if `icons` is empty. |
296 static func chooseIcon(_ icons: [DetectedIcon], width: Int? = nil, height: I
nt? = nil) -> DetectedIcon? { | 305 static func chooseIcon(_ icons: [DetectedIcon], width: Int? = nil, height: I
nt? = nil) -> DetectedIcon? { |
297 guard icons.count > 0 else { return nil } | 306 guard icons.count > 0 else { return nil } |
298 | 307 |
299 let iconsInPreferredOrder = icons.sorted { left, right in | 308 let iconsInPreferredOrder = icons.sorted { left, right in |
300 if width! > 0 || height! > 0 { | 309 if width! > 0 || height! > 0 { |
301 let preferredWidth = width, preferredHeight = height, | 310 let preferredWidth = width, preferredHeight = height, |
302 widthLeft = left.width, heightLeft = left.height, | 311 widthLeft = left.width, heightLeft = left.height, |
303 widthRight = right.width, heightRight = right.height; | 312 widthRight = right.width, heightRight = right.height |
304 // Which is closest to preferred size? | 313 // Which is closest to preferred size? |
305 let deltaA = abs(widthLeft! - preferredWidth!) * abs(heightLeft!
- preferredHeight!) | 314 let deltaA = abs(widthLeft! - preferredWidth!) * abs(heightLeft!
- preferredHeight!) |
306 let deltaB = abs(widthRight! - preferredWidth!) * abs(heightRigh
t! - preferredHeight!) | 315 let deltaB = abs(widthRight! - preferredWidth!) * abs(heightRigh
t! - preferredHeight!) |
307 return deltaA < deltaB | 316 return deltaA < deltaB |
308 } else { | 317 } else { |
309 if let areaLeft = left.area, let areaRight = right.area { | 318 if let areaLeft = left.area, let areaRight = right.area { |
310 // Which is larger? | 319 // Which is larger? |
311 return areaRight < areaLeft | 320 return areaRight < areaLeft |
312 } | 321 } |
313 } | 322 } |
314 | 323 |
315 if left.area != nil { | 324 if left.area != nil { |
316 // Only A has dimensions, prefer it. | 325 // Only A has dimensions, prefer it. |
317 return true | 326 return true |
318 } | 327 } |
319 if right.area != nil { | 328 if right.area != nil { |
320 // Only B has dimensions, prefer it. | 329 // Only B has dimensions, prefer it. |
321 return false | 330 return false |
322 } | 331 } |
323 | 332 |
324 // Neither has dimensions, order by enum value | 333 // Neither has dimensions, order by enum value |
325 return left.type.rawValue < right.type.rawValue | 334 return left.type.rawValue < right.type.rawValue |
326 } | 335 } |
327 | 336 |
328 return iconsInPreferredOrder.first! | 337 return iconsInPreferredOrder.first! |
329 } | 338 } |
330 | 339 |
331 fileprivate override init () { | 340 private override init () { |
332 } | 341 } |
333 } | 342 } |
334 | 343 |
335 /// Enumerates errors that can be thrown while detecting or downloading icons. | 344 /// Enumerates errors that can be thrown while detecting or downloading icons. |
336 enum IconError: Error { | 345 enum IconError: Error { |
337 /// The base URL specified is not a valid URL. | 346 /// The base URL specified is not a valid URL. |
338 case invalidBaseURL | 347 case invalidBaseURL |
339 /// At least one icon to must be specified for downloading. | 348 /// At least one icon to must be specified for downloading. |
340 case atLeastOneOneIconRequired | 349 case atLeastOneOneIconRequired |
341 /// Unexpected response when downloading | 350 /// Unexpected response when downloading |
342 case invalidDownloadResponse | 351 case invalidDownloadResponse |
343 /// No icons were detected, so nothing could be downloaded. | 352 /// No icons were detected, so nothing could be downloaded. |
344 case noIconsDetected | 353 case noIconsDetected |
345 } | 354 } |
346 | 355 |
347 extension FavIcon { | 356 extension FavIcon { |
348 /// Convenience overload for `scan(url:completion:)` that takes a `String` | 357 /// Convenience overload for `scan(url:completion:)` that takes a `String` |
349 /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
ot a valid URL. | 358 /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
ot a valid URL. |
350 /// | 359 /// |
351 /// - parameter url: The base URL to scan. | 360 /// - parameter url: The base URL to scan. |
352 /// - parameter completion: A closure to call when the scan has completed. T
he closure will be called | 361 /// - parameter completion: A closure to call when the scan has completed. T
he closure will be called |
353 /// on the main queue. | 362 /// on the main queue. |
354 /// - throws: An `IconError` if the scan failed for some reason. | 363 /// - throws: An `IconError` if the scan failed for some reason. |
355 @objc public static func scan(_ url: String, completion: @escaping ([Detecte
dIcon], [String:String]) -> Void) throws { | 364 @objc |
| 365 public static func scan(_ url: String, completion: @escaping ([DetectedIcon]
, [String:String]) -> Void) throws { |
356 guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 366 guard let url = URL(string: url) else { throw IconError.invalidBaseURL } |
357 scan(url, completion: completion) | 367 scan(url, completion: completion) |
358 } | 368 } |
359 | 369 |
360 /// Convenience overload for `downloadAll(url:completion:)` that takes a `St
ring` | 370 /// Convenience overload for `downloadAll(url:completion:)` that takes a `St
ring` |
361 /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
ot a valid URL. | 371 /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
ot a valid URL. |
362 /// | 372 /// |
363 /// - parameter url: The URL to scan for icons. | 373 /// - parameter url: The URL to scan for icons. |
364 /// - parameter completion: A closure to call when all download tasks have r
esults available | 374 /// - parameter completion: A closure to call when all download tasks have r
esults available |
365 /// (successful or otherwise). The closure will be c
alled on the main queue. | 375 /// (successful or otherwise). The closure will be c
alled on the main queue. |
366 /// - throws: An `IconError` if the scan or download failed for some reason. | 376 /// - throws: An `IconError` if the scan or download failed for some reason. |
367 @objc public static func downloadAll(_ url: String, completion: @escaping ([
ImageType]) -> Void) throws { | 377 @objc |
| 378 public static func downloadAll(_ url: String, completion: @escaping ([ImageT
ype]) -> Void) throws { |
368 guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 379 guard let url = URL(string: url) else { throw IconError.invalidBaseURL } |
369 downloadAll(url, completion: completion) | 380 downloadAll(url, completion: completion) |
370 } | 381 } |
371 | 382 |
372 /// Convenience overload for `downloadPreferred(url:width:height:completion:
)` that takes a `String` | 383 /// Convenience overload for `downloadPreferred(url:width:height:completion:
)` that takes a `String` |
373 /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
ot a valid URL. | 384 /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
ot a valid URL. |
374 /// | 385 /// |
375 /// - parameter url: The URL to scan for icons. | 386 /// - parameter url: The URL to scan for icons. |
376 /// - parameter width: The preferred icon width, in pixels, or `nil`. | 387 /// - parameter width: The preferred icon width, in pixels, or `nil`. |
377 /// - parameter height: The preferred icon height, in pixels, or `nil`. | 388 /// - parameter height: The preferred icon height, in pixels, or `nil`. |
378 /// - parameter completion: A closure to call when the download task has pro
duced a result. The closure will | 389 /// - parameter completion: A closure to call when the download task has pro
duced a result. The closure will |
379 /// be called on the main queue. | 390 /// be called on the main queue. |
380 /// - throws: An appropriate `IconError` if downloading failed for some reas
on. | 391 /// - throws: An appropriate `IconError` if downloading failed for some reas
on. |
381 @objc public static func downloadPreferred(_ url: String, | 392 @objc |
| 393 public static func downloadPreferred(_ url: String, |
382 width: Int, | 394 width: Int, |
383 height: Int, | 395 height: Int, |
384 completion: @escaping (ImageType?) -> V
oid) throws { | 396 completion: @escaping (ImageType?) -> V
oid) throws { |
385 guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 397 guard let url = URL(string: url) else { throw IconError.invalidBaseURL } |
386 downloadPreferred(url, width: width, height: height, completion: complet
ion) | 398 downloadPreferred(url, width: width, height: height, completion: complet
ion) |
387 } | 399 } |
388 } | 400 } |
389 | 401 |
390 extension DetectedIcon { | 402 extension DetectedIcon { |
391 /// The area of a detected icon, if known. | 403 /// The area of a detected icon, if known. |
392 var area: Int? { | 404 var area: Int? { |
393 if let width = width, let height = height { | 405 if let width = width, let height = height { |
394 return width * height | 406 return width * height |
395 } | 407 } |
396 return nil | 408 return nil |
397 } | 409 } |
398 } | 410 } |
399 | |
400 | |
OLD | NEW |