diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e56a2298..bc338ac58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,22 +10,22 @@ on: jobs: ios-latest: - name: Unit Tests (iOS 16.4, Xcode 14.3.1) - runs-on: macOS-13 + name: Unit Tests (iOS 17.4, Xcode 15.3) + runs-on: macOS-14 env: - DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests run: | - Scripts/test.sh -s "Nuke" -d "OS=16.4,name=iPhone 14 Pro" - Scripts/test.sh -s "NukeUI" -d "OS=16.4,name=iPhone 14 Pro" - Scripts/test.sh -s "NukeExtensions" -d "OS=16.4,name=iPhone 14 Pro" + Scripts/test.sh -s "Nuke" -d "OS=17.4,name=iPhone 15 Pro" + Scripts/test.sh -s "NukeUI" -d "OS=17.4,name=iPhone 15 Pro" + Scripts/test.sh -s "NukeExtensions" -d "OS=17.4,name=iPhone 15 Pro" macos-latest: - name: Unit Tests (macOS, Xcode 14.3.1) - runs-on: macOS-13 - env: - DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + name: Unit Tests (macOS, Xcode 15.3) + runs-on: macOS-14 + env: + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests @@ -34,17 +34,17 @@ jobs: Scripts/test.sh -s "NukeUI" -d "platform=macOS" Scripts/test.sh -s "NukeExtensions" -d "platform=macOS" tvos-latest: - name: Unit Tests (tvOS 16.4, Xcode 14.3.1) - runs-on: macOS-13 - env: - DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + name: Unit Tests (tvOS 17.4, Xcode 15.3) + runs-on: macOS-14 + env: + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests run: | - Scripts/test.sh -s "Nuke" -d "OS=16.4,name=Apple TV" - Scripts/test.sh -s "NukeUI" -d "OS=16.4,name=Apple TV" - Scripts/test.sh -s "NukeExtensions" -d "OS=16.4,name=Apple TV" + Scripts/test.sh -s "Nuke" -d "OS=17.4,name=Apple TV" + Scripts/test.sh -s "NukeUI" -d "OS=17.4,name=Apple TV" + Scripts/test.sh -s "NukeExtensions" -d "OS=17.4,name=Apple TV" # There is a problem with watchOS runners where they often fail to launch on CI # # watchos-latest: @@ -59,27 +59,27 @@ jobs: # Scripts/test.sh -s "Nuke" -d "OS=9.1,name=Apple Watch Series 8 (45mm)" # Scripts/test.sh -s "NukeUI" -d "OS=9.1,name=Apple Watch Series 8 (45mm)" # Scripts/test.sh -s "Nuke Extensions" -d "OS=9.1,name=Apple Watch Series 8 (45mm)" - ios-xcode-14-1: - name: Unit Tests (iOS 16.1, Xcode 14.1) + ios-xcode-14-3-1: + name: Unit Tests (iOS 16.4, Xcode 14.3.1) runs-on: macOS-13 env: - DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests run: | - Scripts/test.sh -s "Nuke" -d "OS=16.1,name=iPhone 14 Pro" - Scripts/test.sh -s "NukeUI" -d "OS=16.1,name=iPhone 14 Pro" - Scripts/test.sh -s "NukeExtensions" -d "OS=16.1,name=iPhone 14 Pro" + Scripts/test.sh -s "Nuke" -d "OS=16.4,name=iPhone 14 Pro" + Scripts/test.sh -s "NukeUI" -d "OS=16.4,name=iPhone 14 Pro" + Scripts/test.sh -s "NukeExtensions" -d "OS=16.4,name=iPhone 14 Pro" ios-thread-safety: name: Thread Safety Tests (TSan Enabled) - runs-on: macOS-13 - env: - DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + runs-on: macOS-14 + env: + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests - run: Scripts/test.sh -s "Nuke Thread Safety Tests" -d "OS=16.4,name=iPhone 14 Pro" + run: Scripts/test.sh -s "Nuke Thread Safety Tests" -d "OS=17.4,name=iPhone 15 Pro" # ios-memory-management-tests: # name: Memory Management Tests # runs-on: macOS-13 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7952e3e4d..f3305a4aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,38 @@ # Nuke 12 +## Nuke 12.6 + +*Apr 23, 2024* + +- Fix an issue with an optimization that is supposed to skip decompression if one or more processors are applied +- Fix a `[Decompressor] Error -17102 decompressing image -- possibly corrupt` console error message when using `ImagePipeline.Configuration.isUsingPrepareForDisplay` (disabled by default). The pipeline will now skip decompression for `.png`. +- Fix https://github.com/kean/Nuke/issues/705 with integration between thumbnail options (link) and original data caching: the original data is now stored without a thumbnail key +- Fix an issue where `.storeAll` and `.automatic` cache policies would not store the thumbnail data +- Fix https://github.com/kean/Nuke/issues/746 an issue with `ImageRequest.UserInfoKey.scaleKey` not interacting correctly with coalescing +- Fix https://github.com/kean/Nuke/issues/763 SwiftUI Warning: Accessing StateObject's object without being installed on a View when using `onStart` +- Fix https://github.com/kean/Nuke/issues/758 by adding support for initializing `ImageProcessors.CoreImageFilter` with `CIFilter` instances +- Add support for disk cache lookup for intermediate processed images (as opposed to only final and original as before) +- Add an optimization that loads local resources with `file` and `data` schemes quickly without using `DataLoader` and `URLSession`. If you rely on the existing behavior, this optimization can be turned off using the `isLocalResourcesSupportEnabled` configuration option. https://github.com/kean/Nuke/pull/779 +- Deprecate `ImagePipeline.Configuration.dataCachingQueue` and perform data cache lookups on the pipeline's queue, reducing the amount of context switching +- Update the infrastructure for coalescing image-processing tasks to use the task-dependency used for other operations + +## Nuke 12.5 + +*Mar 23, 2024* + +- Fix Xcode 15.3 concurrency warnings when using `Screen.scale` by @jszumski in https://github.com/kean/Nuke/pull/766 +- Add `showPlaceholderOnFailure` parameter to show placeholder in case of image loading failure by @mlight3 in https://github.com/kean/Nuke/pull/764 +- Fix image loading test on iOS 17 by @woxtu in https://github.com/kean/Nuke/pull/768 +- Update thumbnail key value for `ImageRequest`` by @woxtu in https://github.com/kean/Nuke/pull/769 +- Remove trailing whitespaces by @woxtu in https://github.com/kean/Nuke/pull/767 +- Apply `if let` shorthand syntax by @mlight3 in https://github.com/kean/Nuke/pull/762 + ## Nuke 12.4 *Feb 10, 2024* -## What's Changed -* Enable visionOS support for all APIs by @zachwaugh in https://github.com/kean/Nuke/pull/752 -* Update documentation by @tkersey in https://github.com/kean/Nuke/pull/747 - -**Full Changelog**: https://github.com/kean/Nuke/compare/12.3.0...12.4.0 +- Enable visionOS support for all APIs by @zachwaugh in https://github.com/kean/Nuke/pull/752 +- Update documentation by @tkersey in https://github.com/kean/Nuke/pull/747 ## Nuke 12.3 diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 05838d73f..a641458dd 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -154,7 +154,6 @@ 0C8D7BED1D9DC02B00D12EB7 /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; }; 0C8D7BF51D9DC07E00D12EB7 /* DataCachePeformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D74201D9D6EEB0036349E /* DataCachePeformanceTests.swift */; }; 0C8DC723209B842600084AA6 /* cat.gif in Resources */ = {isa = PBXBuildFile; fileRef = 0C8DC722209B842600084AA6 /* cat.gif */; }; - 0C9165E626431942006B1D4F /* OperationTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9165E526431942006B1D4F /* OperationTask.swift */; }; 0C91B0EC2438E287007F9100 /* ResizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C91B0EB2438E287007F9100 /* ResizeTests.swift */; }; 0C91B0EE2438E307007F9100 /* CircleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C91B0ED2438E307007F9100 /* CircleTests.swift */; }; 0C91B0F02438E352007F9100 /* RoundedCornersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C91B0EF2438E352007F9100 /* RoundedCornersTests.swift */; }; @@ -200,8 +199,8 @@ 0CB26807208F25C2004C83F4 /* DataCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB26806208F25C2004C83F4 /* DataCacheTests.swift */; }; 0CB2EFD22110F38600F7C63F /* ImagePipelineConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB2EFD12110F38600F7C63F /* ImagePipelineConfigurationTests.swift */; }; 0CB2EFD62110F52C00F7C63F /* RateLimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB2EFD52110F52C00F7C63F /* RateLimiterTests.swift */; }; - 0CB402D525B6569700F5A241 /* TaskFetchOriginalImageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB402D425B6569700F5A241 /* TaskFetchOriginalImageData.swift */; }; - 0CB402DB25B656D200F5A241 /* TaskFetchDecodedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB402DA25B656D200F5A241 /* TaskFetchDecodedImage.swift */; }; + 0CB402D525B6569700F5A241 /* TaskFetchOriginalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */; }; + 0CB402DB25B656D200F5A241 /* TaskFetchOriginalImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */; }; 0CB4030125B6639200F5A241 /* TaskLoadImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4030025B6639200F5A241 /* TaskLoadImage.swift */; }; 0CB6448928567DC300916267 /* MockImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C068A1BCA888800089D7F /* MockImageProcessor.swift */; }; 0CB6448A28567DC300916267 /* MockDataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C068C1BCA888800089D7F /* MockDataLoader.swift */; }; @@ -447,7 +446,6 @@ 0C8D7BDE1D9DBF1600D12EB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0C8D7BE81D9DC02B00D12EB7 /* Nuke Performance Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Nuke Performance Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 0C8DC722209B842600084AA6 /* cat.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = cat.gif; sourceTree = ""; }; - 0C9165E526431942006B1D4F /* OperationTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationTask.swift; sourceTree = ""; }; 0C9174901BAE99EE004A7905 /* Nuke.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Nuke.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0C91B0EB2438E287007F9100 /* ResizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeTests.swift; sourceTree = ""; }; 0C91B0ED2438E307007F9100 /* CircleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleTests.swift; sourceTree = ""; }; @@ -495,8 +493,8 @@ 0CB26806208F25C2004C83F4 /* DataCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCacheTests.swift; sourceTree = ""; }; 0CB2EFD12110F38600F7C63F /* ImagePipelineConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineConfigurationTests.swift; sourceTree = ""; }; 0CB2EFD52110F52C00F7C63F /* RateLimiterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimiterTests.swift; sourceTree = ""; }; - 0CB402D425B6569700F5A241 /* TaskFetchOriginalImageData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchOriginalImageData.swift; sourceTree = ""; }; - 0CB402DA25B656D200F5A241 /* TaskFetchDecodedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchDecodedImage.swift; sourceTree = ""; }; + 0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchOriginalData.swift; sourceTree = ""; }; + 0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchOriginalImage.swift; sourceTree = ""; }; 0CB4030025B6639200F5A241 /* TaskLoadImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskLoadImage.swift; sourceTree = ""; }; 0CB6449928567DE000916267 /* NukeExtensionsTestsHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeExtensionsTestsHelpers.swift; sourceTree = ""; }; 0CB6449B28567E5400916267 /* ImageViewLoadingOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewLoadingOptionsTests.swift; sourceTree = ""; }; @@ -971,10 +969,9 @@ 0C2CD6EA25B67FB30017018F /* ImagePipelineTask.swift */, 0CB4030025B6639200F5A241 /* TaskLoadImage.swift */, 0C2A368A26437BF100F1D000 /* TaskLoadData.swift */, - 0CB402DA25B656D200F5A241 /* TaskFetchDecodedImage.swift */, - 0CB402D425B6569700F5A241 /* TaskFetchOriginalImageData.swift */, + 0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */, + 0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */, 0CE6202226543B6A00AAB8C3 /* TaskFetchWithPublisher.swift */, - 0C9165E526431942006B1D4F /* OperationTask.swift */, ); path = Tasks; sourceTree = ""; @@ -1731,12 +1728,11 @@ 0CA4ECD026E68FC000BAC8E5 /* DataCaching.swift in Sources */, 0CA4ECCA26E6868300BAC8E5 /* ImageProcessingOptions.swift in Sources */, 0C53C8B1263C968200E62D03 /* ImagePipelineDelegate.swift in Sources */, - 0C9165E626431942006B1D4F /* OperationTask.swift in Sources */, 0CA4ECBC26E6856300BAC8E5 /* ImageDecompression.swift in Sources */, 0CA4ECD326E68FDC00BAC8E5 /* ImageCaching.swift in Sources */, 0CA4ECC026E685C900BAC8E5 /* ImageProcessors+Anonymous.swift in Sources */, 0CA4ECC826E6864D00BAC8E5 /* ImageProcessors+RoundedCorners.swift in Sources */, - 0CB402DB25B656D200F5A241 /* TaskFetchDecodedImage.swift in Sources */, + 0CB402DB25B656D200F5A241 /* TaskFetchOriginalImage.swift in Sources */, 0C472F842654AD88007FC0F0 /* ImageRequestKeys.swift in Sources */, 0CE6202126542F7200AAB8C3 /* DataPublisher.swift in Sources */, 0CB0479A2856D9AC00DF9B6D /* Cache.swift in Sources */, @@ -1763,7 +1759,7 @@ 0CA4ECBE26E685A900BAC8E5 /* ImageProcessors+Circle.swift in Sources */, 0CE2D9BA2084FDDD00934B28 /* ImageDecoding.swift in Sources */, 0CC36A1925B8BC2500811018 /* RateLimiter.swift in Sources */, - 0CB402D525B6569700F5A241 /* TaskFetchOriginalImageData.swift in Sources */, + 0CB402D525B6569700F5A241 /* TaskFetchOriginalData.swift in Sources */, 0CA4ECAD26E683E300BAC8E5 /* ImageEncoders.swift in Sources */, 0CA4ECC626E6862A00BAC8E5 /* ImageProcessors+CoreImage.swift in Sources */, 0C2CD6EB25B67FB30017018F /* ImagePipelineTask.swift in Sources */, @@ -2221,7 +2217,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 12.4.0; + MARKETING_VERSION = 12.6.0; ONLY_ACTIVE_ARCH = YES; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; @@ -2280,12 +2276,10 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 12.4.0; + MARKETING_VERSION = 12.6.0; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; SUPPORTS_MACCATALYST = YES; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; diff --git a/README.md b/README.md index 1beedfbd3..d4ade57ab 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,16 @@

