11import UIKit
2+ import CryptoKit
3+ import AVFoundation
24
35/// The system that downloads and caches images, and prepares them for display.
46@ImageDownloaderActor
@@ -27,21 +29,43 @@ public final class ImageDownloader {
2729 self . cache = cache
2830 }
2931
30- public func image( from url: URL , host: MediaHostProtocol ? = nil , options: ImageRequestOptions = . init( ) ) async throws -> UIImage {
32+ public func image(
33+ from url: URL ,
34+ host: MediaHostProtocol ? = nil ,
35+ options: ImageRequestOptions = ImageRequestOptions ( )
36+ ) async throws -> UIImage {
3137 try await image ( for: ImageRequest ( url: url, host: host, options: options) )
3238 }
3339
3440 public func image( for request: ImageRequest ) async throws -> UIImage {
3541 let options = request. options
3642 let key = makeKey ( for: request. source. url, size: options. size)
37- if options. isMemoryCacheEnabled, let image = cache [ key] {
43+
44+ if let cachedImage = try self . fetch ( key, options: request. options) {
45+ return cachedImage
46+ }
47+
48+ if case . video( let url, let mediaHost) = request. source {
49+ let asset : AVAsset = try await mediaHost? . authenticatedAsset ( for: url) ?? AVURLAsset ( url: url)
50+ let generator = AVAssetImageGenerator ( asset: asset)
51+
52+ if let size = options. size {
53+ generator. maximumSize = CGSize ( width: size. width, height: size. height)
54+ }
55+
56+ let result = try await generator. image ( at: . zero)
57+ let image = UIImage ( cgImage: result. image)
58+
59+ try store ( image, for: key, options: options)
60+
3861 return image
3962 }
63+
4064 let data = try await data ( for: request)
4165 let image = try await ImageDecoder . makeImage ( from: data, size: options. size. map ( CGSize . init) )
42- if options . isMemoryCacheEnabled {
43- cache [ key] = image
44- }
66+
67+ try store ( image , for : key, options : options )
68+
4569 return image
4670 }
4771
@@ -63,6 +87,8 @@ public final class ImageDownloader {
6387 return request
6488 case . urlRequest( let urlRequest) :
6589 return urlRequest
90+ case . video:
91+ preconditionFailure ( " Cannot make URLRequest for video – use AVFoundation APIs instead " )
6692 }
6793 }
6894
@@ -151,6 +177,53 @@ public final class ImageDownloader {
151177 throw ImageDownloaderError . unacceptableStatusCode ( response. statusCode)
152178 }
153179 }
180+
181+ // MARK: Manual caching
182+ private func fetch( _ key: String , options: ImageRequestOptions ) throws -> UIImage ? {
183+
184+ if options. isMemoryCacheEnabled, let image = cache [ key] {
185+ return image
186+ }
187+
188+ guard options. isDiskCacheEnabled else {
189+ return nil
190+ }
191+
192+ let path = path ( for: key)
193+
194+ guard FileManager . default. fileExists ( atPath: path. path ( ) ) else {
195+ return nil
196+ }
197+
198+ let pngData = try Data ( contentsOf: path)
199+ return UIImage ( data: pngData)
200+ }
201+
202+ private func store( _ image: UIImage , for key: String , options: ImageRequestOptions ) throws {
203+
204+ if options. isMemoryCacheEnabled {
205+ cache [ key] = image
206+ }
207+
208+ // If the image is immutable, store it in the disk cache "forever"
209+ guard options. isDiskCacheEnabled, options. mutability == . immutable else {
210+ return
211+ }
212+
213+ guard let data = image. pngData ( ) else {
214+ return
215+ }
216+
217+ let path = self . path ( for: key)
218+
219+ try FileManager . default. createDirectory ( at: path. deletingLastPathComponent ( ) , withIntermediateDirectories: true )
220+ try data. write ( to: path)
221+ }
222+
223+ private func path( for key: String ) -> URL {
224+ let hash = SHA256 . hash ( data: Data ( key. utf8) ) . map { String ( format: " %02x " , $0) } . joined ( )
225+ return URL . cachesDirectory. appending ( path: " image-cache " ) . appending ( path: hash)
226+ }
154227}
155228
156229@ImageDownloaderActor
@@ -196,4 +269,5 @@ private extension URLSession {
196269
197270public protocol MediaHostProtocol : Sendable {
198271 @MainActor func authenticatedRequest( for url: URL ) async throws -> URLRequest
272+ @MainActor func authenticatedAsset( for url: URL ) async throws -> AVURLAsset
199273}
0 commit comments