PHLivePhotoView+Kingfisher.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. //
  2. // PHLivePhotoView+Kingfisher.swift
  3. // Kingfisher
  4. //
  5. // Created by onevcat on 2024/10/04.
  6. //
  7. // Copyright (c) 2024 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. #if os(watchOS)
  27. // Only a placeholder.
  28. public struct RetrieveLivePhotoResult: @unchecked Sendable {
  29. }
  30. #else
  31. @preconcurrency import PhotosUI
  32. /// A result type that contains the information of a retrieved live photo.
  33. ///
  34. /// This struct is used to encapsulate the result of a live photo retrieval operation, including the loading information,
  35. /// the retrieved `PHLivePhoto` object, and any additional information provided by the result handler.
  36. ///
  37. /// - Note: The `info` dictionary is considered sendable based on the documentation for "Result Handler Info Dictionary Keys".
  38. /// See: [Result Handler Info Dictionary Keys](https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys)
  39. public struct RetrieveLivePhotoResult: @unchecked Sendable {
  40. /// The loading information of the live photo.
  41. public let loadingInfo: LivePhotoLoadingInfoResult
  42. /// The retrieved live photo object which is given by the
  43. /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method from
  44. /// the result handler.
  45. public let livePhoto: PHLivePhoto?
  46. // According to "Result Handler Info Dictionary Keys", we can trust the `info` in handler is sendable.
  47. // https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys
  48. /// The additional information provided by the result handler when retrieving the live photo.
  49. public let info: [AnyHashable : Any]?
  50. }
  51. @MainActor private var taskIdentifierKey: Void?
  52. @MainActor private var targetSizeKey: Void?
  53. @MainActor private var contentModeKey: Void?
  54. @MainActor
  55. extension KingfisherWrapper where Base: PHLivePhotoView {
  56. /// Gets the task identifier associated with the image view for the live photo task.
  57. public private(set) var taskIdentifier: Source.Identifier.Value? {
  58. get {
  59. let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
  60. return box?.value
  61. }
  62. set {
  63. let box = newValue.map { Box($0) }
  64. setRetainedAssociatedObject(base, &taskIdentifierKey, box)
  65. }
  66. }
  67. /// The target size of the live photo view. It is used in the
  68. /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as
  69. /// the `targetSize` argument when loading the live photo.
  70. ///
  71. /// If not set, `.zero` will be used.
  72. public var targetSize: CGSize {
  73. get { getAssociatedObject(base, &targetSizeKey) ?? .zero }
  74. set { setRetainedAssociatedObject(base, &targetSizeKey, newValue) }
  75. }
  76. /// The content mode of the live photo view. It is used in the
  77. /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as
  78. /// the `contentMode` argument when loading the live photo.
  79. ///
  80. /// If not set, `.default` will be used.
  81. public var contentMode: PHImageContentMode {
  82. get { getAssociatedObject(base, &contentModeKey) ?? .default }
  83. set { setRetainedAssociatedObject(base, &contentModeKey, newValue) }
  84. }
  85. /// Sets a live photo to the view with an array of `URL`.
  86. ///
  87. /// - Parameters:
  88. /// - urls: The `URL`s defining the live photo resource. It should contains two URLs, one for the still image and
  89. /// one for the video.
  90. /// - options: An options set to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.
  91. /// - completionHandler: Called when the image setting process finishes.
  92. /// - Returns: A task represents the image downloading.
  93. /// The return value will be `nil` if the image is set with a empty source.
  94. ///
  95. /// - Note: Not all options in ``KingfisherOptionsInfo`` are supported in this method, for example, the live photo
  96. /// does not support any custom processors. Different from the extension method for a normal image view on the
  97. /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future.
  98. ///
  99. /// - Note: To get refined control of the resources, use the ``setImage(with:options:completionHandler:)-1n4p2``
  100. /// method with a ``LivePhotoSource`` object.
  101. ///
  102. /// Example:
  103. ///
  104. /// ```swift
  105. /// let urls = [
  106. /// URL(string: "https://example.com/image.heic")!, // imageURL
  107. /// URL(string: "https://example.com/video.mov")! // videoURL
  108. /// ]
  109. /// let livePhotoView = PHLivePhotoView()
  110. /// livePhotoView.kf.setImage(with: urls) { result in
  111. /// switch result {
  112. /// case .success(let retrieveResult):
  113. /// print("Live photo loaded: \(retrieveResult.livePhoto).")
  114. /// print("Cache type: \(retrieveResult.loadingInfo.cacheType).")
  115. /// case .failure(let error):
  116. /// print("Error: \(error)")
  117. /// }
  118. /// ```
  119. @discardableResult
  120. public func setImage(
  121. with urls: [URL],
  122. // placeholder: KFCrossPlatformImage? = nil, // Not supported yet
  123. options: KingfisherOptionsInfo? = nil,
  124. // progressBlock: DownloadProgressBlock? = nil, // Not supported yet
  125. completionHandler: (@MainActor @Sendable (Result<RetrieveLivePhotoResult, KingfisherError>) -> Void)? = nil
  126. ) -> Task<(), Never>? {
  127. setImage(
  128. with: LivePhotoSource(urls: urls),
  129. options: options,
  130. completionHandler: completionHandler
  131. )
  132. }
  133. /// Sets a live photo to the view with a ``LivePhotoSource``.
  134. ///
  135. /// - Parameters:
  136. /// - source: The ``LivePhotoSource`` object defining the live photo resource.
  137. /// - options: An options set to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.
  138. /// - completionHandler: Called when the image setting process finishes.
  139. /// - Returns: A task represents the image downloading.
  140. /// The return value will be `nil` if the image is set with a empty source.
  141. ///
  142. /// - Note: Not all options in ``KingfisherOptionsInfo`` are supported in this method, for example, the live photo
  143. /// does not support any custom processors. Different from the extension method for a normal image view on the
  144. /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future.
  145. ///
  146. /// Sample:
  147. /// ```swift
  148. /// let source = LivePhotoSource(urls: [
  149. /// URL(string: "https://example.com/image.heic")!, // imageURL
  150. /// URL(string: "https://example.com/video.mov")! // videoURL
  151. /// ])
  152. /// let livePhotoView = PHLivePhotoView()
  153. /// livePhotoView.kf.setImage(with: source) { result in
  154. /// switch result {
  155. /// case .success(let retrieveResult):
  156. /// print("Live photo loaded: \(retrieveResult.livePhoto).")
  157. /// print("Cache type: \(retrieveResult.loadingInfo.cacheType).")
  158. /// case .failure(let error):
  159. /// print("Error: \(error)")
  160. /// }
  161. /// ```
  162. @discardableResult
  163. public func setImage(
  164. with source: LivePhotoSource?,
  165. // placeholder: KFCrossPlatformImage? = nil, // Not supported yet
  166. options: KingfisherOptionsInfo? = nil,
  167. // progressBlock: DownloadProgressBlock? = nil, // Not supported yet
  168. completionHandler: (@MainActor @Sendable (Result<RetrieveLivePhotoResult, KingfisherError>) -> Void)? = nil
  169. ) -> Task<(), Never>? {
  170. var mutatingSelf = self
  171. // Empty source fails the loading early and clear the current task identifier.
  172. guard let source = source else {
  173. base.livePhoto = nil
  174. mutatingSelf.taskIdentifier = nil
  175. completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
  176. return nil
  177. }
  178. let issuedIdentifier = Source.Identifier.next()
  179. mutatingSelf.taskIdentifier = issuedIdentifier
  180. let taskIdentifierChecking = { issuedIdentifier == self.taskIdentifier }
  181. // Copy these associated values to prevent issues from reentrance.
  182. let targetSize = targetSize
  183. let contentMode = contentMode
  184. let task = Task { @MainActor in
  185. do {
  186. let loadingInfo = try await KingfisherManager.shared.retrieveLivePhoto(
  187. with: source,
  188. options: options,
  189. progressBlock: nil, // progressBlock, // Not supported yet
  190. referenceTaskIdentifierChecker: taskIdentifierChecking
  191. )
  192. if let notCurrentTaskError = self.checkNotCurrentTask(
  193. issuedIdentifier: issuedIdentifier,
  194. result: .init(loadingInfo: loadingInfo, livePhoto: nil, info: nil),
  195. error: nil,
  196. source: source
  197. ) {
  198. completionHandler?(.failure(notCurrentTaskError))
  199. return
  200. }
  201. PHLivePhoto.request(
  202. withResourceFileURLs: loadingInfo.fileURLs,
  203. placeholderImage: nil,
  204. targetSize: targetSize,
  205. contentMode: contentMode,
  206. resultHandler: { livePhoto, info in
  207. let result = RetrieveLivePhotoResult(
  208. loadingInfo: loadingInfo,
  209. livePhoto: livePhoto,
  210. info: info
  211. )
  212. if let notCurrentTaskError = self.checkNotCurrentTask(
  213. issuedIdentifier: issuedIdentifier,
  214. result: result,
  215. error: nil,
  216. source: source
  217. ) {
  218. completionHandler?(.failure(notCurrentTaskError))
  219. return
  220. }
  221. base.livePhoto = livePhoto
  222. if let error = info[PHLivePhotoInfoErrorKey] as? NSError {
  223. let failingReason: KingfisherError.ImageSettingErrorReason =
  224. .livePhotoResultError(result: result, error: error, source: source)
  225. completionHandler?(.failure(.imageSettingError(reason: failingReason)))
  226. return
  227. }
  228. // Since we are not returning the request ID, seems no way for user to cancel it if the
  229. // `request` method is called. However, we are sure the request method will always load the
  230. // image from disk, it should not be a problem. In case we still report the error in the
  231. // completion
  232. if (info[PHLivePhotoInfoCancelledKey] as? NSNumber)?.boolValue ?? false {
  233. completionHandler?(.failure(
  234. .requestError(reason: .livePhotoTaskCancelled(source: source)))
  235. )
  236. return
  237. }
  238. // If the PHLivePhotoInfoIsDegradedKey value in your result handler’s info dictionary is true,
  239. // Photos will call your result handler again.
  240. if (info[PHLivePhotoInfoIsDegradedKey] as? NSNumber)?.boolValue == true {
  241. // This ensures `completionHandler` be only called once.
  242. return
  243. }
  244. completionHandler?(.success(result))
  245. }
  246. )
  247. } catch {
  248. if let notCurrentTaskError = self.checkNotCurrentTask(
  249. issuedIdentifier: issuedIdentifier,
  250. result: nil,
  251. error: error,
  252. source: source
  253. ) {
  254. completionHandler?(.failure(notCurrentTaskError))
  255. return
  256. }
  257. if let kfError = error as? KingfisherError {
  258. completionHandler?(.failure(kfError))
  259. } else if error is CancellationError {
  260. completionHandler?(.failure(.requestError(reason: .livePhotoTaskCancelled(source: source))))
  261. } else {
  262. completionHandler?(.failure(.imageSettingError(
  263. reason: .livePhotoResultError(result: nil, error: error, source: source)))
  264. )
  265. }
  266. }
  267. }
  268. return task
  269. }
  270. private func checkNotCurrentTask(
  271. issuedIdentifier: Source.Identifier.Value,
  272. result: RetrieveLivePhotoResult?,
  273. error: (any Error)?,
  274. source: LivePhotoSource
  275. ) -> KingfisherError? {
  276. if issuedIdentifier == self.taskIdentifier {
  277. return nil
  278. }
  279. return .imageSettingError(reason: .notCurrentLivePhotoSourceTask(result: result, error: error, source: source))
  280. }
  281. }
  282. #endif