diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..d0a3e904e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*.swift] +indent_style = space +indent_size = 4 +tab_width = 4 +end_of_line = crlf +insert_final_newline = false +max_line_length = 120 +trim_trailing_whitespace = true diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 4faa7c6ae..59da93a47 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -128,6 +128,7 @@ 0CDB92801DAF9BB900002905 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 0CDB92821DAF9BC600002905 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 0CDB92831DAF9BCB00002905 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + 207CFF4E2F92C1F8007289D8 /* .editorconfig */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = .editorconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -275,6 +276,7 @@ 0C9174861BAE99EE004A7905 = { isa = PBXGroup; children = ( + 207CFF4E2F92C1F8007289D8 /* .editorconfig */, 0C096C7B1BAE9ADD007FE380 /* Sources */, 0C7C06551BCA87EC00089D7F /* Tests */, 0C9174911BAE99EE004A7905 /* Products */, diff --git a/Sources/Nuke/Decoding/ImageDecoderRegistry.swift b/Sources/Nuke/Decoding/ImageDecoderRegistry.swift index 735baddc0..d8d3ab40e 100644 --- a/Sources/Nuke/Decoding/ImageDecoderRegistry.swift +++ b/Sources/Nuke/Decoding/ImageDecoderRegistry.swift @@ -9,7 +9,7 @@ public final class ImageDecoderRegistry: @unchecked Sendable { /// A shared registry. public static let shared = ImageDecoderRegistry() - private var matches = [(ImageDecodingContext) -> (any ImageDecoding)?]() + private var matches = [(ImageDecodingContext) -> (any BaseImageDecoding)?]() private let lock = NSLock() /// Initializes a custom registry. @@ -18,7 +18,7 @@ public final class ImageDecoderRegistry: @unchecked Sendable { } /// Returns a decoder that matches the given context. - public func decoder(for context: ImageDecodingContext) -> (any ImageDecoding)? { + public func decoder(for context: ImageDecodingContext) -> (any BaseImageDecoding)? { for match in matches.reversed() { if let decoder = match(context) { return decoder @@ -34,7 +34,7 @@ public final class ImageDecoderRegistry: @unchecked Sendable { /// The decoder is created once and is used for the entire decoding session, /// including progressively decoded images. If the decoder doesn't support /// progressive decoding, return `nil` when `isCompleted` is `false`. - public func register(_ match: @escaping (ImageDecodingContext) -> (any ImageDecoding)?) { + public func register(_ match: @escaping (ImageDecodingContext) -> (any BaseImageDecoding)?) { lock.withLock { matches.append(match) } } diff --git a/Sources/Nuke/Decoding/ImageDecoding.swift b/Sources/Nuke/Decoding/ImageDecoding.swift index fa2bb7057..9f62aa3eb 100644 --- a/Sources/Nuke/Decoding/ImageDecoding.swift +++ b/Sources/Nuke/Decoding/ImageDecoding.swift @@ -4,13 +4,15 @@ import Foundation +public protocol BaseImageDecoding: Sendable {} + /// An image decoder. /// /// A decoder is a one-shot object created for a single image decoding session. /// /// - note: If you need additional information in the decoder, you can pass /// anything that you might need from the ``ImageDecodingContext``. -public protocol ImageDecoding: Sendable { +public protocol ImageDecoding: BaseImageDecoding { /// Returns `true` if you want the decoding to be performed on the decoding /// queue (see ``ImagePipeline/Configuration-swift.struct/imageDecodingQueue``). If `false`, the decoding will be /// performed synchronously on the pipeline operation queue. By default, `true`. @@ -36,6 +38,24 @@ extension ImageDecoding { public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { nil } } +public protocol AsyncImageDecoding: BaseImageDecoding { + /// Produces an image from the given image data. + func decode(_ data: Data) async throws -> ImageContainer + + /// Produces an image from the given partially downloaded image data. + /// This method might be called multiple times during a single decoding + /// session. When the image download is complete, ``decode(_:)`` method is called. + /// + /// - returns: nil by default. + func decodePartiallyDownloadedData(_ data: Data) async -> ImageContainer? +} + +extension AsyncImageDecoding { + /// The default implementation which simply returns `nil` (no progressive + /// decoding available). + public func decodePartiallyDownloadedData(_ data: Data) async -> ImageContainer? { nil } +} + public enum ImageDecodingError: Error, CustomStringConvertible, Sendable { case unknown @@ -62,3 +82,26 @@ extension ImageDecoding { return ImageResponse(container: container, request: context.request, urlResponse: context.urlResponse, cacheType: context.cacheType) } } + +extension AsyncImageDecoding { + func decode(_ context: ImageDecodingContext) async throws -> ImageResponse { + // autoreleasepool does not support async-await + let container: ImageContainer = try await { + if context.isCompleted { + return try await decode(context.data) + } else { + if let preview = await decodePartiallyDownloadedData(context.data) { + return preview + } + throw ImageDecodingError.unknown + } + }() + +#if !os(macOS) + if container.userInfo[.isThumbnailKey] == nil && !container.isPreview { + ImageDecompression.setDecompressionNeeded(true, for: container.image) + } +#endif + return ImageResponse(container: container, request: context.request, urlResponse: context.urlResponse, cacheType: context.cacheType) + } +} diff --git a/Sources/Nuke/Internal/Log.swift b/Sources/Nuke/Internal/Log.swift index 641ca737e..93742959f 100644 --- a/Sources/Nuke/Internal/Log.swift +++ b/Sources/Nuke/Internal/Log.swift @@ -24,6 +24,17 @@ func signpost(_ name: StaticString, _ work: () throws -> T) rethrows -> T { return result } +func signpost(_ name: StaticString, _ work: () async throws -> T) async rethrows -> T { + guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return try await work() } + + let log = log.value + let signpostId = OSSignpostID(log: log) + os_signpost(.begin, log: log, name: name, signpostID: signpostId) + let result = try await work() + os_signpost(.end, log: log, name: name, signpostID: signpostId) + return result +} + private let log = Mutex(value: OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading")) enum Formatter { diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Cache.swift b/Sources/Nuke/Pipeline/ImagePipeline+Cache.swift index 38812e1bb..579c359b0 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Cache.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Cache.swift @@ -49,7 +49,7 @@ extension ImagePipeline.Cache { /// - request: The request. Make sure to remove the processors if you want /// to retrieve an original image (if it's stored). /// - caches: `[.all]`, by default. - public func cachedImage(for request: ImageRequest, caches: Caches = [.all]) -> ImageContainer? { + public func cachedImage(for request: ImageRequest, caches: Caches = [.all]) async -> ImageContainer? { if caches.contains(.memory) { if let image = cachedImageFromMemoryCache(for: request) { return image @@ -57,7 +57,7 @@ extension ImagePipeline.Cache { } if caches.contains(.disk) { if let data = cachedData(for: request), - let image = decodeImageData(data, for: request) { + let image = await decodeImageData(data, for: request) { return image } } @@ -222,12 +222,20 @@ extension ImagePipeline.Cache { // MARK: Private - private func decodeImageData(_ data: Data, for request: ImageRequest) -> ImageContainer? { + private func decodeImageData(_ data: Data, for request: ImageRequest) async -> ImageContainer? { let context = ImageDecodingContext(request: request, data: data, cacheType: .disk) guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else { return nil } - return (try? decoder.decode(context))?.container + + switch decoder { + case let decoder as ImageDecoding: + return (try? decoder.decode(context))?.container + case let decoder as AsyncImageDecoding: + return (try? await decoder.decode(context))?.container + default: + fatalError("Invalid BaseImageDecoding Implementation") + } } private func encodeImage(_ image: ImageContainer, for request: ImageRequest) -> Data? { diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift index fe7a2fcdb..ea9af7bcd 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift @@ -30,7 +30,7 @@ extension ImagePipeline { /// Default implementation uses shared ``ImageDecoderRegistry`` to create /// a decoder that matches the context. - public var makeImageDecoder: @Sendable (ImageDecodingContext) -> (any ImageDecoding)? = { + public var makeImageDecoder: @Sendable (ImageDecodingContext) -> (any BaseImageDecoding)? = { ImageDecoderRegistry.shared.decoder(for: $0) } diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift index 2e44794cc..cdbe52b91 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift @@ -13,7 +13,7 @@ extension ImagePipeline { // MARK: Misc /// Returns image decoder for the given context. - func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)? + func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any BaseImageDecoding)? /// Returns image encoder for the given context. func imageEncoder(for context: ImageEncodingContext, pipeline: ImagePipeline) -> any ImageEncoding @@ -126,7 +126,7 @@ extension ImagePipeline.Delegate { pipeline.configuration.dataCache } - public func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)? { + public func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any BaseImageDecoding)? { pipeline.configuration.makeImageDecoder(context) } diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Error.swift b/Sources/Nuke/Pipeline/ImagePipeline+Error.swift index f31d2f21f..3e7582695 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Error.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Error.swift @@ -19,7 +19,7 @@ extension ImagePipeline { /// By default, the pipeline uses ``ImageDecoders/Default`` as a catch-all. case decoderNotRegistered(context: ImageDecodingContext) /// Decoder failed to produce a final image. - case decodingFailed(decoder: any ImageDecoding, context: ImageDecodingContext, error: Swift.Error) + case decodingFailed(decoder: any BaseImageDecoding, context: ImageDecodingContext, error: Swift.Error) /// Processor failed to produce a final image. case processingFailed(processor: any ImageProcessing, context: ImageProcessingContext, error: Swift.Error) /// Load image method was called with no image request or no URL. diff --git a/Sources/Nuke/Tasks/AsyncPipelineTask.swift b/Sources/Nuke/Tasks/AsyncPipelineTask.swift index e8a72cb8d..1e4d41e00 100644 --- a/Sources/Nuke/Tasks/AsyncPipelineTask.swift +++ b/Sources/Nuke/Tasks/AsyncPipelineTask.swift @@ -39,20 +39,63 @@ extension AsyncPipelineTask: ImageTaskSubscribers { } extension AsyncPipelineTask { - /// Decodes the data on the dedicated queue and calls the completion - /// on the pipeline's internal queue. - func decode(_ context: ImageDecodingContext, decoder: any ImageDecoding, _ completion: @escaping @ImagePipelineActor (Result) -> Void) { - @Sendable func decode() -> Result { - signpost(context.isCompleted ? "DecodeImageData" : "DecodeProgressiveImageData") { - Result { try decoder.decode(context) } - .mapError { .decodingFailed(decoder: decoder, context: context, error: $0) } - } + @Sendable nonisolated func decode( + _ context: ImageDecodingContext, + using decoder: any ImageDecoding + ) -> Result { + signpost(context.isCompleted ? "DecodeImageData" : "DecodeProgressiveImageData") { + Result { try decoder.decode(context) } + .mapError { .decodingFailed(decoder: decoder, context: context, error: $0) } } - guard decoder.isAsynchronous else { - return completion(decode()) + } + + @Sendable nonisolated func decode( + _ context: ImageDecodingContext, + using decoder: any AsyncImageDecoding + ) async -> Result { + await signpost(context.isCompleted ? "DecodeImageData" : "DecodeProgressiveImageData") { + do { + return .success(try await decoder.decode(context)) + } catch { + let mappedError = ImagePipeline.Error.decodingFailed( + decoder: decoder, + context: context, + error: error + ) + + return .failure(mappedError) + } } - operation = pipeline.configuration.imageDecodingQueue.add { - completion(await performInBackground(decode)) + } + + /// Decodes the data on the dedicated queue and calls the completion + /// on the pipeline's internal queue. + func decode( + _ context: ImageDecodingContext, + decoder: any BaseImageDecoding, + _ completion: @escaping @ImagePipelineActor (Result) -> Void + ) { + switch decoder { + case let decoder as ImageDecoding where decoder.isAsynchronous: + return completion(decode(context, using: decoder)) + + case let decoder as ImageDecoding: + operation = pipeline.configuration.imageDecodingQueue.add { + let imageContainer = await performInBackground { + self.decode(context, using: decoder) + } + + completion(imageContainer) + } + + case let decoder as AsyncImageDecoding: + operation = pipeline.configuration.imageDecodingQueue.add { + let imageContainer = await self.decode(context, using: decoder) + completion(imageContainer) + } + + default: + fatalError("Invalid BaseImageDecoding Implementation") } } } diff --git a/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift b/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift index 7ef169308..1d4cbc2eb 100644 --- a/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift @@ -6,7 +6,7 @@ import Foundation /// Receives data from ``TaskLoadImageData`` and decodes it as it arrives. final class TaskFetchOriginalImage: AsyncPipelineTask, @unchecked Sendable { - private var decoder: (any ImageDecoding)? + private var decoder: (any BaseImageDecoding)? private var lastPreviewTime: CFAbsoluteTime? override func start() { @@ -76,7 +76,7 @@ final class TaskFetchOriginalImage: AsyncPipelineTask, @unchecked } // Lazily creates decoding for task - private func getDecoder(for context: ImageDecodingContext) -> (any ImageDecoding)? { + private func getDecoder(for context: ImageDecodingContext) -> (any BaseImageDecoding)? { // Return the existing processor in case it has already been created. if let decoder { return decoder