Explorar el Código

Add documentation for most components of live photo support

onevcat hace 1 año
padre
commit
b10ee06327

+ 40 - 0
Sources/Cache/ImageCache.swift

@@ -396,6 +396,8 @@ open class ImageCache: @unchecked Sendable {
     ///   - key: The key used for caching the image.
     ///   - identifier: The identifier of the processor being used for caching. If you are using a processor for the 
     ///   image, pass the identifier of the processor to this parameter.
+    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the
+    ///   disk storage configuration instead.
     ///   - serializer: The ``CacheSerializer`` used to convert the `image` and `original` to the data that will be
     ///   stored to disk. By default, the ``DefaultCacheSerializer/default`` will be used.
     ///   - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory.
@@ -441,6 +443,22 @@ open class ImageCache: @unchecked Sendable {
         )
     }
     
+    /// Store some data to the disk.
+    /// 
+    /// - Parameters:
+    ///   - data: The data to be stored.
+    ///   - key: The key used for caching the data.
+    ///   - identifier: The identifier of the processor being used for caching. If you are using a processor for the
+    ///   image, pass the identifier of the processor to this parameter.
+    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the
+    ///   disk storage configuration instead.
+    ///   - expiration: The expiration policy used by this storage action.
+    ///   - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is
+    ///   ``CallbackQueue/untouch``. Under this default ``CallbackQueue/untouch`` queue, if `toDisk` is `false`, it
+    ///   means the `completionHandler` will be invoked from the caller queue of this method; if `toDisk` is `true`,
+    ///   the `completionHandler` will be called from an internal file IO queue. To change this behavior, specify
+    ///   another ``CallbackQueue`` value.
+    ///   - completionHandler: A closure that is invoked when the cache operation finishes.
     open func storeToDisk(
         _ data: Data,
         forKey key: String,
@@ -510,6 +528,8 @@ open class ImageCache: @unchecked Sendable {
     ///   - key: The key used for caching the image.
     ///   - identifier: The identifier of the processor being used for caching. If you are using a processor for the 
     ///   image, pass the identifier of the processor to this parameter.
+    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the
+    ///   disk storage configuration instead.
     ///   - fromMemory: Whether this image should be removed from memory storage or not. If `false`, the image won't be 
     ///   removed from the memory storage. The default is `true`.
     ///   - fromDisk: Whether this image should be removed from the disk storage or not. If `false`, the image won't be
@@ -888,6 +908,9 @@ open class ImageCache: @unchecked Sendable {
     ///   - key: The key used for caching the image.
     ///   - identifier: The processor identifier used for this image. The default value is the
     ///    ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.
+    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the
+    ///   disk storage configuration instead.
+    /// 
     /// - Returns: A ``CacheType`` instance that indicates the cache status. ``CacheType/none`` indicates that the
     /// image is not in the cache or that it has already expired.
     open func imageCachedType(
@@ -908,6 +931,9 @@ open class ImageCache: @unchecked Sendable {
     ///   - key: The key used for caching the image.
     ///   - identifier: The processor identifier used for this image. The default value is the
     ///    ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.
+    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the
+    ///   disk storage configuration instead.
+    ///
     /// - Returns: A `Bool` value indicating whether a cache matches the given `key` and `identifier` combination.
     ///
     /// > The return value does not contain information about the kind of storage the cache matches from.
@@ -928,6 +954,9 @@ open class ImageCache: @unchecked Sendable {
     ///   - key: The key used for caching the image.
     ///   - identifier: The processor identifier used for this image. The default value is the
     ///    ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.
+    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the
+    ///   disk storage configuration instead.
+    /// 
     /// - Returns: The hash used as the cache file name.
     ///
     /// > By default, for a given combination of `key` and `identifier`, the ``ImageCache`` instance uses the value
@@ -972,6 +1001,9 @@ open class ImageCache: @unchecked Sendable {
     ///   - key: The key used for caching the image.
     ///   - identifier: The processor identifier used for this image. The default value is the
     ///    ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.
+    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the
+    ///   disk storage configuration instead.
+    /// 
     /// - Returns: The disk path of the cached image under the given `key` and `identifier`.
     ///
     /// > This method does not guarantee that there is an image already cached in the returned path. It simply provides
@@ -989,6 +1021,14 @@ open class ImageCache: @unchecked Sendable {
         return diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension).path
     }
     
+    /// Returns the file URL if a disk cache file is existing for the target key, identifier and forcedExtension
+    /// combination. Otherwise, if the requested cache value is not on the disk as a file, `nil`.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the item.
+    ///   - identifier: The processor identifier used for this image. It involves into calculating the final cache key.
+    ///   - forcedExtension: The expected extension of the file.
+    /// - Returns: The file URL if a disk cache file is existing for the combination. Otherwise, `nil`.
     open func cacheFileURLIfOnDisk(
         forKey key: String,
         processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,

+ 45 - 3
Sources/Extensions/PHLivePhotoView+Kingfisher.swift

@@ -31,12 +31,26 @@ public struct RetrieveLivePhotoResult: @unchecked Sendable {
 #else
 @preconcurrency import PhotosUI
 
+/// A result type that contains the information of a retrieved live photo.
+///
+/// This struct is used to encapsulate the result of a live photo retrieval operation, including the loading information,
+/// the retrieved `PHLivePhoto` object, and any additional information provided by the result handler.
+///
+/// - Note: The `info` dictionary is considered sendable based on the documentation for "Result Handler Info Dictionary Keys".
+///         See: [Result Handler Info Dictionary Keys](https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys)
 public struct RetrieveLivePhotoResult: @unchecked Sendable {
+    /// The loading information of the live photo.
     public let loadingInfo: LivePhotoLoadingInfoResult
+
+    /// The retrieved live photo object which is given by the 
+    /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method from
+    /// the result handler.
     public let livePhoto: PHLivePhoto?
     
+
     // According to "Result Handler Info Dictionary Keys", we can trust the `info` in handler is sendable.
     // https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys
+    /// The additional information provided by the result handler when retrieving the live photo.
     public let info: [AnyHashable : Any]?
 }
 
@@ -46,7 +60,7 @@ public struct RetrieveLivePhotoResult: @unchecked Sendable {
 
 @MainActor
 extension KingfisherWrapper where Base: PHLivePhotoView {
-    
+    /// Gets the task identifier associated with the image view for the live photo task.
     public private(set) var taskIdentifier: Source.Identifier.Value? {
         get {
             let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
@@ -58,16 +72,38 @@ extension KingfisherWrapper where Base: PHLivePhotoView {
         }
     }
     
+    /// The target size of the live photo view. It is used in the 
+    /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as 
+    /// the `targetSize` argument when loading the live photo. 
+    /// 
+    /// If not set, `.zero` will be used.
     public var targetSize: CGSize {
         get { getAssociatedObject(base, &targetSizeKey) ?? .zero }
         set { setRetainedAssociatedObject(base, &targetSizeKey, newValue) }
     }
     
+    /// The content mode of the live photo view. It is used in the
+    /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as
+    /// the `contentMode` argument when loading the live photo.
+    /// 
+    /// If not set, `.default` will be used.
     public var contentMode: PHImageContentMode {
         get { getAssociatedObject(base, &contentModeKey) ?? .default }
         set { setRetainedAssociatedObject(base, &contentModeKey, newValue) }
     }
     
+    /// Sets a live photo to the view with a `LivePhotoSource`.
+    ///
+    /// - Parameters:
+    ///   - source: The `LivePhotoSource` object defining the live photo resource.
+    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
+    ///   - completionHandler: Called when the image setting process finishes.
+    /// - Returns: A task represents the image downloading.
+    ///            The return value will be `nil` if the image is set with a empty source.
+    ///
+    /// - Note: Not all options in `KingfisherOptionsInfo` are supported in this method, for example, the live photo
+    /// does not support any custom processors. Different from the extension method for a normal image view on the 
+    /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future.
     @discardableResult
     public func setImage(
         with source: LivePhotoSource?,
@@ -77,6 +113,8 @@ extension KingfisherWrapper where Base: PHLivePhotoView {
         completionHandler: (@MainActor @Sendable (Result<RetrieveLivePhotoResult, KingfisherError>) -> Void)? = nil
     ) -> Task<(), Never>? {
         var mutatingSelf = self
+
+        // Empty source fails the loading early and clear the current task identifier.
         guard let source = source else {
             base.livePhoto = nil
             mutatingSelf.taskIdentifier = nil
@@ -89,7 +127,7 @@ extension KingfisherWrapper where Base: PHLivePhotoView {
         
         let taskIdentifierChecking = { issuedIdentifier == self.taskIdentifier }
 
-        // Copy these associated values in case of re-entry.
+        // Copy these associated values to prevent issues from reentrance.
         let targetSize = targetSize
         let contentMode = contentMode
         
@@ -142,6 +180,10 @@ extension KingfisherWrapper where Base: PHLivePhotoView {
                             return
                         }
                         
+                        // Since we are not returning the request ID, seems no way for user to cancel it if the 
+                        // `request` method is called. However, we are sure the request method will always load the 
+                        // image from disk, it should not be a problem. In case we still report the error in the 
+                        // completion
                         if (info[PHLivePhotoInfoCancelledKey] as? NSNumber)?.boolValue ?? false {
                             completionHandler?(.failure(
                                 .requestError(reason: .livePhotoTaskCancelled(source: source)))
@@ -152,7 +194,7 @@ extension KingfisherWrapper where Base: PHLivePhotoView {
                         // If the PHLivePhotoInfoIsDegradedKey value in your result handler’s info dictionary is true,
                         // Photos will call your result handler again.
                         if (info[PHLivePhotoInfoIsDegradedKey] as? NSNumber)?.boolValue == true {
-                            // This makes `completionHandler` be only called once.
+                            // This ensures `completionHandler` be only called once.
                             return
                         }
                         

+ 15 - 1
Sources/General/ImageSource/LivePhotoSource.swift

@@ -26,11 +26,26 @@
 
 import Foundation
 
+/// A resource type representing a component of a Live Photo, which consists of a still image and a video.
+///
+/// ``LivePhotoResource`` encapsulates the necessary information to download and cache a single components of a Live
+/// Photo: it is either a still image (typically in HEIC format) or a video (typically in MOV format). Multiple
+/// ``LivePhotoResource`` values (typically two, one for the image and one for the video) can form a ``LivePhotoSource``,
+/// which is expected by Kingfisher in its live photo loading high level APIs.
+///
+/// The Live Photo data can be retrieved by `PHAssetResourceManager.requestData` method and uploaded to your server.
+/// You should not modify the metadata or other information of the data, otherwise, it is possible that the
+/// `PHLivePhoto` class cannot read and recognize it anymore. For more information, please refer to Apple's
+/// documentation of Photos framework.
 public struct LivePhotoResource: Sendable {
     
+    /// The file type of a ``LivePhotoResource``.
     public enum FileType: Sendable, Equatable {
+        /// File type HEIC. Usually it represents the still image in a Live Photo.
         case heic
+        /// File type MOV. Usually it represents the video in a Live Photo.
         case mov
+        /// Other file types with the file extension.
         case other(String)
         
         var fileExtension: String {
@@ -40,7 +55,6 @@ public struct LivePhotoResource: Sendable {
             case .other(let ext): return ext
             }
         }
-        
     }
     
     public let resource: any Resource

+ 51 - 2
Sources/General/KingfisherManager+LivePhoto.swift

@@ -27,6 +27,7 @@
 #if !os(watchOS)
 @preconcurrency import Photos
 
+/// A structure that contains information about the result of loading a live photo.
 public struct LivePhotoLoadingInfoResult: Sendable {
     
     /// Retrieves the live photo disk URLs from this result.
@@ -60,6 +61,28 @@ public struct LivePhotoLoadingInfoResult: Sendable {
 }
 
 extension KingfisherManager {
+
+    /// Retrieves a live photo from the specified source.
+    ///
+    /// This method asynchronously loads a live photo from the given source, applying the specified options and
+    /// reporting progress if a progress block is provided.
+    ///
+    /// - Parameters:
+    ///   - source: The ``LivePhotoSource`` from which to retrieve the live photo.
+    ///   - options: A dictionary of options to apply to the retrieval process. If `nil`, the default options will be
+    ///   used.
+    ///   - progressBlock: An optional closure to be called periodically during the download process.
+    ///   - referenceTaskIdentifierChecker: An optional closure that returns a Boolean value indicating whether the task
+    ///   should proceed.
+    ///
+    /// - Returns: A ``LivePhotoLoadingInfoResult`` containing information about the retrieved live photo.
+    ///
+    /// - Throws: An error if the retrieval process fails.
+    ///
+    /// - Note: This method uses `LivePhotoImageProcessor` by default. Custom processors are not supported for live photos.
+    ///
+    /// - Warning: Not all options are working for this method. And currently the `progressBlock` is not working. 
+    /// It will be implemented in the future.
     public func retrieveLivePhoto(
         with source: LivePhotoSource,
         options: KingfisherOptionsInfo? = nil,
@@ -73,6 +96,7 @@ extension KingfisherManager {
             // The default processor is a default behavior so we replace it silently.
             checkedOptions.processor = LivePhotoImageProcessor.default
         } else if checkedOptions.processor != LivePhotoImageProcessor.default {
+            // Warn the framework user that the processor is not supported.
             assertionFailure("[Kingfisher] Using of custom processors during loading of live photo resource is not supported.")
             checkedOptions.processor = LivePhotoImageProcessor.default
         }
@@ -109,6 +133,8 @@ extension KingfisherManager {
             })
     }
     
+    // Returns the missing resources for the given source and options. If the resource is not in the cache, it will be
+    // returned as a missing resource.
     func missingResources(_ source: LivePhotoSource, options: KingfisherParsedOptionsInfo) -> [LivePhotoResource] {
         let missingResources: [LivePhotoResource]
         if options.forceRefresh {
@@ -116,6 +142,7 @@ extension KingfisherManager {
         } else {
             let targetCache = options.targetCache ?? cache
             missingResources = source.resources.reduce([], { r, resource in
+                // Check if the resource is in the cache. It includes a guess of the file extension.
                 let cachedFileURL = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: options)
                 if cachedFileURL == nil {
                     return r + [resource]
@@ -127,6 +154,9 @@ extension KingfisherManager {
         return missingResources
     }
     
+    // Download the resources and store them to the cache.
+    // If the resource does not specify a file extension (from either the URL extension or the explicit 
+    // `referenceFileType`), we infer it from the file signature.
     func downloadAndCache(
         resources: [LivePhotoResource],
         options: KingfisherParsedOptionsInfo
@@ -136,13 +166,21 @@ extension KingfisherManager {
         }
         let downloader = options.downloader ?? downloader
         let cache = options.targetCache ?? cache
-        return try await withThrowingTaskGroup(of: LivePhotoResourceDownloadingResult.self) { group in
+
+        // Download all resources concurrently.
+        return try await withThrowingTaskGroup(of: LivePhotoResourceDownloadingResult.self) { 
+            group in
+            
             for resource in resources {
                 group.addTask {
                     let downloadedResource = try await downloader.downloadLivePhotoResource(
                         with: resource.downloadURL,
                         options: options
                     )
+
+                    // We need to specify the extension so the file is saved correctly. Live photo loading requires
+                    // the file extension to be correct. Otherwise, a 3302 error will be thrown.
+                    // https://developer.apple.com/documentation/photokit/phphotoserror/code/invalidresource
                     let fileExtension = resource.referenceFileType
                         .determinedFileExtension(downloadedResource.originalData)
                     try await cache.storeToDisk(
@@ -178,6 +216,13 @@ extension ImageCache {
         )
     }
     
+    // Returns the possible cache file URL for the given key and processor identifier. If the file is on disk, it will
+    // return the URL. Otherwise, it will return `nil`.
+    //
+    // This method also tries to guess the file extension if it is not specified in the `referenceFileType`. 
+    // `PHLivePhoto`'s `request` method requires the file extension to be correct on the disk, and we also stored the 
+    // downloaded data with the correct extension (if it is not specified in the `referenceFileType`, we infer it from
+    // the file signature. See `FileType.determinedFileExtension` for more).
     func possibleCacheFileURLIfOnDisk(
         forKey key: String,
         processorIdentifier identifier: String,
@@ -185,23 +230,27 @@ extension ImageCache {
     ) -> URL? {
         switch referenceFileType {
         case .heic, .mov:
+            // The extension is specified and is what necessary to load a live photo, use it.
             return cacheFileURLIfOnDisk(
                 forKey: key, processorIdentifier: identifier, forcedExtension: referenceFileType.fileExtension
             )
         case .other(let ext):
             if ext.isEmpty {
-                // The extension is not specified. Guess from the default values.
+                // The extension is not specified. Guess from the default set of values.
                 let possibleFileTypes: [LivePhotoResource.FileType] = [.heic, .mov]
                 for fileType in possibleFileTypes {
                     let url = cacheFileURLIfOnDisk(
                         forKey: key, processorIdentifier: identifier, forcedExtension: fileType.fileExtension
                     )
                     if url != nil {
+                        // Found, early return.
                         return url
                     }
                 }
                 return nil
             } else {
+                // The extension is specified but maybe not valid for live photo. Trust the user and use it to find the
+                // file.
                 return cacheFileURLIfOnDisk(
                     forKey: key, processorIdentifier: identifier, forcedExtension: ext
                 )