Browse Source

Add Kingfisher manager support for live photo

onevcat 1 year ago
parent
commit
297e0f732a

+ 8 - 0
Kingfisher.xcodeproj/project.pbxproj

@@ -67,6 +67,8 @@
 		D12E0C581C47F23500AC98AD /* UIButtonExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */; };
 		D12EB83C24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */; };
 		D12F67602CAC2DBF00AB63AB /* ImageDownloader+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F675F2CAC2DB700AB63AB /* ImageDownloader+LivePhoto.swift */; };
+		D12F67622CAC32BF00AB63AB /* KingfisherManager+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */; };
+		D12F67642CAC330A00AB63AB /* LivePhotoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67632CAC330600AB63AB /* LivePhotoSource.swift */; };
 		D13646742165A1A100A33652 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = D13646732165A1A100A33652 /* Result.swift */; };
 		D16CC3D624E02E9500F1A515 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */; };
 		D16FEA3A23078C63006E67D5 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = D16FE9F623078C63006E67D5 /* LICENSE */; };
@@ -215,6 +217,8 @@
 		D12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensionTests.swift; sourceTree = "<group>"; };
 		D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Kingfisher.swift"; sourceTree = "<group>"; };
 		D12F675F2CAC2DB700AB63AB /* ImageDownloader+LivePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageDownloader+LivePhoto.swift"; sourceTree = "<group>"; };
+		D12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KingfisherManager+LivePhoto.swift"; sourceTree = "<group>"; };
+		D12F67632CAC330600AB63AB /* LivePhotoSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoSource.swift; sourceTree = "<group>"; };
 		D1356CEA2B273AEC009554C8 /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = "<group>"; };
 		D13646732165A1A100A33652 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
 		D16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVAssetImageDataProvider.swift; sourceTree = "<group>"; };
@@ -415,6 +419,7 @@
 				D1132C9625919F69003E528D /* KFOptionsSetter.swift */,
 				D12AB6B2215D2BB50013BA68 /* KingfisherError.swift */,
 				D12AB6B3215D2BB50013BA68 /* KingfisherManager.swift */,
+				D12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */,
 				D12AB6B4215D2BB50013BA68 /* KingfisherOptionsInfo.swift */,
 			);
 			path = General;