+

+> *Serving Images Since 2015* + Load images from different sources and display them in your app using simple and flexible APIs. Take advantage of the powerful image processing capabilities and a robust caching system. The framework is lean and compiles in under 2 seconds[¹](#footnote-1). It has an automated test suite 2x the codebase size, ensuring excellent reliability. Nuke is optimized for [performance](https://kean-docs.github.io/nuke/documentation/nuke/performance-guide), and its advanced architecture enables virtually unlimited possibilities for customization. -> **Fast LRU memory and disk cache** · **SwiftUI** · **Smart background decompression** · **Image processing** · **Resumable downloads** · **Intelligent deduplication** · **Request prioritization** · **Prefetching** · **Rate limiting** · **Progressive JPEG, HEIF, WebP, SVG, GIF** · **Alamofire** · **Combine** · **Async/Await** +> **Memory and Disk Cache** · **Image Processing & Decompression** · **Request Coalescing & Priority** · **Prefetching** · **Resumable Downloads** · **Progressive JPEG** · **HEIF, WebP, SVG, GIF** · **SwiftUI** · **Async/Await** ## Sponsors @@ -32,11 +35,42 @@ The framework is lean and compiles in under 2 seconds[¹](#footnote-1). It has a Nuke supports [Swift Package Manager](https://www.swift.org/package-manager/), which is the recommended option. If that doesn't work for you, you can use binary frameworks attached to the [releases](https://github.com/kean/Nuke/releases). +The package ships with four modules that you can install depending on your needs: + +|Module|Description| +|--|--| +|[**Nuke**](https://kean-docs.github.io/nuke/documentation/nuke)|The lean core framework with `ImagePipeline`, `ImageRequest`, and more| +|[**NukeUI**](https://kean-docs.github.io/nukeui/documentation/nukeui/)|The UI components: `LazyImage` (SwiftUI) and `ImageView` (UIKit, AppKit)| +|[**NukeExtensions**](https://kean-docs.github.io/nukeextensions/documentation/nukeextensions/)|The extensions for `UIImageView` (UIKit, AppKit)| +|[**NukeVideo**](https://kean-docs.github.io/nukevideo/documentation/nukevideo/)|The components for decoding and playing short videos| + ## Documentation -Nuke is easy to learn and use thanks to documentation generated using DocC: [**Nuke**](https://kean-docs.github.io/nuke/documentation/nuke/getting-started/), [**NukeUI**](https://kean-docs.github.io/nukeui/documentation/nukeui/), [**NukeExtensions**](https://kean-docs.github.io/nukeextensions/documentation/nukeextensions/). Make sure to also check out [**Nuke Demo**](https://github.com/kean/NukeDemo). +Nuke is easy to learn and use, thanks to its extensive documentation and a modern API. -> Upgrading from the previous version? Use a [**Migration Guide**](https://github.com/kean/Nuke/tree/master/Documentation/Migrations). +You can load images using `ImagePipeline` from the lean core [**Nuke**](https://kean-docs.github.io/nuke/documentation/nuke) module: + +```swift +func loadImage() async throws { + let imageTask = ImagePipeline.shared.imageTask(with: url) + for await progress in imageTask.progress { + // Update progress + } + imageView.image = try await imageTask.image +} +``` + +Or you can use the built-in UI components from the [**NukeUI**](https://kean-docs.github.io/nukeui/documentation/nukeui/) module: + +```swift +struct ContentView: View { + var body: some View { + LazyImage(url: URL(string: "https://example.com/image.jpeg")) + } +} +``` + +The [**Getting Started**](https://kean-docs.github.io/nuke/documentation/nuke/getting-started/) guide is the best place to start learning about these and many other APIs provided by the framework. Check out [**Nuke Demo**](https://github.com/kean/NukeDemo) for more usage examples. Nuke Docs @@ -58,6 +92,8 @@ The image pipeline is easy to customize and extend. Check out the following firs ## Minimum Requirements +> Upgrading from the previous version? Use a [**Migration Guide**](https://github.com/kean/Nuke/tree/master/Documentation/Migrations). + | Nuke | Date | Swift | Xcode | Platforms | |------------|--------------|-------------|------------|-----------------------------------------------| | Nuke 12.0 | Mar 4, 2023 | Swift 5.7 | Xcode 14.1 | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0 | diff --git a/Sources/Nuke/Caching/ImageCaching.swift b/Sources/Nuke/Caching/ImageCaching.swift index eaef111c6..37f408f29 100644 --- a/Sources/Nuke/Caching/ImageCaching.swift +++ b/Sources/Nuke/Caching/ImageCaching.swift @@ -24,7 +24,7 @@ public struct ImageCacheKey: Hashable, Sendable { // This is faster than using AnyHashable (and it shows in performance tests). enum Inner: Hashable, Sendable { case custom(String) - case `default`(CacheKey) + case `default`(MemoryCacheKey) } public init(key: String) { @@ -32,6 +32,6 @@ public struct ImageCacheKey: Hashable, Sendable { } public init(request: ImageRequest) { - self.key = .default(request.makeImageCacheKey()) + self.key = .default(MemoryCacheKey(request)) } } diff --git a/Sources/Nuke/Decoding/ImageDecoderRegistry.swift b/Sources/Nuke/Decoding/ImageDecoderRegistry.swift index 0232de83a..3c5b54650 100644 --- a/Sources/Nuke/Decoding/ImageDecoderRegistry.swift +++ b/Sources/Nuke/Decoding/ImageDecoderRegistry.swift @@ -62,7 +62,7 @@ public struct ImageDecodingContext: @unchecked Sendable { public var urlResponse: URLResponse? public var cacheType: ImageResponse.CacheType? - public init(request: ImageRequest, data: Data, isCompleted: Bool, urlResponse: URLResponse?, cacheType: ImageResponse.CacheType?) { + public init(request: ImageRequest, data: Data, isCompleted: Bool = true, urlResponse: URLResponse? = nil, cacheType: ImageResponse.CacheType? = nil) { self.request = request self.data = data self.isCompleted = isCompleted diff --git a/Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift b/Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift index b4f1aa5f7..89bfa172d 100644 --- a/Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift +++ b/Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift @@ -47,19 +47,21 @@ extension ImageEncoders { } public func encode(_ image: PlatformImage) -> Data? { - let data = NSMutableData() + guard let source = image.cgImage, + let data = CFDataCreateMutable(nil, 0), + let destination = CGImageDestinationCreateWithData(data, type.rawValue as CFString, 1, nil) else { + return nil + } var options: [CFString: Any] = [ kCGImageDestinationLossyCompressionQuality: compressionRatio ] #if canImport(UIKit) options[kCGImagePropertyOrientation] = CGImagePropertyOrientation(image.imageOrientation).rawValue #endif - guard let source = image.cgImage, - let destination = CGImageDestinationCreateWithData(data as CFMutableData, type.rawValue as CFString, 1, nil) else { - return nil - } CGImageDestinationAddImage(destination, source, options as CFDictionary) - CGImageDestinationFinalize(destination) + guard CGImageDestinationFinalize(destination) else { + return nil + } return data as Data } } diff --git a/Sources/Nuke/ImageRequest.swift b/Sources/Nuke/ImageRequest.swift index ce0c20200..5212c0ffa 100644 --- a/Sources/Nuke/ImageRequest.swift +++ b/Sources/Nuke/ImageRequest.swift @@ -367,7 +367,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri /// (``ImageProcessors/Resize``). /// /// - note: You must be using the default image decoder to make it work. - public static let thumbnailKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/thumbmnailKey" + public static let thumbnailKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/thumbnail" } /// Thumbnail options. diff --git a/Sources/Nuke/Internal/Graphics.swift b/Sources/Nuke/Internal/Graphics.swift index 4e06db882..7909062b6 100644 --- a/Sources/Nuke/Internal/Graphics.swift +++ b/Sources/Nuke/Internal/Graphics.swift @@ -314,7 +314,6 @@ extension CGSize { } } -@MainActor enum Screen { #if os(iOS) || os(tvOS) /// Returns the current screen scale. diff --git a/Sources/Nuke/Internal/ImageRequestKeys.swift b/Sources/Nuke/Internal/ImageRequestKeys.swift index 85dd8dd6a..90046776b 100644 --- a/Sources/Nuke/Internal/ImageRequestKeys.swift +++ b/Sources/Nuke/Internal/ImageRequestKeys.swift @@ -4,98 +4,73 @@ import Foundation -extension ImageRequest { - - // MARK: - Cache Keys - - /// A key for processed image in memory cache. - func makeImageCacheKey() -> CacheKey { - CacheKey(self) - } - - /// A key for processed image data in disk cache. - func makeDataCacheKey() -> String { - "\(preferredImageId)\(thumbnail?.identifier ?? "")\(ImageProcessors.Composition(processors).identifier)" - } - - // MARK: - Load Keys - - /// A key for deduplicating operations for fetching the processed image. - func makeImageLoadKey() -> ImageLoadKey { - ImageLoadKey(self) - } - - /// A key for deduplicating operations for fetching the decoded image. - func makeDecodedImageLoadKey() -> DecodedImageLoadKey { - DecodedImageLoadKey(self) - } - - /// A key for deduplicating operations for fetching the original image. - func makeDataLoadKey() -> DataLoadKey { - DataLoadKey(self) - } -} - /// Uniquely identifies a cache processed image. -final class CacheKey: Hashable, Sendable { +final class MemoryCacheKey: Hashable, Sendable { // Using a reference type turned out to be significantly faster private let imageId: String? + private let scale: Float private let thumbnail: ImageRequest.ThumbnailOptions? private let processors: [any ImageProcessing] init(_ request: ImageRequest) { self.imageId = request.preferredImageId + self.scale = request.scale ?? 1 self.thumbnail = request.thumbnail self.processors = request.processors } func hash(into hasher: inout Hasher) { hasher.combine(imageId) + hasher.combine(scale) hasher.combine(thumbnail) hasher.combine(processors.count) } - static func == (lhs: CacheKey, rhs: CacheKey) -> Bool { - lhs.imageId == rhs.imageId && lhs.thumbnail == rhs.thumbnail && lhs.processors == rhs.processors + static func == (lhs: MemoryCacheKey, rhs: MemoryCacheKey) -> Bool { + lhs.imageId == rhs.imageId && lhs.scale == rhs.scale && lhs.thumbnail == rhs.thumbnail && lhs.processors == rhs.processors } } +// MARK: - Identifying Tasks + /// Uniquely identifies a task of retrieving the processed image. -final class ImageLoadKey: Hashable, Sendable { - let cacheKey: CacheKey - let options: ImageRequest.Options - let loadKey: DataLoadKey +final class TaskLoadImageKey: Hashable, Sendable { + private let loadKey: TaskFetchOriginalImageKey + private let options: ImageRequest.Options + private let processors: [any ImageProcessing] init(_ request: ImageRequest) { - self.cacheKey = CacheKey(request) + self.loadKey = TaskFetchOriginalImageKey(request) self.options = request.options - self.loadKey = DataLoadKey(request) + self.processors = request.processors } func hash(into hasher: inout Hasher) { - hasher.combine(cacheKey.hashValue) - hasher.combine(options.hashValue) hasher.combine(loadKey.hashValue) + hasher.combine(options.hashValue) + hasher.combine(processors.count) } - static func == (lhs: ImageLoadKey, rhs: ImageLoadKey) -> Bool { - lhs.cacheKey == rhs.cacheKey && lhs.options == rhs.options && lhs.loadKey == rhs.loadKey + static func == (lhs: TaskLoadImageKey, rhs: TaskLoadImageKey) -> Bool { + lhs.loadKey == rhs.loadKey && lhs.options == rhs.options && lhs.processors == rhs.processors } } -/// Uniquely identifies a task of retrieving the decoded image. -struct DecodedImageLoadKey: Hashable { - let dataLoadKey: DataLoadKey - let thumbnail: ImageRequest.ThumbnailOptions? +/// Uniquely identifies a task of retrieving the original image. +struct TaskFetchOriginalImageKey: Hashable { + private let dataLoadKey: TaskFetchOriginalDataKey + private let scale: Float + private let thumbnail: ImageRequest.ThumbnailOptions? init(_ request: ImageRequest) { - self.dataLoadKey = DataLoadKey(request) + self.dataLoadKey = TaskFetchOriginalDataKey(request) + self.scale = request.scale ?? 1 self.thumbnail = request.thumbnail } } -/// Uniquely identifies a task of retrieving the original image dataa. -struct DataLoadKey: Hashable { +/// Uniquely identifies a task of retrieving the original image data. +struct TaskFetchOriginalDataKey: Hashable { private let imageId: String? private let cachePolicy: URLRequest.CachePolicy private let allowsCellularAccess: Bool @@ -112,13 +87,3 @@ struct DataLoadKey: Hashable { } } } - -struct ImageProcessingKey: Equatable, Hashable { - let imageId: ObjectIdentifier - let processorId: AnyHashable - - init(image: ImageResponse, processor: any ImageProcessing) { - self.imageId = ObjectIdentifier(image.image) - self.processorId = processor.hashableIdentifier - } -} diff --git a/Sources/Nuke/Internal/Log.swift b/Sources/Nuke/Internal/Log.swift index 4c5bf977a..475b9a63b 100644 --- a/Sources/Nuke/Internal/Log.swift +++ b/Sources/Nuke/Internal/Log.swift @@ -5,13 +5,6 @@ import Foundation import os -func signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType) { - guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return } - - let signpostId = OSSignpostID(log: log, object: object) - os_signpost(type, log: log, name: name, signpostID: signpostId) -} - func signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType, _ message: @autoclosure () -> String) { guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return } @@ -20,19 +13,10 @@ func signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType, } func signpost(_ name: StaticString, _ work: () throws -> T) rethrows -> T { - try signpost(name, "", work) -} - -func signpost(_ name: StaticString, _ message: @autoclosure () -> String, _ work: () throws -> T) rethrows -> T { guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return try work() } let signpostId = OSSignpostID(log: log) - let message = message() - if !message.isEmpty { - os_signpost(.begin, log: log, name: name, signpostID: signpostId, "%{public}s", message) - } else { - os_signpost(.begin, log: log, name: name, signpostID: signpostId) - } + os_signpost(.begin, log: log, name: name, signpostID: signpostId) let result = try work() os_signpost(.end, log: log, name: name, signpostID: signpostId) return result diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 34af0cacf..1c3324fbc 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -34,11 +34,10 @@ public final class ImagePipeline: @unchecked Sendable { private var tasks = [ImageTask: TaskSubscription]() - private let tasksLoadData: TaskPool - private let tasksLoadImage: TaskPool - private let tasksFetchDecodedImage: TaskPool - private let tasksFetchOriginalImageData: TaskPool - private let tasksProcessImage: TaskPool + private let tasksLoadData: TaskPool + private let tasksLoadImage: TaskPool + private let tasksFetchOriginalImage: TaskPool + private let tasksFetchOriginalData: TaskPool // The queue on which the entire subsystem is synchronized. let queue = DispatchQueue(label: "com.github.kean.Nuke.ImagePipeline", qos: .userInitiated) @@ -77,9 +76,8 @@ public final class ImagePipeline: @unchecked Sendable { let isCoalescingEnabled = configuration.isTaskCoalescingEnabled self.tasksLoadData = TaskPool(isCoalescingEnabled) self.tasksLoadImage = TaskPool(isCoalescingEnabled) - self.tasksFetchDecodedImage = TaskPool(isCoalescingEnabled) - self.tasksFetchOriginalImageData = TaskPool(isCoalescingEnabled) - self.tasksProcessImage = TaskPool(isCoalescingEnabled) + self.tasksFetchOriginalImage = TaskPool(isCoalescingEnabled) + self.tasksFetchOriginalData = TaskPool(isCoalescingEnabled) self.lock = .allocate(capacity: 1) self.lock.initialize(to: os_unfair_lock()) @@ -503,12 +501,11 @@ public final class ImagePipeline: @unchecked Sendable { // // `loadImage()` call is represented by TaskLoadImage: // - // TaskLoadImage -> TaskFetchDecodedImage -> TaskFetchOriginalImageData - // -> TaskProcessImage + // TaskLoadImage -> TaskFetchOriginalImage -> TaskFetchOriginalData // // `loadData()` call is represented by TaskLoadData: // - // TaskLoadData -> TaskFetchOriginalImageData + // TaskLoadData -> TaskFetchOriginalData // // // Each task represents a resource or a piece of work required to produce the @@ -518,33 +515,27 @@ public final class ImagePipeline: @unchecked Sendable { // is created. The work is split between tasks to minimize any duplicated work. func makeTaskLoadImage(for request: ImageRequest) -> AsyncTask.Publisher { - tasksLoadImage.publisherForKey(request.makeImageLoadKey()) { + tasksLoadImage.publisherForKey(TaskLoadImageKey(request)) { TaskLoadImage(self, request) } } func makeTaskLoadData(for request: ImageRequest) -> AsyncTask<(Data, URLResponse?), Error>.Publisher { - tasksLoadData.publisherForKey(request.makeImageLoadKey()) { + tasksLoadData.publisherForKey(TaskLoadImageKey(request)) { TaskLoadData(self, request) } } - func makeTaskProcessImage(key: ImageProcessingKey, process: @escaping () throws -> ImageResponse) -> AsyncTask.Publisher { - tasksProcessImage.publisherForKey(key) { - OperationTask(self, configuration.imageProcessingQueue, process) + func makeTaskFetchOriginalImage(for request: ImageRequest) -> AsyncTask.Publisher { + tasksFetchOriginalImage.publisherForKey(TaskFetchOriginalImageKey(request)) { + TaskFetchOriginalImage(self, request) } } - func makeTaskFetchDecodedImage(for request: ImageRequest) -> AsyncTask.Publisher { - tasksFetchDecodedImage.publisherForKey(request.makeDecodedImageLoadKey()) { - TaskFetchDecodedImage(self, request) - } - } - - func makeTaskFetchOriginalImageData(for request: ImageRequest) -> AsyncTask<(Data, URLResponse?), Error>.Publisher { - tasksFetchOriginalImageData.publisherForKey(request.makeDataLoadKey()) { + func makeTaskFetchOriginalData(for request: ImageRequest) -> AsyncTask<(Data, URLResponse?), Error>.Publisher { + tasksFetchOriginalData.publisherForKey(TaskFetchOriginalDataKey(request)) { request.publisher == nil ? - TaskFetchOriginalImageData(self, request) : + TaskFetchOriginalData(self, request) : TaskFetchWithPublisher(self, request) } } diff --git a/Sources/Nuke/Pipeline/ImagePipelineCache.swift b/Sources/Nuke/Pipeline/ImagePipelineCache.swift index 895c4fbdf..5637d3550 100644 --- a/Sources/Nuke/Pipeline/ImagePipelineCache.swift +++ b/Sources/Nuke/Pipeline/ImagePipelineCache.swift @@ -200,7 +200,7 @@ extension ImagePipeline.Cache { if let customKey = pipeline.delegate.cacheKey(for: request, pipeline: pipeline) { return customKey } - return request.makeDataCacheKey() // Use the default key + return "\(request.preferredImageId)\(request.thumbnail?.identifier ?? "")\(ImageProcessors.Composition(request.processors).identifier)" } // MARK: Misc @@ -222,7 +222,7 @@ extension ImagePipeline.Cache { // MARK: Private private func decodeImageData(_ data: Data, for request: ImageRequest) -> ImageContainer? { - let context = ImageDecodingContext(request: request, data: data, isCompleted: true, urlResponse: nil, cacheType: .disk) + let context = ImageDecodingContext(request: request, data: data, cacheType: .disk) guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else { return nil } diff --git a/Sources/Nuke/Pipeline/ImagePipelineConfiguration.swift b/Sources/Nuke/Pipeline/ImagePipelineConfiguration.swift index 59e1dcdde..64e27b912 100644 --- a/Sources/Nuke/Pipeline/ImagePipelineConfiguration.swift +++ b/Sources/Nuke/Pipeline/ImagePipelineConfiguration.swift @@ -114,6 +114,10 @@ extension ImagePipeline { /// `Last-Modified`). Resumable downloads are enabled by default. public var isResumableDataEnabled = true + /// If enabled, the pipeline will load the local resources (`file` and + /// `data` schemes) inline without using the data loader. By default, `true`. + public var isLocalResourcesSupportEnabled = true + /// A queue on which all callbacks, like `progress` and `completion` /// callbacks are called. `.main` by default. public var callbackQueue = DispatchQueue.main @@ -136,7 +140,8 @@ extension ImagePipeline { /// Data loading queue. Default maximum concurrent task count is 6. public var dataLoadingQueue = OperationQueue(maxConcurrentCount: 6) - /// Data caching queue. Default maximum concurrent task count is 2. + // Deprecated in Nuke 12.6 + @available(*, deprecated, message: "The pipeline now performs cache lookup on the internal queue, reducing the amount of context switching") public var dataCachingQueue = OperationQueue(maxConcurrentCount: 2) /// Image decoding queue. Default maximum concurrent task count is 1. diff --git a/Sources/Nuke/Prefetching/ImagePrefetcher.swift b/Sources/Nuke/Prefetching/ImagePrefetcher.swift index 88cb4ed60..a83bda1e1 100644 --- a/Sources/Nuke/Prefetching/ImagePrefetcher.swift +++ b/Sources/Nuke/Prefetching/ImagePrefetcher.swift @@ -53,7 +53,7 @@ public final class ImagePrefetcher: @unchecked Sendable { public var didComplete: (() -> Void)? private let pipeline: ImagePipeline - private var tasks = [ImageLoadKey: Task]() + private var tasks = [TaskLoadImageKey: Task]() private let destination: Destination private var _priority: ImageRequest.Priority = .low let queue = OperationQueue() // internal for testing @@ -122,7 +122,7 @@ public final class ImagePrefetcher: @unchecked Sendable { guard pipeline.cache[request] == nil else { return } - let key = request.makeImageLoadKey() + let key = TaskLoadImageKey(request) guard tasks[key] == nil else { return } @@ -189,7 +189,7 @@ public final class ImagePrefetcher: @unchecked Sendable { } private func _stopPrefetching(with request: ImageRequest) { - if let task = tasks.removeValue(forKey: request.makeImageLoadKey()) { + if let task = tasks.removeValue(forKey: TaskLoadImageKey(request)) { task.cancel() } } @@ -211,13 +211,13 @@ public final class ImagePrefetcher: @unchecked Sendable { } private final class Task: @unchecked Sendable { - let key: ImageLoadKey + let key: TaskLoadImageKey let request: ImageRequest weak var imageTask: ImageTask? weak var operation: Operation? var onCancelled: (() -> Void)? - init(request: ImageRequest, key: ImageLoadKey) { + init(request: ImageRequest, key: TaskLoadImageKey) { self.request = request self.key = key } diff --git a/Sources/Nuke/Processing/ImageDecompression.swift b/Sources/Nuke/Processing/ImageDecompression.swift index 83e2e771b..1e7e2e9fd 100644 --- a/Sources/Nuke/Processing/ImageDecompression.swift +++ b/Sources/Nuke/Processing/ImageDecompression.swift @@ -5,6 +5,18 @@ import Foundation enum ImageDecompression { + static func isDecompressionNeeded(for response: ImageResponse) -> Bool { + guard response.container.type != .png else { + // Attempting to decompress a `.png` image using + // `prepareForReuse` results in the following error: + // + // [Decompressor] Error -17102 decompressing image -- possibly corrupt + // + // It's also, in general, inefficient and unnecessary. + return false + } + return isDecompressionNeeded(for: response.image) ?? false + } static func decompress(image: PlatformImage, isUsingPrepareForDisplay: Bool = false) -> PlatformImage { image.decompressed(isUsingPrepareForDisplay: isUsingPrepareForDisplay) ?? image diff --git a/Sources/Nuke/Processing/ImageProcessors+CoreImage.swift b/Sources/Nuke/Processing/ImageProcessors+CoreImage.swift index 089bfc266..71cd73ea3 100644 --- a/Sources/Nuke/Processing/ImageProcessors+CoreImage.swift +++ b/Sources/Nuke/Processing/ImageProcessors+CoreImage.swift @@ -27,23 +27,36 @@ extension ImageProcessors { /// - [Core Image Programming Guide](https://developer.apple.com/library/ios/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html) /// - [Core Image Filter Reference](https://developer.apple.com/library/prerelease/ios/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html) public struct CoreImageFilter: ImageProcessing, CustomStringConvertible, @unchecked Sendable { - public let name: String - public let parameters: [String: Any] + let filter: Filter public let identifier: String + enum Filter { + case named(String, parameters: [String: Any]) + case custom(CIFilter) + } + + /// Initializes the processor with a name of the `CIFilter` and its parameters. + /// /// - parameter identifier: Uniquely identifies the processor. public init(name: String, parameters: [String: Any], identifier: String) { - self.name = name - self.parameters = parameters + self.filter = .named(name, parameters: parameters) self.identifier = identifier } + /// Initializes the processor with a name of the `CIFilter`. public init(name: String) { - self.name = name - self.parameters = [:] + self.filter = .named(name, parameters: [:]) self.identifier = "com.github.kean/nuke/core_image?name=\(name))" } + /// Initialize the processor with the given `CIFilter`. + /// + /// - parameter identifier: Uniquely identifies the processor. + public init(_ filter: CIFilter, identifier: String) { + self.filter = .custom(filter) + self.identifier = identifier + } + public func process(_ image: PlatformImage) -> PlatformImage? { try? _process(image) } @@ -53,7 +66,12 @@ extension ImageProcessors { } private func _process(_ image: PlatformImage) throws -> PlatformImage { - try CoreImageFilter.applyFilter(named: name, parameters: parameters, to: image) + switch filter { + case let .named(name, parameters): + return try CoreImageFilter.applyFilter(named: name, parameters: parameters, to: image) + case .custom(let filter): + return try CoreImageFilter.apply(filter: filter, to: image) + } } // MARK: - Apply Filter @@ -91,7 +109,12 @@ extension ImageProcessors { } public var description: String { - "CoreImageFilter(name: \(name), parameters: \(parameters))" + switch filter { + case let .named(name, parameters): + return "CoreImageFilter(name: \(name), parameters: \(parameters))" + case .custom(let filter): + return "CoreImageFilter(filter: \(filter))" + } } public enum Error: Swift.Error, CustomStringConvertible { diff --git a/Sources/Nuke/Processing/ImageProcessors.swift b/Sources/Nuke/Processing/ImageProcessors.swift index db1f6c5d0..796a505ac 100644 --- a/Sources/Nuke/Processing/ImageProcessors.swift +++ b/Sources/Nuke/Processing/ImageProcessors.swift @@ -101,6 +101,10 @@ extension ImageProcessing where Self == ImageProcessors.CoreImageFilter { public static func coreImageFilter(name: String) -> ImageProcessors.CoreImageFilter { ImageProcessors.CoreImageFilter(name: name) } + + public static func coreImageFilter(_ filter: CIFilter, identifier: String) -> ImageProcessors.CoreImageFilter { + ImageProcessors.CoreImageFilter(filter, identifier: identifier) + } } extension ImageProcessing where Self == ImageProcessors.GaussianBlur { diff --git a/Sources/Nuke/Tasks/AsyncTask.swift b/Sources/Nuke/Tasks/AsyncTask.swift index 5d55151da..f70cf6dbd 100644 --- a/Sources/Nuke/Tasks/AsyncTask.swift +++ b/Sources/Nuke/Tasks/AsyncTask.swift @@ -50,7 +50,6 @@ class AsyncTask: AsyncTaskSubscriptionDelegate guard oldValue != priority else { return } operation?.queuePriority = priority.queuePriority dependency?.setPriority(priority) - dependency2?.setPriority(priority) } } @@ -64,14 +63,6 @@ class AsyncTask: AsyncTaskSubscriptionDelegate } } - // The tasks only ever need up to 2 dependencies and this code is much faster - // than creating an array. - var dependency2: TaskSubscription? { - didSet { - dependency2?.setPriority(priority) - } - } - weak var operation: Foundation.Operation? { didSet { guard priority != .normal else { return } @@ -88,7 +79,7 @@ class AsyncTask: AsyncTaskSubscriptionDelegate // MARK: - Managing Observers /// - notes: Returns `nil` if the task was disposed. - private func subscribe(priority: TaskPriority = .normal, subscriber: AnyObject? = nil, _ closure: @escaping (Event) -> Void) -> TaskSubscription? { + private func subscribe(priority: TaskPriority = .normal, subscriber: AnyObject, _ closure: @escaping (Event) -> Void) -> TaskSubscription? { guard !isDisposed else { return nil } let subscriptionKey = nextSubscriptionKey @@ -111,7 +102,6 @@ class AsyncTask: AsyncTaskSubscriptionDelegate // The task may have been completed synchronously by `starter`. guard !isDisposed else { return nil } - return subscription } @@ -194,7 +184,6 @@ class AsyncTask: AsyncTaskSubscriptionDelegate if reason == .cancelled { operation?.cancel() dependency?.unsubscribe() - dependency2?.unsubscribe() onCancelled?() } onDisposed?() @@ -234,7 +223,7 @@ extension AsyncTask { /// Attaches the subscriber to the task. /// - notes: Returns `nil` if the task is already disposed. - func subscribe(priority: TaskPriority = .normal, subscriber: AnyObject? = nil, _ closure: @escaping (Event) -> Void) -> TaskSubscription? { + func subscribe(priority: TaskPriority = .normal, subscriber: AnyObject, _ closure: @escaping (Event) -> Void) -> TaskSubscription? { task.subscribe(priority: priority, subscriber: subscriber, closure) } diff --git a/Sources/Nuke/Tasks/ImagePipelineTask.swift b/Sources/Nuke/Tasks/ImagePipelineTask.swift index 2068a283a..b6dc6707e 100644 --- a/Sources/Nuke/Tasks/ImagePipelineTask.swift +++ b/Sources/Nuke/Tasks/ImagePipelineTask.swift @@ -36,3 +36,26 @@ extension ImagePipelineTask: ImageTaskSubscribers { } } } + +extension ImagePipelineTask { + /// 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 (Result) -> Void) { + @Sendable func decode() -> 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()) + } + operation = pipeline.configuration.imageDecodingQueue.add { [weak self] in + guard let self else { return } + let response = decode() + self.pipeline.queue.async { + completion(response) + } + } + } +} diff --git a/Sources/Nuke/Tasks/OperationTask.swift b/Sources/Nuke/Tasks/OperationTask.swift deleted file mode 100644 index fd3da5331..000000000 --- a/Sources/Nuke/Tasks/OperationTask.swift +++ /dev/null @@ -1,35 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// A one-shot task for performing a single () -> T function. -final class OperationTask: AsyncTask { - private let pipeline: ImagePipeline - private let queue: OperationQueue - private let process: () throws -> T - - init(_ pipeline: ImagePipeline, _ queue: OperationQueue, _ process: @escaping () throws -> T) { - self.pipeline = pipeline - self.queue = queue - self.process = process - } - - override func start() { - operation = queue.add { [weak self] in - guard let self else { return } - let result = Result(catching: { try self.process() }) - self.pipeline.queue.async { - switch result { - case .success(let value): - self.send(value: value, isCompleted: true) - case .failure(let error): - self.send(error: error) - } - } - } - } - - struct Error: Swift.Error {} -} diff --git a/Sources/Nuke/Tasks/TaskFetchOriginalImageData.swift b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift similarity index 89% rename from Sources/Nuke/Tasks/TaskFetchOriginalImageData.swift rename to Sources/Nuke/Tasks/TaskFetchOriginalData.swift index c4e1dc37d..53200ea8f 100644 --- a/Sources/Nuke/Tasks/TaskFetchOriginalImageData.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift @@ -6,19 +6,29 @@ import Foundation /// Fetches original image from the data loader (`DataLoading`) and stores it /// in the disk cache (`DataCaching`). -final class TaskFetchOriginalImageData: ImagePipelineTask<(Data, URLResponse?)> { +final class TaskFetchOriginalData: ImagePipelineTask<(Data, URLResponse?)> { private var urlResponse: URLResponse? private var resumableData: ResumableData? private var resumedDataCount: Int64 = 0 private var data = Data() override func start() { - guard let urlRequest = request.urlRequest else { + guard let urlRequest = request.urlRequest, let url = urlRequest.url else { // A malformed URL prevented a URL request from being initiated. send(error: .dataLoadingFailed(error: URLError(.badURL))) return } + if url.isLocalResource && pipeline.configuration.isLocalResourcesSupportEnabled { + do { + let data = try Data(contentsOf: url) + send(value: (data, nil), isCompleted: true) + } catch { + send(error: .dataLoadingFailed(error: error)) + } + return + } + if let rateLimiter = pipeline.rateLimiter { // Rate limiter is synchronized on pipeline's queue. Delayed work is // executed asynchronously also on the same queue. @@ -159,6 +169,7 @@ final class TaskFetchOriginalImageData: ImagePipelineTask<(Data, URLResponse?)> extension ImagePipelineTask where Value == (Data, URLResponse?) { func storeDataInCacheIfNeeded(_ data: Data) { + let request = makeSanitizedRequest() guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), shouldStoreDataInDiskCache() else { return } @@ -170,7 +181,17 @@ extension ImagePipelineTask where Value == (Data, URLResponse?) { } } + /// Returns a request that doesn't contain any information non-related + /// to data loading. + private func makeSanitizedRequest() -> ImageRequest { + var request = request + request.processors = [] + request.userInfo[.thumbnailKey] = nil + return request + } + private func shouldStoreDataInDiskCache() -> Bool { + let imageTasks = imageTasks guard imageTasks.contains(where: { !$0.request.options.contains(.disableDiskCacheWrites) }) else { return false } diff --git a/Sources/Nuke/Tasks/TaskFetchDecodedImage.swift b/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift similarity index 57% rename from Sources/Nuke/Tasks/TaskFetchDecodedImage.swift rename to Sources/Nuke/Tasks/TaskFetchOriginalImage.swift index 04f4fecbe..38ff464ae 100644 --- a/Sources/Nuke/Tasks/TaskFetchDecodedImage.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift @@ -5,16 +5,16 @@ import Foundation /// Receives data from ``TaskLoadImageData`` and decodes it as it arrives. -final class TaskFetchDecodedImage: ImagePipelineTask { +final class TaskFetchOriginalImage: ImagePipelineTask { private var decoder: (any ImageDecoding)? override func start() { - dependency = pipeline.makeTaskFetchOriginalImageData(for: request).subscribe(self) { [weak self] in + dependency = pipeline.makeTaskFetchOriginalData(for: request).subscribe(self) { [weak self] in self?.didReceiveData($0.0, urlResponse: $0.1, isCompleted: $1) } } - /// Receiving data from `OriginalDataTask`. + /// Receiving data from `TaskFetchOriginalData`. private func didReceiveData(_ data: Data, urlResponse: URLResponse?, isCompleted: Bool) { guard isCompleted || pipeline.configuration.isProgressiveDecodingEnabled else { return @@ -28,7 +28,7 @@ final class TaskFetchDecodedImage: ImagePipelineTask { operation?.cancel() // Cancel any potential pending progressive decoding tasks } - let context = ImageDecodingContext(request: request, data: data, isCompleted: isCompleted, urlResponse: urlResponse, cacheType: nil) + let context = ImageDecodingContext(request: request, data: data, isCompleted: isCompleted, urlResponse: urlResponse) guard let decoder = getDecoder(for: context) else { if isCompleted { send(error: .decoderNotRegistered(context: context)) @@ -38,35 +38,20 @@ final class TaskFetchDecodedImage: ImagePipelineTask { return } - // Fast-track default decoders, most work is already done during - // initialization anyway. - @Sendable func decode() -> Result { - signpost("DecodeImageData", isCompleted ? "FinalImage" : "ProgressiveImage") { - Result(catching: { try decoder.decode(context) }) - } - } - - if !decoder.isAsynchronous { - didFinishDecoding(decoder: decoder, context: context, result: decode()) - } else { - operation = pipeline.configuration.imageDecodingQueue.add { [weak self] in - guard let self else { return } - - let result = decode() - self.pipeline.queue.async { - self.didFinishDecoding(decoder: decoder, context: context, result: result) - } - } + decode(context, decoder: decoder) { [weak self] in + self?.didFinishDecoding(context: context, result: $0) } } - private func didFinishDecoding(decoder: any ImageDecoding, context: ImageDecodingContext, result: Result) { + private func didFinishDecoding(context: ImageDecodingContext, result: Result) { + operation = nil + switch result { case .success(let response): send(value: response, isCompleted: context.isCompleted) case .failure(let error): if context.isCompleted { - send(error: .decodingFailed(decoder: decoder, context: context, error: error)) + send(error: error) } } } diff --git a/Sources/Nuke/Tasks/TaskLoadData.swift b/Sources/Nuke/Tasks/TaskLoadData.swift index e7e113ccd..ec77f24b5 100644 --- a/Sources/Nuke/Tasks/TaskLoadData.swift +++ b/Sources/Nuke/Tasks/TaskLoadData.swift @@ -7,26 +7,10 @@ import Foundation /// Wrapper for tasks created by `loadData` calls. final class TaskLoadData: ImagePipelineTask<(Data, URLResponse?)> { override func start() { - guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), - !request.options.contains(.disableDiskCacheReads) else { - loadData() - return - } - operation = pipeline.configuration.dataCachingQueue.add { [weak self] in - self?.getCachedData(dataCache: dataCache) - } - } - - private func getCachedData(dataCache: any DataCaching) { - let data = signpost("ReadCachedImageData") { - pipeline.cache.cachedData(for: request) - } - pipeline.queue.async { - if let data { - self.send(value: (data, nil), isCompleted: true) - } else { - self.loadData() - } + if let data = pipeline.cache.cachedData(for: request) { + self.send(value: (data, nil), isCompleted: true) + } else { + self.loadData() } } @@ -36,7 +20,7 @@ final class TaskLoadData: ImagePipelineTask<(Data, URLResponse?)> { } let request = self.request.withProcessors([]) - dependency = pipeline.makeTaskFetchOriginalImageData(for: request).subscribe(self) { [weak self] in + dependency = pipeline.makeTaskFetchOriginalData(for: request).subscribe(self) { [weak self] in self?.didReceiveData($0.0, urlResponse: $0.1, isCompleted: $1) } } diff --git a/Sources/Nuke/Tasks/TaskLoadImage.swift b/Sources/Nuke/Tasks/TaskLoadImage.swift index 56fc61dc7..d13ab5def 100644 --- a/Sources/Nuke/Tasks/TaskLoadImage.swift +++ b/Sources/Nuke/Tasks/TaskLoadImage.swift @@ -11,74 +11,33 @@ import Foundation /// scenarios in which coalescing can kick in). final class TaskLoadImage: ImagePipelineTask { override func start() { - // Memory cache lookup - if let image = pipeline.cache[request] { - let response = ImageResponse(container: image, request: request, cacheType: .memory) - send(value: response, isCompleted: !image.isPreview) - if !image.isPreview { - return // Already got the result! + if let container = pipeline.cache[request] { + let response = ImageResponse(container: container, request: request, cacheType: .memory) + send(value: response, isCompleted: !container.isPreview) + if !container.isPreview { + return // The final image is loaded } } - - // Disk cache lookup - if let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), - !request.options.contains(.disableDiskCacheReads) { - operation = pipeline.configuration.dataCachingQueue.add { [weak self] in - self?.getCachedData(dataCache: dataCache) - } - return - } - - // Fetch image - fetchImage() - } - - // MARK: Disk Cache Lookup - - private func getCachedData(dataCache: any DataCaching) { - let data = signpost("ReadCachedProcessedImageData") { - pipeline.cache.cachedData(for: request) - } - pipeline.queue.async { - if let data { - self.didReceiveCachedData(data) - } else { - self.fetchImage() - } + if let data = pipeline.cache.cachedData(for: request) { + decodeCachedData(data) + } else { + fetchImage() } } - private func didReceiveCachedData(_ data: Data) { - guard !isDisposed else { return } - - let context = ImageDecodingContext(request: request, data: data, isCompleted: true, urlResponse: nil, cacheType: .disk) + private func decodeCachedData(_ data: Data) { + let context = ImageDecodingContext(request: request, data: data, cacheType: .disk) guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else { - // This shouldn't happen in practice unless encoder/decoder pair - // for data cache is misconfigured. - return fetchImage() + return didFinishDecoding(with: nil) } - - @Sendable func decode() -> ImageResponse? { - signpost("DecodeCachedProcessedImageData") { - try? decoder.decode(context) - } - } - if !decoder.isAsynchronous { - didDecodeCachedData(decode()) - } else { - operation = pipeline.configuration.imageDecodingQueue.add { [weak self] in - guard let self else { return } - let response = decode() - self.pipeline.queue.async { - self.didDecodeCachedData(response) - } - } + decode(context, decoder: decoder) { [weak self] in + self?.didFinishDecoding(with: try? $0.get()) } } - private func didDecodeCachedData(_ response: ImageResponse?) { + private func didFinishDecoding(with response: ImageResponse?) { if let response { - decompressImage(response, isCompleted: true, isFromDiskCache: true) + didReceiveImageResponse(response, isCompleted: true) } else { fetchImage() } @@ -87,153 +46,110 @@ final class TaskLoadImage: ImagePipelineTask { // MARK: Fetch Image private func fetchImage() { - // Memory cache lookup for intermediate images. - // For example, for processors ["p1", "p2"], check only ["p1"]. - // Then apply the remaining processors. - // - // We are not performing data cache lookup for intermediate requests - // for now (because it's not free), but maybe adding an option would be worth it. - // You can emulate this behavior by manually creating intermediate requests. - if request.processors.count > 1 { - var processors = request.processors - var remaining: [any ImageProcessing] = [] - if let last = processors.popLast() { - remaining.append(last) - } - while !processors.isEmpty { - if let image = pipeline.cache[request.withProcessors(processors)] { - let response = ImageResponse(container: image, request: request, cacheType: .memory) - process(response, isCompleted: !image.isPreview, processors: remaining) - if !image.isPreview { - return // Nothing left to do, just apply the processors - } else { - break - } - } - if let last = processors.popLast() { - remaining.append(last) - } - } + guard !request.options.contains(.returnCacheDataDontLoad) else { + return send(error: .dataMissingInCache) } - - let processors: [any ImageProcessing] = request.processors.reversed() - // The only remaining choice is to fetch the image - if request.options.contains(.returnCacheDataDontLoad) { - send(error: .dataMissingInCache) - } else if request.processors.isEmpty { - dependency = pipeline.makeTaskFetchDecodedImage(for: request).subscribe(self) { [weak self] in - self?.process($0, isCompleted: $1, processors: processors) + if let processor = request.processors.last { + let request = request.withProcessors(request.processors.dropLast()) + dependency = pipeline.makeTaskLoadImage(for: request).subscribe(self) { [weak self] in + self?.process($0, isCompleted: $1, processor: processor) } } else { - let request = self.request.withProcessors([]) - dependency = pipeline.makeTaskLoadImage(for: request).subscribe(self) { [weak self] in - self?.process($0, isCompleted: $1, processors: processors) + dependency = pipeline.makeTaskFetchOriginalImage(for: request).subscribe(self) { [weak self] in + self?.didReceiveImageResponse($0, isCompleted: $1) } } } // MARK: Processing - /// - parameter processors: Remaining processors to by applied - private func process(_ response: ImageResponse, isCompleted: Bool, processors: [any ImageProcessing]) { + private func process(_ response: ImageResponse, isCompleted: Bool, processor: any ImageProcessing) { + guard !isDisposed else { return } if isCompleted { - dependency2?.unsubscribe() // Cancel any potential pending progressive processing tasks - } else if dependency2 != nil { - return // Back pressure - already processing another progressive image - } - - _process(response, isCompleted: isCompleted, processors: processors) - } - - /// - parameter processors: Remaining processors to by applied - private func _process(_ response: ImageResponse, isCompleted: Bool, processors: [any ImageProcessing]) { - guard let processor = processors.last else { - self.decompressImage(response, isCompleted: isCompleted) - return + operation?.cancel() // Cancel any potential pending progressive + } else if operation != nil { + return // Back pressure - already processing another progressive image } - - let key = ImageProcessingKey(image: response, processor: processor) let context = ImageProcessingContext(request: request, response: response, isCompleted: isCompleted) - dependency2 = pipeline.makeTaskProcessImage(key: key, process: { - try signpost("ProcessImage", isCompleted ? "FinalImage" : "ProgressiveImage") { - var response = response - response.container = try processor.process(response.container, context: context) - return response - } - }).subscribe(priority: priority) { [weak self] event in + operation = pipeline.configuration.imageProcessingQueue.add { [weak self] in guard let self else { return } - if event.isCompleted { - self.dependency2 = nil - } - switch event { - case .value(let response, _): - self._process(response, isCompleted: isCompleted, processors: processors.dropLast()) - case .error(let error): - if isCompleted { - self.send(error: .processingFailed(processor: processor, context: context, error: error)) + let result = signpost(isCompleted ? "ProcessImage" : "ProcessProgressiveImage") { + Result { + var response = response + response.container = try processor.process(response.container, context: context) + return response + }.mapError { error in + ImagePipeline.Error.processingFailed(processor: processor, context: context, error: error) } - case .progress: - break // Do nothing (Not reported by OperationTask) + } + self.pipeline.queue.async { + self.operation = nil + self.didFinishProcessing(result: result, isCompleted: isCompleted) + } + } + } + + private func didFinishProcessing(result: Result, isCompleted: Bool) { + switch result { + case .success(let response): + didReceiveImageResponse(response, isCompleted: isCompleted) + case .failure(let error): + if isCompleted { + send(error: error) } } } // MARK: Decompression - private func decompressImage(_ response: ImageResponse, isCompleted: Bool, isFromDiskCache: Bool = false) { + private func didReceiveImageResponse(_ response: ImageResponse, isCompleted: Bool) { guard isDecompressionNeeded(for: response) else { - storeImageInCaches(response, isFromDiskCache: isFromDiskCache) - send(value: response, isCompleted: isCompleted) - return + return didReceiveDecompressedImage(response, isCompleted: isCompleted) } - + guard !isDisposed else { return } if isCompleted { operation?.cancel() // Cancel any potential pending progressive decompression tasks } else if operation != nil { - return // Back-pressure: we are receiving data too fast + return // Back-pressure: receiving progressive scans too fast } - - guard !isDisposed else { return } - operation = pipeline.configuration.imageDecompressingQueue.add { [weak self] in guard let self else { return } - - let response = signpost("DecompressImage", isCompleted ? "FinalImage" : "ProgressiveImage") { + let response = signpost(isCompleted ? "DecompressImage" : "DecompressProgressiveImage") { self.pipeline.delegate.decompress(response: response, request: self.request, pipeline: self.pipeline) } - self.pipeline.queue.async { - self.storeImageInCaches(response, isFromDiskCache: isFromDiskCache) - self.send(value: response, isCompleted: isCompleted) + self.operation = nil + self.didReceiveDecompressedImage(response, isCompleted: isCompleted) } } } private func isDecompressionNeeded(for response: ImageResponse) -> Bool { - (ImageDecompression.isDecompressionNeeded(for: response.image) ?? false) && + ImageDecompression.isDecompressionNeeded(for: response) && !request.options.contains(.skipDecompression) && + hasDirectSubscribers && pipeline.delegate.shouldDecompress(response: response, for: request, pipeline: pipeline) } + private func didReceiveDecompressedImage(_ response: ImageResponse, isCompleted: Bool) { + storeImageInCaches(response) + send(value: response, isCompleted: isCompleted) + } + // MARK: Caching - private func storeImageInCaches(_ response: ImageResponse, isFromDiskCache: Bool) { - guard subscribers.contains(where: { $0 is ImageTask }) else { - return // Only store for direct requests + private func storeImageInCaches(_ response: ImageResponse) { + guard hasDirectSubscribers else { + return } - // Memory cache (ImageCaching) pipeline.cache[request] = response.container - // Disk cache (DataCaching) - if !isFromDiskCache { + if shouldStoreResponseInDataCache(response) { storeImageInDataCache(response) } } private func storeImageInDataCache(_ response: ImageResponse) { - guard !response.container.isPreview else { - return - } - guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), shouldStoreImageInDiskCache() else { + guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline) else { return } let context = ImageEncodingContext(request: request, image: response.image, urlResponse: response.urlResponse) @@ -244,9 +160,9 @@ final class TaskLoadImage: ImagePipelineTask { let encodedData = signpost("EncodeImage") { encoder.encode(response.container, context: context) } - guard let data = encodedData else { return } + guard let data = encodedData, !data.isEmpty else { return } pipeline.delegate.willCache(data: data, image: response.container, for: request, pipeline: pipeline) { - guard let data = $0 else { return } + guard let data = $0, !data.isEmpty else { return } // Important! Storing directly ignoring `ImageRequest.Options`. dataCache.storeData(data, for: key) // This is instant, writes are async } @@ -256,20 +172,29 @@ final class TaskLoadImage: ImagePipelineTask { } } - private func shouldStoreImageInDiskCache() -> Bool { - guard !(request.url?.isLocalResource ?? false) else { + private func shouldStoreResponseInDataCache(_ response: ImageResponse) -> Bool { + guard !response.container.isPreview, + !(response.cacheType == .disk), + !(request.url?.isLocalResource ?? false) else { return false } - let isProcessed = !request.processors.isEmpty + let isProcessed = !request.processors.isEmpty || request.thumbnail != nil switch pipeline.configuration.dataCachePolicy { case .automatic: return isProcessed case .storeOriginalData: return false case .storeEncodedImages: - return isProcessed || imageTasks.contains { $0.request.processors.isEmpty } + return true case .storeAll: return isProcessed } } + + /// Returns `true` if the task has at least one image task that was directly + /// subscribed to it, which means that the request was initiated by the + /// user and not the framework. + private var hasDirectSubscribers: Bool { + subscribers.contains { $0 is ImageTask } + } } diff --git a/Sources/NukeUI/Internal.swift b/Sources/NukeUI/Internal.swift index 91c770e65..8d5b51fd8 100644 --- a/Sources/NukeUI/Internal.swift +++ b/Sources/NukeUI/Internal.swift @@ -29,12 +29,14 @@ typealias _PlatformColor = UIColor extension _PlatformBaseView { @discardableResult func pinToSuperview() -> [NSLayoutConstraint] { + guard let superview else { return [] } + translatesAutoresizingMaskIntoConstraints = false let constraints = [ - topAnchor.constraint(equalTo: superview!.topAnchor), - bottomAnchor.constraint(equalTo: superview!.bottomAnchor), - leftAnchor.constraint(equalTo: superview!.leftAnchor), - rightAnchor.constraint(equalTo: superview!.rightAnchor) + topAnchor.constraint(equalTo: superview.topAnchor), + bottomAnchor.constraint(equalTo: superview.bottomAnchor), + leftAnchor.constraint(equalTo: superview.leftAnchor), + rightAnchor.constraint(equalTo: superview.rightAnchor) ] NSLayoutConstraint.activate(constraints) return constraints @@ -42,10 +44,12 @@ extension _PlatformBaseView { @discardableResult func centerInSuperview() -> [NSLayoutConstraint] { + guard let superview else { return [] } + translatesAutoresizingMaskIntoConstraints = false let constraints = [ - centerXAnchor.constraint(equalTo: superview!.centerXAnchor), - centerYAnchor.constraint(equalTo: superview!.centerYAnchor) + centerXAnchor.constraint(equalTo: superview.centerXAnchor), + centerYAnchor.constraint(equalTo: superview.centerYAnchor) ] NSLayoutConstraint.activate(constraints) return constraints diff --git a/Sources/NukeUI/LazyImage.swift b/Sources/NukeUI/LazyImage.swift index 3d4f8d3f5..1a5e68763 100644 --- a/Sources/NukeUI/LazyImage.swift +++ b/Sources/NukeUI/LazyImage.swift @@ -111,7 +111,7 @@ public struct LazyImage: View { /// Gets called when the request is started. public func onStart(_ closure: @escaping (ImageTask) -> Void) -> Self { - map { $0.viewModel.onStart = closure } + map { $0.onStart = closure } } /// Override the behavior on disappear. By default, the view is reset. diff --git a/Sources/NukeUI/LazyImageView.swift b/Sources/NukeUI/LazyImageView.swift index 3abaa5340..5bc7fcff5 100644 --- a/Sources/NukeUI/LazyImageView.swift +++ b/Sources/NukeUI/LazyImageView.swift @@ -53,6 +53,10 @@ public final class LazyImageView: _PlatformBaseView { } } + /// Displays the placeholder image or view in the case of a failure. + /// `false` by default. + public var showPlaceholderOnFailure = false + private var placeholderViewConstraints: [NSLayoutConstraint] = [] // MARK: Failure View @@ -323,7 +327,11 @@ public final class LazyImageView: _PlatformBaseView { case let .success(response): display(response.container, isFromMemory: isSync) case .failure: - setFailureViewHidden(false) + if showPlaceholderOnFailure { + setPlaceholderViewHidden(false) + } else { + setFailureViewHidden(false) + } } imageTask = nil diff --git a/Tests/Helpers.swift b/Tests/Helpers.swift index f3f411de6..9f45b833e 100644 --- a/Tests/Helpers.swift +++ b/Tests/Helpers.swift @@ -33,7 +33,7 @@ enum Test { return try! ImageDecoders.Default().decode(data) } - static let url = URL(string: "http://test.com")! + static let url = URL(string: "http://test.com/example.jpeg")! static let data: Data = Test.data(name: "fixture", extension: "jpeg") @@ -82,7 +82,7 @@ extension ImageDecodingContext { } static func mock(data: Data) -> ImageDecodingContext { - ImageDecodingContext(request: Test.request, data: data, isCompleted: true, urlResponse: nil, cacheType: nil) + ImageDecodingContext(request: Test.request, data: data) } } diff --git a/Tests/NukeTests/DataLoaderTests.swift b/Tests/NukeTests/DataLoaderTests.swift index 4913ab5d3..fb34ca625 100644 --- a/Tests/NukeTests/DataLoaderTests.swift +++ b/Tests/NukeTests/DataLoaderTests.swift @@ -59,7 +59,7 @@ class DataLoaderTests: XCTestCase { expectation.fulfill() } }) - wait(for: [expectation], timeout: 2.0) + wait(for: [expectation], timeout: 5) // THEN XCTAssertEqual(delegate.recordedMetrics.count, 1) diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index f39f96700..75cf11901 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -124,11 +124,12 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { func testCancelAsyncImageTask() async throws { dataLoader.queue.isSuspended = true + pipeline.queue.suspend() let task = pipeline.imageTask(with: Test.url) - observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in task.cancel() } + pipeline.queue.resume() var caughtError: Error? do { diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift index 2abc6d709..3416ccfb7 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift @@ -127,6 +127,36 @@ class ImagePipelineCoalescingTests: XCTestCase { } } + // MARK: - Scale + +#if !os(macOS) + func testOverridingImageScale() throws { + dataLoader.queue.isSuspended = true + + // GIVEN requests with the same URLs but one accesses thumbnail + let request1 = ImageRequest(url: Test.url, userInfo: [.scaleKey: 2]) + let request2 = ImageRequest(url: Test.url, userInfo: [.scaleKey: 3]) + + // WHEN loading images for those requests + expect(pipeline).toLoadImage(with: request1) { result in + // THEN + guard let image = result.value?.image else { return XCTFail() } + XCTAssertEqual(image.scale, 2) + } + expect(pipeline).toLoadImage(with: request2) { result in + // THEN + guard let image = result.value?.image else { return XCTFail() } + XCTAssertEqual(image.scale, 3) + } + + dataLoader.queue.isSuspended = false + + wait() + + XCTAssertEqual(self.dataLoader.createdTaskCount, 1) + } +#endif + // MARK: - Thumbnail func testDeduplicationGivenSameURLButDifferentThumbnailOptions() { @@ -156,6 +186,34 @@ class ImagePipelineCoalescingTests: XCTestCase { } } + func testDeduplicationGivenSameURLButDifferentThumbnailOptionsReversed() { + dataLoader.queue.isSuspended = true + + // GIVEN requests with the same URLs but one accesses thumbnail + // (in this test, order is reversed) + let request1 = ImageRequest(url: Test.url) + let request2 = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: ImageRequest.ThumbnailOptions(maxPixelSize: 400)]) + + // WHEN loading images for those requests + expect(pipeline).toLoadImage(with: request1) { result in + // THEN + guard let image = result.value?.image else { return XCTFail() } + XCTAssertEqual(image.sizeInPixels, CGSize(width: 640.0, height: 480.0)) + } + expect(pipeline).toLoadImage(with: request2) { result in + // THEN + guard let image = result.value?.image else { return XCTFail() } + XCTAssertEqual(image.sizeInPixels, CGSize(width: 400, height: 300)) + } + + dataLoader.queue.isSuspended = false + + wait { _ in + // THEN the image data is fetched once + XCTAssertEqual(self.dataLoader.createdTaskCount, 1) + } + } + // MARK: - Processing func testProcessorsAreDeduplicated() { @@ -539,7 +597,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase { } } - func testWhenApplingMultipleImageProcessorsIntermediateDataCacheResultsAreNotUsed() { + func testWhenApplingMultipleImageProcessorsIntermediateDataCacheResultsAreUsed() { // Given let dataCache = MockDataCache() dataCache.store[Test.url.absoluteString + "12"] = Test.data @@ -555,13 +613,12 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase { guard let image = result.value?.image else { return XCTFail("Expected image to be loaded successfully") } - XCTAssertEqual(image.nk_test_processorIDs, ["1", "2", "3"], "Expected only the last processor to be applied") + XCTAssertEqual(image.nk_test_processorIDs, ["3"], "Expected only the last processor to be applied") } - // Then we don't expect any intermediate results to be stored in data cache wait { _ in - XCTAssertEqual(self.dataLoader.createdTaskCount, 1, "Expected no data task to be performed") - XCTAssertEqual(factory.numberOfProcessorsApplied, 3, "Expected only one processor to be applied") + XCTAssertEqual(self.dataLoader.createdTaskCount, 0, "Expected no data task to be performed") + XCTAssertEqual(factory.numberOfProcessorsApplied, 1, "Expected only one processor to be applied") } } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift index 0c075807c..2ad079646 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift @@ -46,31 +46,80 @@ class ImagePipelineDataCachingTests: XCTestCase { } } - func testGeneratedThumbnailDataIsStoredIncache() { - // When + func testThumbnailOptionsDataCacheStoresOriginalDataByDefault() throws { + // GIVEN + pipeline = pipeline.reconfigured { + $0.dataCachePolicy = .storeOriginalData + $0.imageCache = MockImageCache() + $0.debugIsSyncImageEncoding = true + } + + // WHEN let request = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFit)]) expect(pipeline).toLoadImage(with: request) - - // Then - wait { _ in - XCTAssertFalse(self.dataCache.store.isEmpty) - - XCTAssertNotNil(self.pipeline.cache.cachedData(for: request)) - - guard let container = self.pipeline.cache.cachedImage(for: request, caches: [.disk]) else { - return XCTFail() - } - XCTAssertEqual(container.image.sizeInPixels, CGSize(width: 400, height: 300)) - - XCTAssertNil(self.pipeline.cache.cachedData(for: ImageRequest(url: Test.url))) + + // THEN + wait() + + do { // Check memory cache + // Image does not exists for the original image + XCTAssertNil(pipeline.cache.cachedImage(for: ImageRequest(url: Test.url), caches: [.memory])) + + // Image exists for thumbnail + let thumbnail = try XCTUnwrap(pipeline.cache.cachedImage(for: request, caches: [.memory])) + XCTAssertEqual(thumbnail.image.sizeInPixels, CGSize(width: 400, height: 300)) + } + + do { // Check disk cache + // Data exists for the original image + let original = try XCTUnwrap(pipeline.cache.cachedImage(for: ImageRequest(url: Test.url), caches: [.disk])) + XCTAssertEqual(original.image.sizeInPixels, CGSize(width: 640, height: 480)) + + // Data does not exist for thumbnail + XCTAssertNil(pipeline.cache.cachedData(for: request)) } } - + + func testThumbnailOptionsDataCacheStoresOriginalDataWithStoreAllPolicy() throws { + // GIVEN + pipeline = pipeline.reconfigured { + $0.dataCachePolicy = .storeAll + $0.imageCache = MockImageCache() + $0.debugIsSyncImageEncoding = true + } + + // WHEN + let request = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFit)]) + expect(pipeline).toLoadImage(with: request) + + // THEN + wait() + + do { // Check memory cache + // Image does not exists for the original image + XCTAssertNil(pipeline.cache.cachedImage(for: ImageRequest(url: Test.url), caches: [.memory])) + + // Image exists for thumbnail + let thumbnail = try XCTUnwrap(pipeline.cache.cachedImage(for: request, caches: [.memory])) + XCTAssertEqual(thumbnail.image.sizeInPixels, CGSize(width: 400, height: 300)) + } + + do { // Check disk cache + // Data exists for the original image + let original = try XCTUnwrap(pipeline.cache.cachedImage(for: ImageRequest(url: Test.url), caches: [.disk])) + XCTAssertEqual(original.image.sizeInPixels, CGSize(width: 640, height: 480)) + + // Data exists for thumbnail + let thumbnail = try XCTUnwrap(pipeline.cache.cachedImage(for: request, caches: [.disk])) + XCTAssertEqual(thumbnail.image.sizeInPixels, CGSize(width: 400, height: 300)) + } + } + // MARK: - Updating Priority func testPriorityUpdated() { // Given - let queue = pipeline.configuration.dataCachingQueue + let queue = pipeline.configuration.dataLoadingQueue queue.isSuspended = true let request = Test.request @@ -95,7 +144,7 @@ class ImagePipelineDataCachingTests: XCTestCase { func testOperationCancelled() { // Given - let queue = pipeline.configuration.dataCachingQueue + let queue = pipeline.configuration.dataLoadingQueue queue.isSuspended = true let observer = self.expect(queue).toEnqueueOperationsWithCount(1) let task = pipeline.loadImage(with: Test.request) { _ in } @@ -575,8 +624,8 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { XCTAssertEqual(dataCache.store.count, 2) } - // MARK: Local Storage - + // MARK: Local Resources + func testImagesFromLocalStorageNotCached() { // GIVEN pipeline = pipeline.reconfigured { @@ -584,8 +633,8 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { } // GIVEN request without a processor - let request = ImageRequest(url: URL(string: "file://example/image.jpeg")) - + let request = ImageRequest(url: Test.url(forResource: "fixture", extension: "jpeg")) + // WHEN expect(pipeline).toLoadImage(with: request) wait() @@ -603,8 +652,8 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { } // GIVEN request with a processor - let request = ImageRequest(url: URL(string: "file://example/image.jpeg") ,processors: [.resize(width: 100)]) - + let request = ImageRequest(url: Test.url(forResource: "fixture", extension: "jpeg") ,processors: [.resize(width: 100)]) + // WHEN expect(pipeline).toLoadImage(with: request) wait() @@ -622,8 +671,8 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { } // GIVEN request without a processor - let request = ImageRequest(url: URL(string: "data://example/image.jpeg")) - + let request = ImageRequest(url: Test.url(forResource: "fixture", extension: "jpeg")) + // WHEN expect(pipeline).toLoadImage(with: request) wait() @@ -633,7 +682,28 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { XCTAssertEqual(dataCache.writeCount, 0) XCTAssertEqual(dataCache.store.count, 0) } - + + func testImagesFromData() { + // GIVEN + pipeline = pipeline.reconfigured { + $0.dataCachePolicy = .automatic + } + + // GIVEN request without a processor + let data = Test.data(name: "fixture", extension: "jpeg") + let url = URL(string: "data:image/jpeg;base64,\(data.base64EncodedString())") + let request = ImageRequest(url: url) + + // WHEN + expect(pipeline).toLoadImage(with: request) + wait() + + // THEN original image data is stored in disk cache + XCTAssertEqual(encoder.encodeCount, 0) + XCTAssertEqual(dataCache.writeCount, 0) + XCTAssertEqual(dataCache.store.count, 0) + } + // MARK: Misc func testSetCustomImageEncoder() { @@ -668,4 +738,19 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { XCTAssertNil(self.dataCache.cachedData(for: Test.url.absoluteString + "1"), "Expected processed image data to not be stored") } } + + // MARK: Integration with Thumbnail Feature + + func testOriginalDataStoredWhenThumbnailRequested() { + // GIVEN + let options = ImageRequest.ThumbnailOptions(maxPixelSize: 400) + let request = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: options]) + + // WHEN + expect(pipeline).toLoadImage(with: request) + wait() + + // THEN + XCTAssertTrue(dataCache.containsData(for: "http://test.com/example.jpeg")) + } } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineImageCacheTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineImageCacheTests.swift index 6aa2bb2e7..558066857 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineImageCacheTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineImageCacheTests.swift @@ -304,7 +304,7 @@ class ImagePipelineCacheLayerPriorityTests: XCTestCase { XCTAssertEqual(imageCache.writeCount, 2) // Processed + original XCTAssertNotNil(imageCache[originalRequest]) XCTAssertNotNil(imageCache[request]) - XCTAssertEqual(dataCache.readCount, 1) // Processed + XCTAssertEqual(dataCache.readCount, 2) // "1", "2" XCTAssertEqual(dataCache.writeCount, 1) // Initial XCTAssertEqual(dataLoader.createdTaskCount, 0) } @@ -323,7 +323,7 @@ class ImagePipelineCacheLayerPriorityTests: XCTestCase { XCTAssertEqual(imageCache.readCount, 3) // Processed + intermediate + original XCTAssertEqual(imageCache.writeCount, 1) // Processed XCTAssertNotNil(imageCache[request]) - XCTAssertEqual(dataCache.readCount, 2) // Processed + original + XCTAssertEqual(dataCache.readCount, 3) // "1" + "2" + original XCTAssertEqual(dataCache.writeCount, 1) // Initial XCTAssertEqual(dataLoader.createdTaskCount, 0) } @@ -387,7 +387,7 @@ class ImagePipelineCacheLayerPriorityTests: XCTestCase { XCTAssertEqual(imageCache.readCount, 3) // Processed + intermediate + original XCTAssertEqual(imageCache.writeCount, 1) // Processed XCTAssertNotNil(imageCache[request]) - XCTAssertEqual(dataCache.readCount, 2) // Processed + original + XCTAssertEqual(dataCache.readCount, 3) // "1" + "2" + original XCTAssertEqual(dataCache.writeCount, 0) XCTAssertEqual(dataLoader.createdTaskCount, 1) } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift index 86b342697..624535739 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift @@ -116,7 +116,7 @@ class ImagePipelineLoadDataTests: XCTestCase { } // WHEN - let record = expect(pipeline).toLoadData(with: ImageRequest(url: URL(string: "http://example.com/invalid url"))) + let record = expect(pipeline).toLoadData(with: ImageRequest(url: URL(string: ""))) wait() // THEN diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineProcessorTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineProcessorTests.swift index a3d5c2042..99ca391c9 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineProcessorTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineProcessorTests.swift @@ -75,4 +75,22 @@ class ImagePipelineProcessorTests: XCTestCase { } wait() } + + // MARK: - Decompression + +#if !os(macOS) + func testDecompressionSkippedIfProcessorsAreApplied() { + // Given + let request = ImageRequest(url: Test.url, processors: [ImageProcessors.Anonymous(id: "1", { image in + XCTAssertTrue(ImageDecompression.isDecompressionNeeded(for: image) == true) + return image + })]) + + // When + expect(pipeline).toLoadImage(with: request) { result in + // Then + } + wait() + } +#endif } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift index 05d3ff981..a870b510d 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift @@ -368,25 +368,25 @@ class ImagePipelineTests: XCTestCase { func testCacheKeyForRequest() { let request = Test.request - XCTAssertEqual(pipeline.cache.makeDataCacheKey(for: request), "http://test.com") + XCTAssertEqual(pipeline.cache.makeDataCacheKey(for: request), "http://test.com/example.jpeg") } func testCacheKeyForRequestWithProcessors() { var request = Test.request request.processors = [ImageProcessors.Anonymous(id: "1", { $0 })] - XCTAssertEqual(pipeline.cache.makeDataCacheKey(for: request), "http://test.com1") + XCTAssertEqual(pipeline.cache.makeDataCacheKey(for: request), "http://test.com/example.jpeg1") } func testCacheKeyForRequestWithThumbnail() { let options = ImageRequest.ThumbnailOptions(maxPixelSize: 400) let request = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: options]) - XCTAssertEqual(pipeline.cache.makeDataCacheKey(for: request), "http://test.comcom.github/kean/nuke/thumbnail?maxPixelSize=400.0,options=truetruetruetrue") + XCTAssertEqual(pipeline.cache.makeDataCacheKey(for: request), "http://test.com/example.jpegcom.github/kean/nuke/thumbnail?maxPixelSize=400.0,options=truetruetruetrue") } func testCacheKeyForRequestWithThumbnailFlexibleSize() { let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFit) let request = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: options]) - XCTAssertEqual(pipeline.cache.makeDataCacheKey(for: request), "http://test.comcom.github/kean/nuke/thumbnail?width=400.0,height=400.0,contentMode=.aspectFit,options=truetruetruetrue") + XCTAssertEqual(pipeline.cache.makeDataCacheKey(for: request), "http://test.com/example.jpegcom.github/kean/nuke/thumbnail?width=400.0,height=400.0,contentMode=.aspectFit,options=truetruetruetrue") } // MARK: - Invalidate @@ -598,8 +598,8 @@ class ImagePipelineTests: XCTestCase { } // WHEN - for _ in 0...100 { - expect(pipeline).toFailRequest(ImageRequest(url: URL(string: "http://example.com/invalid url"))) + for _ in 0...10 { + expect(pipeline).toFailRequest(ImageRequest(url: URL(string: ""))) wait() } } diff --git a/Tests/NukeTests/ImageProcessorsTests/CoreImageFilterTests.swift b/Tests/NukeTests/ImageProcessorsTests/CoreImageFilterTests.swift index cf24a9935..9d8d6ee3a 100644 --- a/Tests/NukeTests/ImageProcessorsTests/CoreImageFilterTests.swift +++ b/Tests/NukeTests/ImageProcessorsTests/CoreImageFilterTests.swift @@ -102,6 +102,19 @@ class ImageProcessorsCoreImageFilterTests: XCTestCase { // THEN XCTAssertEqual("\(processor)", "CoreImageFilter(name: CISepiaTone, parameters: [\"inputIntensity\": 0.5])") } + + func testApplyCustomFilter() throws { + // GIVEN + let input = Test.image(named: "fixture-tiny.jpeg") + let filter = try XCTUnwrap(CIFilter(name: "CISepiaTone", parameters: nil)) + let processor = ImageProcessors.CoreImageFilter(filter, identifier: "test") + + // WHEN + let output = try XCTUnwrap(processor.process(input)) + + // THEN + XCTAssertNotNil(output) + } } #endif diff --git a/Tests/NukeTests/ImageRequestTests.swift b/Tests/NukeTests/ImageRequestTests.swift index 7e519576b..48e65302c 100644 --- a/Tests/NukeTests/ImageRequestTests.swift +++ b/Tests/NukeTests/ImageRequestTests.swift @@ -64,86 +64,86 @@ class ImageRequestTests: XCTestCase { class ImageRequestCacheKeyTests: XCTestCase { func testDefaults() { let request = Test.request - AssertHashableEqual(CacheKey(request), CacheKey(request)) // equal to itself + AssertHashableEqual(MemoryCacheKey(request), MemoryCacheKey(request)) // equal to itself } func testRequestsWithTheSameURLsAreEquivalent() { - let request1 = ImageRequest(url: Test.url) - let request2 = ImageRequest(url: Test.url) - AssertHashableEqual(CacheKey(request1), CacheKey(request2)) + let lhs = ImageRequest(url: Test.url) + let rhs = ImageRequest(url: Test.url) + AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testRequestsWithDefaultURLRequestAndURLAreEquivalent() { - let request1 = ImageRequest(url: Test.url) - let request2 = ImageRequest(urlRequest: URLRequest(url: Test.url)) - AssertHashableEqual(CacheKey(request1), CacheKey(request2)) + let lhs = ImageRequest(url: Test.url) + let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url)) + AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testRequestsWithDifferentURLsAreNotEquivalent() { - let request1 = ImageRequest(url: URL(string: "http://test.com/1.png")) - let request2 = ImageRequest(url: URL(string: "http://test.com/2.png")) - XCTAssertNotEqual(CacheKey(request1), CacheKey(request2)) + let lhs = ImageRequest(url: URL(string: "http://test.com/1.png")) + let rhs = ImageRequest(url: URL(string: "http://test.com/2.png")) + XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testRequestsWithTheSameProcessorsAreEquivalent() { - let request1 = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) - let request2 = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) - AssertHashableEqual(CacheKey(request1), CacheKey(request2)) + let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) + let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) + AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testRequestsWithDifferentProcessorsAreNotEquivalent() { - let request1 = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) - let request2 = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "2")]) - XCTAssertNotEqual(CacheKey(request1), CacheKey(request2)) + let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) + let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "2")]) + XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testURLRequestParametersAreIgnored() { - let request1 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50)) - let request2 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0)) - AssertHashableEqual(CacheKey(request1), CacheKey(request2)) + let lhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50)) + let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0)) + AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testSettingDefaultProcessorManually() { - let request1 = ImageRequest(url: Test.url) - let request2 = ImageRequest(url: Test.url, processors: request1.processors) - AssertHashableEqual(CacheKey(request1), CacheKey(request2)) + let lhs = ImageRequest(url: Test.url) + let rhs = ImageRequest(url: Test.url, processors: lhs.processors) + AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } } class ImageRequestLoadKeyTests: XCTestCase { func testDefaults() { let request = ImageRequest(url: Test.url) - AssertHashableEqual(request.makeDataLoadKey(), request.makeDataLoadKey()) + AssertHashableEqual(TaskFetchOriginalDataKey(request), TaskFetchOriginalDataKey(request)) } func testRequestsWithTheSameURLsAreEquivalent() { - let request1 = ImageRequest(url: Test.url) - let request2 = ImageRequest(url: Test.url) - AssertHashableEqual(request1.makeDataLoadKey(), request2.makeDataLoadKey()) + let lhs = ImageRequest(url: Test.url) + let rhs = ImageRequest(url: Test.url) + AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } func testRequestsWithDifferentURLsAreNotEquivalent() { - let request1 = ImageRequest(url: URL(string: "http://test.com/1.png")) - let request2 = ImageRequest(url: URL(string: "http://test.com/2.png")) - XCTAssertNotEqual(request1.makeDataLoadKey(), request2.makeDataLoadKey()) + let lhs = ImageRequest(url: URL(string: "http://test.com/1.png")) + let rhs = ImageRequest(url: URL(string: "http://test.com/2.png")) + XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } func testRequestsWithTheSameProcessorsAreEquivalent() { - let request1 = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) - let request2 = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) - AssertHashableEqual(request1.makeDataLoadKey(), request2.makeDataLoadKey()) + let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) + let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) + AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } func testRequestsWithDifferentProcessorsAreEquivalent() { - let request1 = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) - let request2 = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "2")]) - AssertHashableEqual(request1.makeDataLoadKey(), request2.makeDataLoadKey()) + let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) + let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "2")]) + AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } func testRequestWithDifferentURLRequestParametersAreNotEquivalent() { - let request1 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50)) - let request2 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0)) - XCTAssertNotEqual(request1.makeDataLoadKey(), request2.makeDataLoadKey()) + let lhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50)) + let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0)) + XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } func testMockImageProcessorCorrectlyImplementsIdentifiers() { @@ -159,37 +159,37 @@ class ImageRequestImageIdTests: XCTestCase { func testThatCacheKeyUsesAbsoluteURLByDefault() { let lhs = ImageRequest(url: Test.url) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1")) - XCTAssertNotEqual(lhs.makeImageCacheKey(), rhs.makeImageCacheKey()) + XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testThatCacheKeyUsesFilteredURLWhenSet() { let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString]) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString]) - AssertHashableEqual(lhs.makeImageCacheKey(), rhs.makeImageCacheKey()) + AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testThatCacheKeyForProcessedImageDataUsesAbsoluteURLByDefault() { let lhs = ImageRequest(url: Test.url) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1")) - XCTAssertNotEqual(lhs.makeImageCacheKey(), rhs.makeImageCacheKey()) + XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testThatCacheKeyForProcessedImageDataUsesFilteredURLWhenSet() { let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString]) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString]) - AssertHashableEqual(lhs.makeImageCacheKey(), rhs.makeImageCacheKey()) + AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } func testThatLoadKeyForProcessedImageDoesntUseFilteredURL() { let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString]) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString]) - XCTAssertNotEqual(lhs.makeImageLoadKey(), rhs.makeImageLoadKey()) + XCTAssertNotEqual(TaskLoadImageKey(lhs), TaskLoadImageKey(rhs)) } func testThatLoadKeyForOriginalImageDoesntUseFilteredURL() { let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString]) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString]) - XCTAssertNotEqual(lhs.makeDataLoadKey(), rhs.makeDataLoadKey()) + XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } } diff --git a/Tests/NukeTests/TaskTests.swift b/Tests/NukeTests/TaskTests.swift index 5186e7f20..003e1fcb8 100644 --- a/Tests/NukeTests/TaskTests.swift +++ b/Tests/NukeTests/TaskTests.swift @@ -466,6 +466,6 @@ private final class SimpleTask: AsyncTask { extension AsyncTask { func subscribe(priority: TaskPriority = .normal, _ observer: @escaping (Event) -> Void) -> TaskSubscription? { - publisher.subscribe(priority: priority, observer) + publisher.subscribe(priority: priority, subscriber: "" as AnyObject, observer) } } diff --git a/Tests/NukeUITests/FetchImageTests.swift b/Tests/NukeUITests/FetchImageTests.swift index 6adf1b0b4..a0264a89d 100644 --- a/Tests/NukeUITests/FetchImageTests.swift +++ b/Tests/NukeUITests/FetchImageTests.swift @@ -77,7 +77,7 @@ class FetchImageTests: XCTestCase { } func testPriorityUpdated() { - let queue = pipeline.configuration.dataCachingQueue + let queue = pipeline.configuration.dataLoadingQueue queue.isSuspended = true let observer = self.expect(queue).toEnqueueOperationsWithCount(1) @@ -92,7 +92,7 @@ class FetchImageTests: XCTestCase { } func testPriorityUpdatedDynamically() { - let queue = pipeline.configuration.dataCachingQueue + let queue = pipeline.configuration.dataLoadingQueue queue.isSuspended = true let observer = self.expect(queue).toEnqueueOperationsWithCount(1) diff --git a/Tests/XCTestCaseExtensions.swift b/Tests/XCTestCaseExtensions.swift index 461a020ef..f710fcaf0 100644 --- a/Tests/XCTestCaseExtensions.swift +++ b/Tests/XCTestCaseExtensions.swift @@ -12,7 +12,7 @@ extension XCTestCase { return self.expectation(forNotification: name, object: object, handler: handler) } - func wait(_ timeout: TimeInterval = 4, handler: XCWaitCompletionHandler? = nil) { + func wait(_ timeout: TimeInterval = 5, handler: XCWaitCompletionHandler? = nil) { self.waitForExpectations(timeout: timeout, handler: handler) } }