@@ -656,6 +661,7 @@
 			isa = PBXGroup;
 			children = (
 				D1A1CC99219FAB4B00263AD8 /* Source.swift */,
+				D12F67632CAC330600AB63AB /* LivePhotoSource.swift */,
 				D12AB69E215D2BB50013BA68 /* Resource.swift */,
 				D1E56444219B16330057AAE3 /* ImageDataProvider.swift */,
 				D16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */,
@@ -829,11 +835,13 @@
 				D16CC3D624E02E9500F1A515 /* AVAssetImageDataProvider.swift in Sources */,
 				D1839845216E333E003927D3 /* Delegate.swift in Sources */,
 				D12AB6D8215D2BB50013BA68 /* ImageTransition.swift in Sources */,
+				D12F67642CAC330A00AB63AB /* LivePhotoSource.swift in Sources */,
 				D1A37BE8215D365A009B39B7 /* ExtensionHelpers.swift in Sources */,
 				C9286407228584EB00257182 /* ImageProgressive.swift in Sources */,
 				D12AB6DC215D2BB50013BA68 /* ImageProcessor.swift in Sources */,
 				D12AB6D4215D2BB50013BA68 /* Image.swift in Sources */,
 				D1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */,
+				D12F67622CAC32BF00AB63AB /* KingfisherManager+LivePhoto.swift in Sources */,
 				4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */,
 				E9E3ED8B2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift in Sources */,
 				D1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */,

+ 8 - 0
Sources/Cache/ImageCache.swift

@@ -954,6 +954,14 @@ open class ImageCache: @unchecked Sendable {
         return diskStorage.cacheFileURL(forKey: computedKey).path
     }
     
+    open func cacheFileURLIfOnDisk(
+        forKey key: String,
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> URL?
+    {
+        let computedKey = key.computedKey(with: identifier)
+        return diskStorage.isCached(forKey: computedKey) ? diskStorage.cacheFileURL(forKey: computedKey) : nil
+    }
+    
     // MARK: - Concurrency
     
     /// Stores an image to the cache.

+ 40 - 0
Sources/General/ImageSource/LivePhotoSource.swift

@@ -0,0 +1,40 @@
+//
+//  LivePhotoSource.swift
+//  Kingfisher
+//
+//  Created by onevcat on 2024/10/01.
+//
+//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+import Foundation
+
+public struct LivePhotoSource: Sendable {
+    
+    public let resources: [any Resource]
+    
+    public init(resources: [any Resource]) {
+        self.resources = resources
+    }
+    
+    public init(urls: [URL]) {
+        self.resources = urls.map { KF.ImageResource(downloadURL: $0) }
+    }
+}

+ 165 - 0
Sources/General/KingfisherManager+LivePhoto.swift

@@ -0,0 +1,165 @@
+//
+//  KingfisherManager+LivePhoto.swift
+//  Kingfisher
+//
+//  Created by onevcat on 2024/10/01.
+//
+//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+@preconcurrency import Photos
+
+public struct RetrieveLivePhotoResult: Sendable {
+    
+    /// Retrieves the live photo disk URLs from this result.
+    public let fileURLs: [URL]
+
+    /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved.
+    ///
+    /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned.
+    /// Otherwise, ``CacheType/disk`` will be returned for the live photo. ``CacheType/memory`` is not available for
+    /// live photos since it may take too much memory. All cached live photos are loaded from disk only.
+    public let cacheType: CacheType
+
+    /// The ``LivePhotoSource`` to which this result is related. This indicates where the `livePhoto` referenced by
+    /// `self` is located.
+    public let source: LivePhotoSource
+
+    /// The original ``LivePhotoSource`` from which the retrieval task begins. It may differ from the ``source`` property.
+    /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the
+    /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process.
+    public let originalSource: LivePhotoSource
+    
+    /// Retrieves the data associated with this result.
+    ///
+    /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns
+    /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache
+    /// serializer from the loading options and returns the result.
+    ///
+    /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to
+    /// use it multiple times and avoid frequent calls to this method.
+    public let data: @Sendable () -> [Data]
+}
+
+extension KingfisherManager {
+    public func retrieveLivePhoto(
+        with source: LivePhotoSource,
+        options: KingfisherOptionsInfo? = nil,
+        progressBlock: DownloadProgressBlock? = nil,
+        referenceTaskIdentifierChecker: (() -> Bool)? = nil
+    ) async throws -> RetrieveLivePhotoResult {
+        let fullOptions = currentDefaultOptions + (options ?? .empty)
+        var checkedOptions = KingfisherParsedOptionsInfo(fullOptions)
+        
+        if checkedOptions.processor == DefaultImageProcessor.default {
+            // The default processor is a default behavior so we replace it silently.
+            checkedOptions.processor = LivePhotoImageProcessor.default
+        } else if checkedOptions.processor != LivePhotoImageProcessor.default {
+            assertionFailure("[Kingfisher] Using of custom processors during loading of live photo resource is not supported.")
+            checkedOptions.processor = LivePhotoImageProcessor.default
+        }
+        
+        if let checker = referenceTaskIdentifierChecker {
+            checkedOptions.onDataReceived?.forEach {
+                $0.onShouldApply = checker
+            }
+        }
+        
+        // TODO. We ignore the retry of live photo now to suppress the complexity.
+        
+        let missingResources = missingResources(source, options: checkedOptions)
+        let resourcesResult = try await downloadAndCache(resources: missingResources, options: checkedOptions)
+        
+        let targetCache = checkedOptions.targetCache ?? cache
+        let fileURLs = source.resources.map {
+            targetCache.cacheFileURLIfOnDisk(
+                forKey: $0.cacheKey,
+                processorIdentifier: checkedOptions.processor.identifier
+            )
+        }
+        if fileURLs.contains(nil) {
+            // not all file done. throw error
+        }
+        return RetrieveLivePhotoResult(
+            fileURLs: fileURLs.compactMap { $0 },
+            cacheType: missingResources.isEmpty ? .disk : .none,
+            source: source,
+            originalSource: source,
+            data: {
+                resourcesResult.map { $0.originalData }
+            })
+    }
+    
+    func missingResources(_ source: LivePhotoSource, options: KingfisherParsedOptionsInfo) -> [any Resource] {
+        let missingResources: [any Resource]
+        if options.forceRefresh {
+            missingResources = source.resources
+        } else {
+            let targetCache = options.targetCache ?? cache
+            missingResources = source.resources.reduce([], { r, resource in
+                let cacheKey = resource.cacheKey
+                let existingCachedFileURL = targetCache.cacheFileURLIfOnDisk(
+                    forKey: cacheKey,
+                    processorIdentifier: options.processor.identifier
+                )
+                if existingCachedFileURL == nil {
+                    return r + [resource]
+                } else {
+                    return r
+                }
+            })
+        }
+        return missingResources
+    }
+    
+    private func downloadAndCache(
+        resources: [any Resource],
+        options: KingfisherParsedOptionsInfo
+    ) async throws -> [LivePhotoResourceLoadingResult] {
+        if resources.isEmpty {
+            return []
+        }
+        let downloader = options.downloader ?? downloader
+        let cache = options.targetCache ?? cache
+        return try await withThrowingTaskGroup(of: LivePhotoResourceLoadingResult.self) { group in
+            for resource in resources {
+                group.addTask {
+                    let downloadedResource = try await downloader.downloadLivePhotoResource(
+                        with: resource.downloadURL,
+                        options: options
+                    )
+                    try await cache.storeToDisk(
+                        downloadedResource.originalData,
+                        forKey: resource.cacheKey,
+                        processorIdentifier: options.processor.identifier,
+                        expiration: options.diskCacheExpiration
+                    )
+                    return downloadedResource
+                }
+            }
+            
+            var result: [LivePhotoResourceLoadingResult] = []
+            for try await resource in group {
+                result.append(resource)
+            }
+            return result
+        }
+    }
+}

+ 8 - 8
Sources/General/KingfisherManager.swift

@@ -146,7 +146,7 @@ public class KingfisherManager: @unchecked Sendable {
     public var defaultOptions = KingfisherOptionsInfo.empty
     
     // Use `defaultOptions` to overwrite the `downloader` and `cache`.
-    private var currentDefaultOptions: KingfisherOptionsInfo {
+    var currentDefaultOptions: KingfisherOptionsInfo {
         return [.downloader(downloader), .targetCache(cache)] + defaultOptions
     }
 
@@ -384,7 +384,7 @@ public class KingfisherManager: @unchecked Sendable {
     
     private func retrieveImage(
         with source: Source,
-        context: RetrievingContext,
+        context: RetrievingContext<Source>,
         completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
     {
         let options = context.options
@@ -457,7 +457,7 @@ public class KingfisherManager: @unchecked Sendable {
     private func cacheImage(
         source: Source,
         options: KingfisherParsedOptionsInfo,
-        context: RetrievingContext,
+        context: RetrievingContext<Source>,
         result: Result<ImageLoadingResult, KingfisherError>,
         completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?
     )
@@ -519,7 +519,7 @@ public class KingfisherManager: @unchecked Sendable {
     @discardableResult
     func loadAndCacheImage(
         source: Source,
-        context: RetrievingContext,
+        context: RetrievingContext<Source>,
         completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
     {
         let options = context.options
@@ -582,7 +582,7 @@ public class KingfisherManager: @unchecked Sendable {
     ///
     func retrieveImageFromCache(
         source: Source,
-        context: RetrievingContext,
+        context: RetrievingContext<Source>,
         completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> Bool
     {
         let options = context.options
@@ -863,7 +863,7 @@ extension KingfisherManager {
     }
 }
 
-class RetrievingContext: @unchecked Sendable {
+class RetrievingContext<SourceType>: @unchecked Sendable {
 
     private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.RetrievingContextPropertyQueue")
     
@@ -873,10 +873,10 @@ class RetrievingContext: @unchecked Sendable {
         set { propertyQueue.sync { _options = newValue } }
     }
 
-    let originalSource: Source
+    let originalSource: SourceType
     var propagationErrors: [PropagationError] = []
 
-    init(options: KingfisherParsedOptionsInfo, originalSource: Source) {
+    init(options: KingfisherParsedOptionsInfo, originalSource: SourceType) {
         self.originalSource = originalSource
         _options = options
     }