[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: kean\npatreon: # \nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: \"Nuke CI\"\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - '*'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  ios-latest:\n    name: Unit Tests (iOS 26.2, Xcode 26.2)\n    runs-on: macOS-26\n    timeout-minutes: 12\n    env:\n      DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer\n    steps:\n      - uses: actions/checkout@v4\n      - name: Boot simulator\n        run: |\n          xcrun simctl boot \"iPhone 17 Pro\"\n          xcrun simctl bootstatus \"iPhone 17 Pro\" -b\n      - name: Run Tests\n        run: |\n          .scripts/test.sh -s \"Nuke\" -d \"OS=26.2,name=iPhone 17 Pro\"\n          .scripts/test.sh -s \"NukeUI\" -d \"OS=26.2,name=iPhone 17 Pro\"\n          .scripts/test.sh -s \"NukeExtensions\" -d \"OS=26.2,name=iPhone 17 Pro\"\n  macos-latest:\n    name: Unit Tests (macOS, Xcode 26.2)\n    runs-on: macOS-26\n    timeout-minutes: 12\n    env:\n      DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run Tests\n        run: |\n          .scripts/test.sh -s \"Nuke\" -d \"platform=macOS\"\n          .scripts/test.sh -s \"NukeUI\" -d \"platform=macOS\"\n          .scripts/test.sh -s \"NukeExtensions\" -d \"platform=macOS\"\n  tvos-latest:\n    name: Unit Tests (tvOS 26.2, Xcode 26.2)\n    runs-on: macOS-26\n    timeout-minutes: 12\n    env:\n      DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer\n    steps:\n      - uses: actions/checkout@v4\n      - name: Boot simulator\n        run: |\n          xcrun simctl boot \"Apple TV\"\n          xcrun simctl bootstatus \"Apple TV\" -b\n      - name: Run Tests\n        run: |\n          .scripts/test.sh -s \"Nuke\" -d \"OS=26.2,name=Apple TV\"\n          .scripts/test.sh -s \"NukeUI\" -d \"OS=26.2,name=Apple TV\"\n          .scripts/test.sh -s \"NukeExtensions\" -d \"OS=26.2,name=Apple TV\"\n# There is a problem with watchOS runners where they often fail to launch on CI\n#\n#  watchos-latest:\n#    name: Unit Tests (watchOS 9.1, Xcode 14.1)\n#    runs-on: macOS-13\n#    env:\n#      DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer\n#    steps:\n#      - uses: actions/checkout@v4\n#      - name: Run Tests\n#        run: |\n#          .scripts/test.sh -s \"Nuke\" -d \"OS=9.1,name=Apple Watch Series 8 (45mm)\"\n#          .scripts/test.sh -s \"NukeUI\" -d \"OS=9.1,name=Apple Watch Series 8 (45mm)\"\n#          .scripts/test.sh -s \"NukeExtensions\" -d \"OS=9.1,name=Apple Watch Series 8 (45mm)\"\n\n#  Nuke 13.0 supports only the latest version of Xcode (16).\n#\n#  ios-xcode-14-3-1:\n#    name: Unit Tests (iOS 17.0, Xcode 15.0)\n#    runs-on: macOS-13\n#    env:\n#      DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer\n#    steps:\n#      - uses: actions/checkout@v4\n#      - name: Run Tests\n#        run: |\n#          .scripts/test.sh -s \"Nuke\" -d \"OS=17.0,name=iPhone 15 Pro\"\n#          .scripts/test.sh -s \"NukeUI\" -d \"OS=17.0,name=iPhone 15 Pro\"\n#          .scripts/test.sh -s \"NukeExtensions\" -d \"OS=17.0,name=iPhone 15 Pro\"\n#  ios-thread-safety:\n#    name: Thread Safety Tests (TSan Enabled)\n#    runs-on: macOS-26\n#    timeout-minutes: 12\n#    env:\n#      DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer\n#    steps:\n#      - uses: actions/checkout@v4\n#      - name: Run Tests\n#        run: .scripts/test.sh -s \"NukeThreadSafetyTests\" -d \"OS=26.2,name=iPhone 17 Pro\"\n#  ios-memory-management-tests:\n#    name: Memory Management Tests\n#    runs-on: macOS-13\n#    env:\n#      DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer\n#    steps:\n#      - uses: actions/checkout@v4\n#      - name: Run Tests\n#        run: .scripts/test.sh -s \"NukeMemoryManagementTests\" -d \"OS=14.4,name=iPhone 12 Pro\"\n#  ios-performance-tests:\n#    name: Performance Tests\n#    runs-on: macOS-26\n#    timeout-minutes: 12\n#    env:\n#      DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer\n#    steps:\n#      - uses: actions/checkout@v4\n#      - name: Run Tests\n#        run: .scripts/test.sh -s \"NukePerformanceTests\" -d \"OS=26.2,name=iPhone 17 Pro\"\n  swift-build:\n    name: Swift Build (SPM)\n    runs-on: macOS-26\n    timeout-minutes: 12\n    env:\n      DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer\n    steps:\n      - uses: actions/checkout@v4\n      - name: Build\n        run: swift build\n"
  },
  {
    "path": ".gitignore",
    "content": "## System\n.DS_Store\n\n.claude/settings.local.json\n.claude/worktrees\n\n## Build generated\nbuild/\nDerivedData\nNuke.xcodeproj/xcshareddata/xcbaselines/\n.swiftpm/\n\n## Various settings\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\nxcuserdata\n\n## Other\n*.xccheckout\n*.moved-aside\n*.xcuserstate\n*.xcscmblueprint\n\n## Obj-C/Swift specific\n*.hmap\n*.ipa\n\n## Playgrounds\ntimeline.xctimeline\nplayground.xcworkspace\n\n\n## Swift Package Manager\n#\n# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.\n# Packages/\n\n.build/\n\n\n## CocoaPods\n#\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n#\n\nPods/\n\n\n## Carthage\n#\n# Add this line if you want to avoid checking in source code from Carthage dependencies.\n\nCarthage\n"
  },
  {
    "path": ".scripts/create-xcframeworks.sh",
    "content": "ROOT=\"./.build/xcframeworks\"\n\nrm -rf $ROOT\n\nfor SDK in iphoneos iphonesimulator macosx appletvos appletvsimulator watchos watchsimulator\ndo\nxcodebuild archive \\\n    -scheme NukeUI \\\n    -archivePath \"$ROOT/nuke-$SDK.xcarchive\" \\\n    -sdk $SDK \\\n    SKIP_INSTALL=NO \\\n    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \\\n    DEBUG_INFORMATION_FORMAT=DWARF\ndone\n\nxcodebuild -create-xcframework \\\n    -framework \"$ROOT/nuke-iphoneos.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -framework \"$ROOT/nuke-iphonesimulator.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -output \"$ROOT/Nuke.xcframework\"\n\nxcodebuild -create-xcframework \\\n    -framework \"$ROOT/nuke-iphoneos.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -framework \"$ROOT/nuke-iphonesimulator.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -output \"$ROOT/NukeUI.xcframework\"\n\ncd $ROOT\nzip -r -X nuke-xcframeworks-ios.zip *.xcframework\nrm -rf *.xcframework\ncd -\n\nxcodebuild -create-xcframework \\\n    -framework \"$ROOT/nuke-iphoneos.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -framework \"$ROOT/nuke-iphonesimulator.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -framework \"$ROOT/nuke-macosx.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -framework \"$ROOT/nuke-appletvos.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -framework \"$ROOT/nuke-appletvsimulator.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -framework \"$ROOT/nuke-watchos.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -framework \"$ROOT/nuke-watchsimulator.xcarchive/Products/Library/Frameworks/Nuke.framework\" \\\n    -output \"$ROOT/Nuke.xcframework\"\n\nxcodebuild -create-xcframework \\\n    -framework \"$ROOT/nuke-iphoneos.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -framework \"$ROOT/nuke-iphonesimulator.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -framework \"$ROOT/nuke-macosx.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -framework \"$ROOT/nuke-appletvos.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -framework \"$ROOT/nuke-appletvsimulator.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -framework \"$ROOT/nuke-watchos.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -framework \"$ROOT/nuke-watchsimulator.xcarchive/Products/Library/Frameworks/NukeUI.framework\" \\\n    -output \"$ROOT/NukeUI.xcframework\"\n\ncd $ROOT\nzip -r -X nuke-xcframeworks-all-platforms.zip *.xcframework\nrm -rf *.xcframework\ncd -\n\nmv $ROOT/*.zip ./\n"
  },
  {
    "path": ".scripts/install_swiftlint.sh",
    "content": "#!/bin/sh\n\n# -L to enable redirects\necho \"Installing SwiftLint by downloading a pre-compiled binary\"\ncurl -L 'https://github.com/realm/SwiftLint/releases/download/0.47.1/portable_swiftlint.zip' -o swiftlint.zip\nmkdir temp\nunzip swiftlint.zip -d temp\nrm -f swiftlint.zip\n"
  },
  {
    "path": ".scripts/lint.sh",
    "content": "#!/bin/sh\n\nif which swiftlint >/dev/null; then\n  swiftlint\nelse\n  echo \"SwiftLint not installed\"\nfi\n"
  },
  {
    "path": ".scripts/test.sh",
    "content": "#!/bin/sh\n\nset -eo pipefail\n\nscheme=\"Nuke\"\n\nwhile getopts \"s:d:\" opt; do\n    case $opt in\n    \ts) scheme=${OPTARG};;\n        d) destinations+=(\"$OPTARG\");;\n        #...\n    esac\ndone\nshift $((OPTIND -1))\n\necho \"scheme = ${scheme}\"\necho \"destinations = ${destinations[@]}\"\n\nxcodebuild -version\n\nxcodebuild build-for-testing -scheme \"$scheme\" -destination \"${destinations[0]}\" | xcbeautify\n\nfor destination in \"${destinations[@]}\";\ndo\n\techo \"\\nRunning tests for destination: $destination\"\n\txcodebuild test-without-building -scheme \"$scheme\" -destination \"$destination\" -parallel-testing-enabled NO -test-timeouts-enabled YES -default-test-execution-time-allowance 120 -retry-tests-on-failure | xcbeautify\ndone\n"
  },
  {
    "path": ".scripts/validate.sh",
    "content": "#!/bin/sh\n\n./temp/swiftlint lint --strict\n"
  },
  {
    "path": ".swiftlint.yml",
    "content": "opt_in_rules:\n    - closure_spacing\n    - convenience_type\n    - empty_count\n    - empty_string\n    - explicit_init\n    - fatal_error_message\n    - first_where\n    - identical_operands\n    - joined_default_parameter\n    - modifier_order\n    - operator_usage_whitespace\n    - overridden_super_call\n    - pattern_matching_keywords\n    - prohibited_super_call\n    - toggle_bool\n    - unavailable_function\n    - vertical_parameter_alignment_on_call\n\ndisabled_rules:\n    - line_length\n    - identifier_name\n    - type_name\n\nnesting:\n    type_level:\n        warning: 2\n\nincluded:\n    - Sources/\n\nfile_length:\n    warning: 1000\n    error: 1500\n\ntype_body_length:\n    warning: 600\n    error: 1000\n\nidentifier_name:\n    min_length:\n        warning: 1\n\nreporter: \"xcode\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Nuke 13\n\n## Nuke 13.0 (WIP)\n\nNuke 13 achieves full Data Race Safety by migrating all pipeline work to Swift Concurrency, replacing `DispatchQueue` and `OperationQueue` with a `@globalActor`-based synchronization model. It also ships over 10 new APIs, including progressive preview policies, a `willLoadData` auth hook, memory size limits, and type-safe `ImageRequest` properties.\n\nThe test suite was rewritten in Swift Testing with Swift 6 mode enabled and significantly expanded:\n\n| Version   | Source lines | Tests | Test lines | Coverage |\n|-----------|-------------|-------|------------|----------|\n| Nuke 13.0 | 4,669       | 768   | 8,509      | 96.0%    |\n| Nuke 12.9 | 4,589       | 496   | 6,167      | 92.4%    |\n\n**Requirements**\n\n- Minimum supported Xcode version: 26.0.\n- Minimum required platforms: iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15.\n\n**Concurrency & Data Race Safety**\n\n- Replace the internal serial `DispatchQueue` with a `@globalActor` (`ImagePipelineActor`) for pipeline synchronization, making thread-safety compiler-enforced. The actor is public so that custom `ImagePipeline.Delegate` implementations can use it when needed to reduce thread hops\n- Replace `OperationQueue`-based scheduling with a custom `TaskQueue` synchronized on `ImagePipelineActor`. Background operations like image processing and decoding now run on the default Swift Concurrency executors, eliminating unnecessary thread hops. The entire pipeline is now a good Swift Concurrency citizen\n- Replace callback-based `DataLoading` protocol with async/await: `loadData(with:)` now returns `(AsyncThrowingStream<Data, Error>, URLResponse)`. Remove `Cancellable` protocol\n- Add typed throws (`throws(ImagePipeline.Error)`) to `ImageTask.image`, `ImageTask.response`, `ImagePipeline.image(for:)`, and `ImagePipeline.data(for:)`. Add `ImagePipeline.Error.cancelled` case. Cancellation now throws this instead of `CancellationError`.\n- Change `userInfo` type from `[UserInfoKey: Any]` to `[UserInfoKey: any Sendable]` in both `ImageRequest` and `ImageContainer`\n- Add `@MainActor @Sendable` to completion-based `loadImage`/`loadData` closure parameters\n- Add `@MainActor @Sendable` to `progress` and `completion` closures in `NukeExtensions` `loadImage` functions\n- Add `@MainActor @Sendable` to all callback closures in `NukeUI`: `FetchImage.onStart`/`onCompletion`, `LazyImage.onStart`/`onCompletion` modifiers, `LazyImageView.onStart`/`onPreview`/`onProgress`/`onSuccess`/`onFailure`/`onCompletion`\n- Eliminate an actor hop during `ImageTask` startup, reducing per-request overhead\n- Synchronize `ResumableDataStorage` on `ImagePipelineActor`, replacing `NSLock` with actor isolation and removing `@unchecked Sendable`.\n- Convert unit tests to Swift Testing and enable Swift 6 mode for all tests\n\n**New Features**\n\n- Add `ImagePipeline.PreviewPolicy` (`.incremental`, `.thumbnail`, `.disabled`) to control how progressive previews are generated per-request\n- Add `ImagePipelineDelegate.previewPolicy(for:pipeline:)` for customizing the policy dynamically. Default policy: `.incremental` for progressive JPEGs and GIFs, `.disabled` for everything else (baseline JPEGs, PNGs, etc.), restoring the original behavior before `CGImageSourceCreateIncremental` was adopted\n- Add `ImagePipeline.Delegate.willLoadData(for:urlRequest:pipeline:)`, an async, throwing hook that intercepts the `URLRequest` just before data loading begins. Use it to inject auth tokens, sign requests, or perform any async pre-flight work. Throw to cancel with a meaningful error (e.g., when a token refresh fails). Default implementation returns the request unchanged — https://github.com/kean/Nuke/issues/774\n- Add `ImageRequest.init(id:image:)` that accepts an async closure returning an `ImageContainer` directly. Use it to process images already in memory or to integrate with systems that provide pre-decoded images (e.g., Photos framework). The image skips data decoding entirely and is loaded in `TaskFetchOriginalImage` – https://github.com/kean/Nuke/issues/823\n- Add type-safe `imageID`, `scale`, and `thumbnail` properties to `ImageRequest`, replacing the previous `userInfo` dictionary-based approach. The new properties are more ergonomic and improve performance by eliminating dictionary lookups and `Any` boxing. The `userInfo[.imageIdKey]`, `userInfo[.scaleKey]`, and `userInfo[.thumbnailKey]` keys are deprecated. The new `imageID` property replaces `imageId` to follow idiomatic Swift naming (uppercase \"ID\") and is now also writable – https://github.com/kean/Nuke/issues/772\n- Add `ImagePipeline.Configuration.progressiveDecodingInterval` (default: 0.5s) to throttle progressive decoding attempts when data arrives faster than the interval\n- Add `ImagePipeline.Configuration.maximumResponseDataSize` — downloads that exceed this limit are automatically cancelled. The default limit is based on the device's physical memory. Set to `nil` to disable — https://github.com/kean/Nuke/issues/738\n- Add `ImagePipeline.Configuration.maximumDecodedImageSize` — images whose decoded bitmap would exceed this limit are automatically downscaled during decoding. The default limit is calculated dynamically based on the device's physical memory. Set to `nil` to disable\n- Add `DataCache.isSweepEnabled` (`true` by default). Set it to `false` in targets that share a cache with the main app (e.g. a Notification Service Extension) so that only the main app enforces size limits via LRU sweeps\n- Add `AssetType.ico` with magic-byte detection for ICO (Windows icon) images\n- Add `ImageTask.Event.started`\n- Mark all public enums as `@frozen` (except error enums and empty namespaces)\n\n**Performance**\n\n- Rewrite `ImageProcessors.GaussianBlur` to use Accelerate (`vImageBoxConvolve`) instead of Core Image, fixing gray border artifacts and improving performance ~5.8x — https://github.com/kean/Nuke/issues/308\n- Optimize data downloading by pre-allocating the buffer using the expected content size from the HTTP response, reducing memory reallocations during image downloads (this only applies when progressive decoding is on) — https://github.com/kean/Nuke/issues/738\n- Update `ImageCache.defaultCostLimit` to 15% of physical memory with no hard cap (previously 20% capped at 512 MB). The cache uses a custom LRU policy that enforces limits precisely, so 15% is effectively more generous than the previous capped value on modern devices – https://github.com/kean/Nuke/issues/838\n- The storage cost limit of `ResumableDataStorage` is now dynamic and varies depending on the available RAM.\n- Add `consuming` to `LazyImage` builder methods (`processors`, `priority`, `pipeline`, `onStart`, `onDisappear`, `onCompletion`) and `ImageContainer.map(_:)`\n\n**API Changes**\n\n- Rename `ImagePipelineDelegate` to `ImagePipeline.Delegate`. A deprecated `ImagePipelineDelegate` typealias is provided for backward compatibility\n- Refactor `ImageDecoders.Default` to fully delegate incremental decoding to Image I/O via `CGImageSourceCreateIncremental`\n- Remove `queue` parameter from completion-based `loadImage`/`loadData` methods — callbacks now always run on the main queue\n- Remove `ImageTask.Event.cancelled` in favor of `.finished(.failure(.cancelled))` — cancellation is now uniformly represented as a failure result\n- Remove `ImageRequest.init(id:dataPublisher:)` and internal `TaskFetchWithPublisher`. Use `ImageRequest.init(id:data:)` (async closure) instead — it is now handled directly by `TaskFetchOriginalData`\n- Remove soft-deprecated per-event `ImagePipelineDelegate` methods (`imageTaskDidStart`, `didUpdateProgress`, `didReceivePreview`, `imageTaskDidCancel`, `didCompleteWithResult`). Use `imageTask(_:didReceiveEvent:pipeline:)` instead\n- Remove previously deprecated APIs: `DataCache.isCompressionEnabled`, `ImageProcessors.Resize.ContentMode` typealias, `AsyncImageTask` typealias, `ImagePipeline.Configuration.callbackQueue`, `ImagePipeline.Configuration.dataCachingQueue`, `ImagePipeline.loadData(with: URL)`, and `ImagePipeline.data(for: URL)`\n- Soft-deprecate the `userInfo` parameter in `ImageRequest` initializers in favor of dedicated type-safe properties\n\n**Bug Fixes**\n\n- Fix progressive JPEGs with large EXIF headers not producing previews — `CGImageSourceCreateIncremental` fails to recognize these files until fully downloaded. The decoder now falls back to generating a thumbnail from a non-incremental source. The issue was raised by and the initial fix provided by @theop-luma in https://github.com/kean/Nuke/pull/835\n- Fix thumbnail requests re-downloading original image data when it is already stored in the disk cache — https://github.com/kean/Nuke/issues/837\n- Fix `ImageTask.state` remaining `.running` after completion when using the completion-based `loadImage` API\n- Fix `ImageDecoders.Video.decode(_:)` returning an empty image instead of a video thumbnail — https://github.com/kean/Nuke/issues/811\n- Fix `VideoPlayerView` accumulating duplicate `AVPlayerItemDidPlayToEndTime` observers on each `play()`/`reset()` cycle, causing `onVideoFinished` to fire multiple times — https://github.com/kean/Nuke/issues/818\n\n## Nuke 12.9.0\n\n*Feb 22, 2026*\n\n- Enable Swift 6 and fix remaining concurrency warnings\n- Optimize `ImageTask` `AsyncStream` APIs and remove the Combine dependency. It now essentially has no overhead.\n- Updating misleading SVG support by @realmtai in https://github.com/kean/Nuke/pull/839\n- Fix deprecation warning typo by @cameronmcefee in https://github.com/kean/Nuke/pull/861\n- Mark `DataLoading` closures as `@Sendable` by @plu in https://github.com/kean/Nuke/pull/862\n- `.storeAll` now stores processed images for locals too, as it should be by @HyperfocusDisordered in https://github.com/kean/Nuke/pull/810\n- Add `.heic` support to `AssetType/init` so it can detect it based on the input `Data`\n- Remove some `@unchecked` markers from `Sendable` types for better Data Race Safety checking\n- Fix an issue with `DataCache` not touching `.contentAccessDate` when accessing files\n\n## Nuke 12.8.0\n\n*Jul 13, 2024*\n\n- Add full Strict Concurrency Checking and Swift 6 support \n- Add `@MainActor` annotation to `ImageLoadingOptions.shared`\n- Fix image scale and orientation issue in thumbnail creation by @prabhuamol in https://github.com/kean/Nuke/pull/793\n- Deprecate `ImagePipeline.Configuration.callbackQueue` – this feature will be removed in Nuke 13\n- `ImagePrefetcher.didComplete` closure is now annotated with `@MainActor @Sendable`\n- Drop Xcode 14 support\n\n## Nuke 12.7.3\n\n*Jun 19, 2024*\n\n- Fix “unrecognized selector sent to instance” crash in NukeExtensions in a cross-dissolve transition on iOS < 17 by @ejensen in https://github.com/kean/Nuke/pull/792\n\n## Nuke 12.7.2\n\n*Jun 8, 2024*\n\n- Fix https://github.com/kean/Nuke/issues/789, an issue with `ImageProcessors.Resize` failing to resize images with packed pixel formats \n\n## Nuke 12.7.1\n\n*May 30, 2024*\n\n- Fix fade transition in some scenarios by @ejensen in https://github.com/kean/Nuke/pull/786\n- Remove `taskDescription` from network tasks by @ejensen in https://github.com/kean/Nuke/pull/785\n- Temporarily revert the change introduced in v12.6 that would skip decompression for some image types – more info in https://github.com/kean/Nuke/issues/788\n\n## Nuke 12.7\n\n*May 18, 2024*\n\nThis release contains major improvements to the Structured Concurrency support and `ImagePipeline` internals.\n\n- Add `previews: AsyncStream<ImageResponse>`, `progress: AsyncStream<Progress>`, `image: PlatformImage async` and `response: ImageResponse async` directly to `ImageTask` and deprecate `AsyncImageTask`. These APIs have zero cost unless you use them.\n- Add `ImageTask.Event` and add `events: AsyncStream<Event>` to `ImageTask` for observing _all_ events associated with the image loading.\n- Improve the support for `AsyncStream`: a new stream is created every time you access the respective property to make it easier to have multiple consumers. \n- Add `ImagePipelineDelegate/imageTask(:didReceiveEvent:pipeline:)` and deprecate the previous methods it replaced (context: these methods were introduced in [Nuke 11.0](https://github.com/kean/Nuke/releases/tag/11.0.0) as the initial and misguided attempt at Structured Concurrency support that tried to borrow from the `URLSession` API design)\n- (Internal) Rework `ImagePipeline` that accumulated a lot of cruft after the introduction of data tasks, Combine, Async/Await, and AsyncStream support in the previous releases.\n- Deprecate `ImagePipeline/loadData(with:)` and `ImagePipeline/data(with:)` methods that accept `URL` as parameters – use the `ImageRequest` variants instead (these are rarely used and low-level APIs that don't require convenience variants)\n- Remove `@discardableResult` from `ImagePipeline/data(with:) async throws` – it was never meant to be there \n- Rename `ImageTask/progress` to `ImageTask/currentProgress` (warning: this is a small breaking change in the API)\n- Fix some of the Strict Concurrency Checking & Swift 6 warnings preparing for the upcoming Swift releases\n- Fix documentation for `AsyncImageTask/previews` that was previously specifying that it was delivering the previews _and_ the final image – it's only the previews.\n- Fix [#782], an issue with grayscale images (8 bpp) not being rendered correctly when `Resize` processor is used\n\n## Nuke 12.6\n\n*Apr 23, 2024*\n\n- Fix an issue with an optimization that is supposed to skip decompression if one or more processors are applied\n- 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`.  \n- 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\n- Fix an issue where `.storeAll` and `.automatic` cache policies would not store the thumbnail data\n- Fix https://github.com/kean/Nuke/issues/746 an issue with `ImageRequest.UserInfoKey.scaleKey` not interacting correctly with coalescing \n- Fix https://github.com/kean/Nuke/issues/763 SwiftUI Warning: Accessing StateObject's object without being installed on a View when using `onStart`\n- Fix https://github.com/kean/Nuke/issues/758 by adding support for initializing `ImageProcessors.CoreImageFilter` with `CIFilter` instances\n- Add support for disk cache lookup for intermediate processed images (as opposed to only final and original as before)\n- 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\n- Deprecate `ImagePipeline.Configuration.dataCachingQueue` and perform data cache lookups on the pipeline's queue, reducing the amount of context switching\n- Update the infrastructure for coalescing image-processing tasks to use the task-dependency used for other operations\n\n## Nuke 12.5\n\n*Mar 23, 2024*\n\n- Fix Xcode 15.3 concurrency warnings when using `Screen.scale` by @jszumski in https://github.com/kean/Nuke/pull/766\n- Add `showPlaceholderOnFailure` parameter to show placeholder in case of image loading failure by @mlight3 in https://github.com/kean/Nuke/pull/764\n- Fix image loading test on iOS 17 by @woxtu in https://github.com/kean/Nuke/pull/768\n- Update thumbnail key value for `ImageRequest` by @woxtu in https://github.com/kean/Nuke/pull/769\n- Remove trailing whitespaces by @woxtu in https://github.com/kean/Nuke/pull/767\n- Apply `if let` shorthand syntax by @mlight3 in https://github.com/kean/Nuke/pull/762\n\n## Nuke 12.4\n\n*Feb 10, 2024*\n\n- Enable visionOS support for all APIs by @zachwaugh in https://github.com/kean/Nuke/pull/752\n- Update documentation by @tkersey in https://github.com/kean/Nuke/pull/747\n\n## Nuke 12.3\n\n*Jan 6, 2024*\n\n- Add support for visionOS by @bobek-balinek in https://github.com/kean/Nuke/pull/743\n\n## Nuke 12.2\n\n*Nov 23, 2023*\n\n- Add another file type signature for .m4v files by @leonid-shevtsov in https://github.com/kean/Nuke/pull/735\n- Added the onStart callback to SwiftUI.LazyImage by @urbaneewe in https://github.com/kean/Nuke/pull/736\n\n## Nuke 12.1.6\n\n*Aug 19, 2023*\n\n- Improve `ImageCache` performance (20%)\n- Improve `NukeExtensions` performance (5%)\n- Update the code to support future visionOS releases by switching to `canImport` where possible\n\n## Nuke 12.1.5\n\n*Jul 29, 2023*\n\n- Fix https://github.com/kean/Nuke/issues/717 by moving `DataCache` metadata to a hidden file - https://github.com/kean/Nuke/pull/718\n\n## Nuke 12.1.4\n\n*Jul 22, 2023*\n\n- Upgrade to [`CryptoKit`](https://developer.apple.com/documentation/cryptokit) from `CommonCrypto` and slightly optimize how cryptographic hashes are converted to strings (used as filenames for `DataCache`)\n- Deprecate `DataCache/isCompressionEnabled`. It was initially added as a general-purpose feature, but it's not recommended to be used with most image formats.\n- `DataCache` now performs sweeps less frequently\n- Minor docs correction – https://github.com/kean/Nuke/pull/715 by @tdkn\n\n## Nuke 12.1.3\n\n*Jul 10, 2023*\n\n- Fix https://github.com/kean/Nuke/issues/709: `LazyImage` fails to perform memory cache lookup in some scenarios\n\n## Nuke 12.1.2\n\n*Jun 25, 2023*\n\n- Fix https://github.com/kean/Nuke/issues/710: build failure on watchOS in debug mode – https://github.com/kean/Nuke/pull/711 by @FieryFlames\n\n## Nuke 12.1.1\n\n*Jun 22, 2023*\n\n- Fix https://github.com/kean/Nuke/issues/693: `ImageRequest` created with an async function now executes it lazily - https://github.com/kean/Nuke/pull/708 by @khlopko\n- Fix https://github.com/kean/Nuke/issues/695: `byCroppingToSquare()` always return square image – https://github.com/kean/Nuke/pull/696 by @zzmasoud\n- Update unit tests – https://github.com/kean/Nuke/pull/701 by @woxtu \n- Fix upcoming warnings in Xcode 15\n\n## Nuke 12.1\n\n*Mar 25, 2023*\n\n- Add `makeImageView` closure to `LazyImageView` to allow using custom views for rendering images\n- Add `onCompletion` closure to `LazyImage` and `FetchImage`\n- Fix an issue with `.videoAssetKey` value missing from `ImageContainer`\n- Fix an issue with `.gif` being encoded as `.jpeg` when `.storeEncodedImages` policy is used \n\n## Nuke 12.0\n\n*Mar 4, 2023*\n\nNuke 12 enhances the two main APIs introduced in the previous release: `LazyImage` and the async `ImagePipeline` methods. They are faster, more robust, and easier to use.\n\n> The [migration guide](https://github.com/kean/Nuke/blob/nuke-12/Documentation/Migrations/Nuke%2012%20Migration%20Guide.md) is available to help with the update. The minimum requirements are unchanged from Nuke 11.\n\n### Concurrency\n\nRedesign the concurrency APIs making them more ergonomic and fully `Sendable` compliant.\n\n- Add `ImagePipeline/imageTask(with:)` method that returns a new type `AsyncImageTask`\n\n```swift\nlet task = ImagePipeline.shared.imageTask(with: URL(string: \"example.com\"))\ntask.priority = .high\nfor await progress in task.progress {\n    print(\"Updated progress: \", progress)\n}\nlet image = try await task.image\n```\n\n- The existing convenience `ImagePipeline/image(for:)` method now returns an image instead of `ImageResponse`\n- Remove the `delegate` parameter from `ImagePipeline/image(for:)` method to address the upcoming concurrency warnings in Xcode 14.3\n- Remove `ImageTaskDelegate` and move its methods to `ImagePipelineDelegate` and add the `pipeline` parameter\n\n### NukeUI 2.0\n\nNukeUI started as a separate [repo](https://github.com/kean/NukeUI), but the initial production version was released as part of [Nuke 11](https://github.com/kean/Nuke/releases/tag/11.0.0). Let's call it NukeUI 1.0. The framework was designed before the [`AsyncImage`](https://developer.apple.com/documentation/swiftui/asyncimage) announcement and had a few discrepancies that made it harder to migrate from `AsyncImage`. This release addresses the shortcomings of the original design and features a couple of performance improvements.\n\n- `LazyImage` now uses `SwiftUI.Image` instead of `NukeUI.Image` backed by `UIImageView` and `NSImageView`. It eliminates any [discrepancies](https://github.com/kean/Nuke/issues/578) between `LazyImage` and `AsyncImage` layout and self-sizing behavior and fixes issues with `.redacted(reason:)`, `ImageRenderer`, and other SwiftUI APIs that don't work with UIKIt and AppKit based views.\n- Remove `NukeUI.Image` so the name no longer [clashes](https://github.com/kean/Nuke/discussions/658) with `SwiftUI.Image`\n- Fix [#669](https://github.com/kean/Nuke/issues/669): `redacted` not working for `LazyImage`\n- GIF rendering is no longer included in the framework. Please consider using one of the frameworks that specialize in playing GIFs, such as [Gifu](https://github.com/kaishin/Gifu). It's easy to integrate, especially with `LazyImage`.\n- Extract progress updates from `FetchImage` to a separate observable object, reducing the number of body reloads\n- `LazyImage` now requires a single body calculation to render the response from the memory cache (instead of three before)\n- Disable animations by default\n- Fix an issue where the image won't reload if you change only `LazyImage` `processors` or `priority` without also changing the image source\n- `FetchImage/image` now returns `Image` instead of `UIImage`\n- Make `_PlatformImageView` internal (was public) and remove more typealiases\n\n### Nuke\n\n- Add a new initializer to `ImageRequest.ThumbnailOptions` that accepts the target size, unit, and content mode - [#677](https://github.com/kean/Nuke/pull/677)\n- ImageCache uses 20% of available RAM which is quite aggressive. It's an OK default on iOS because it clears 90% of the used RAM when entering the background to be a good citizen. But it's not a good default on a Mac. Starting with Nuke 12, the default size is now strictly limited to 512 MB.\n- `ImageDecoder` now defaults to scale `1` for images (configurable using [`UserInfoKey/scaleKey`](https://kean-docs.github.io/nuke/documentation/nuke/imagerequest/userinfokey/scalekey))\n- Removes APIs deprecated in the previous versions\n- Update the [Performance Guide](https://kean-docs.github.io/nuke/documentation/nuke/performance-guide)\n\n### NukeVideo\n\nVideo playback can be significantly [more efficient](https://web.dev/replace-gifs-with-videos/) than playing animated GIFs. This is why the initial version of NukeUI provided support for basic video playback. But it is not something that the majority of the users need, so this feature was extracted to a separate module called `NukeVideo`.\n\nThere is now less code that you need to include in your project, which means faster compile time and smaller code size. With this and some other changes in Nuke 12, the two main frameworks – Nuke and NukeUI – now have 25% less code compared to Nuke 11. In addition to this change, there are a couple of improvements in how the precompiled binary frameworks are generated, significantly reducing their size.\n\n- Move all video-related code to `NukeVideo`\n- Remove `ImageContainer.asset`. The asset is now added to `ImageContainer/userInfo` under the new `.videoAssetKey`.\n- Reduce the size of binary frameworks by up to 50%\n\n# Nuke 11\n\n## Nuke 11.6.4\n\n*Feb 19, 2023*\n\n- Fix [#671](https://github.com/kean/Nuke/pull/671): `ImagePipeline/image(for:)` hangs if you cancel the async Task before it is started \n\n## Nuke 11.6.3\n\n*Feb 18, 2023*\n\n- Fix warnings in Xcode 14.3\n\n## Nuke 11.6.2\n\n*Feb 9, 2023*\n\n- Fix an issue with static GIFs not rendered correctly – [#667](https://github.com/kean/Nuke/pull/667) by [@Havhingstor](https://github.com/Havhingstor)\n\n## Nuke 11.6.1\n\n*Feb 5, 2023*\n\n- Fix [#653](https://github.com/kean/Nuke/issues/653): ImageView wasn't calling `prepareForReuse` on its `animatedImageView`\n\n## Nuke 11.6.0\n\n*Jan 27, 2023*\n\n- Fix [#579](https://github.com/kean/Nuke/issues/579): `ImageEncoders.ImageIO` losing image orientation - [#643](https://github.com/kean/Nuke/pull/643)\n- Deprecate previously soft-deprecated `ImageRequestConvertible` - [#642](https://github.com/kean/Nuke/pull/642)\n- Add `isCompressionEnabled` option to `DataCache` that enables compression using Apple’s [lzfse](https://en.wikipedia.org/wiki/LZFSE) algorithm\n- Add `ExpressibleByStringLiteral` conformance to `ImageRequest`\n- Make compatible with Swift 6 mode\n\n## Nuke 11.5.3\n\n*Jan 4, 2023*\n\n- Remove DocC files to address https://github.com/kean/Nuke/issues/609\n\n## Nuke 11.5.1\n\n*Dec 25, 2022*\n\n- Fix `ImagePipeline.shared` warning with Strict Concurrency Checking set to Complete\n- Fix an issue where `ImagePrefetcher/didComplete` wasn't called in some scenarios\n- `ImagePrefetcher/didComplete` is now called on the main queue\n\n## Nuke 11.5.0\n\n*Dec 17, 2022*\n\n- `DataLoader/delegate` now gets called for all `URLSession/delegate` methods, not just the ones required by [Pulse](https://github.com/kean/Pulse). It allows you to modify `DataLoader` behavior in new ways, e.g. for handling authentication challenges.\n- Add new unit tests, thanks to [@zzmasoud](https://github.com/zzmasoud) - [#626](https://github.com/kean/Nuke/pull/626)\n- Fix an issue with `ImagePrefetcher/didComplete` not being called when images are in the memory cache, thanks to [@0xceed](https://github.com/0xceed) - [#635](https://github.com/kean/Nuke/pull/635)\n- Move .docc folders back to Sources/, so that the Nuke docs are now again available in Xcode\n\n## Nuke 11.4.1\n\n*Dec 15, 2022*\n\n- Correct the release commit/branch\n\n## Nuke 11.4.0\n\n*Dec 14, 2022*\n\n- Add `isVideoFrameAnimationEnabled` option to NukeUI views, thanks to [@maciesielka](https://github.com/maciesielka) \n\n## Nuke 11.3.1\n\n*Oct 22, 2022*\n\n- Fix deprecated `withTaskCancellationHandler` usage - [#614](https://github.com/kean/Nuke/pull/614), thanks to [@swasta](https://github.com/swasta)\n- Fix xcodebuild & docc build issue on Xcode 14.0.1 - [#609](https://github.com/kean/Nuke/issues/609)\n\n## Nuke 11.3.0\n\n*Sep 17, 2022*\n\n- Add support for loading image into `TVPosterView` (tvOS) - [#602](https://github.com/kean/Nuke/pull/602), thanks to [@lukaskukacka](https://github.com/lukaskukacka)\n\n## Nuke 11.2.1\n\n*Sep 10, 2022*\n\n- Fix an issue with Mac Catalyst on Xcode 14.0  \n\n## Nuke 11.2.0\n\n*Sep 10, 2022*\n\n- Add support for Xcode 14.0\n- Fix [#595](https://github.com/kean/Nuke/issues/595) – compilation issue on macOS\n\n## Nuke 11.1.1\n\n*Aug 16, 2022*\n\n- **Breaking** Progressive decoding is now disabled by default as a way to mitigate [#572](https://github.com/kean/Nuke/issues/572)\n- Add `prefersIncrementalDelivery` to `DataLoader`. When progressive decoding is disabled, it now uses `prefersIncrementalDelivery` on `URLSessionTask`, slightly increasing the performance\n- Fix an issue with placeholder not being shown by `LazyImage` when the initial URL is `nil` – [#586](https://github.com/kean/Nuke/pull/586), thanks to @jeffreykuiken\n- Add convenience options to `Image` and `LazyImage`: `resizingMode(_:)`, `videoRenderingEnabled(_:)`, `videoLoopingEnabled(_:)`, `animatedImageRenderingEnabled(_:)`\n- Fix an issue where `AVPlayerLayer` was created eagerly\n- Disable `prepareForDisplay` by default and add a configuration option to enable it\n\n## Nuke 11.1.0\n\n*Aug 7, 2022*\n\n- Add `DataLoader` delegate for easy Pulse integration - [#583](https://github.com/kean/Nuke/pull/583)\n- Add missing content mode to NukeUI - [#582](https://github.com/kean/Nuke/pull/582), thanks to [Ethan Pippin](https://github.com/LePips)\n\n## Nuke 11.0.1\n\n*Jul 24, 2022*\n\n- Fix an issue with cancellation of requests created with Combine publishers - [#576](https://github.com/kean/Nuke/pull/576), thanks to [douknow](https://github.com/douknow)  \n\n## Nuke 11.0.0\n\n*Jul 20, 2022*\n\n**Nuke 11** embraces **Swift Structured Concurrency** with full feature parity with legacy completion-based APIs. **NukeUI** is now part of the main repo. Docs were completely rewritten using DocC and hosted on GitHub: [Nuke](https://kean-docs.github.io/nuke/documentation/nuke/), [NukeUI](https://kean-docs.github.io/nukeui/documentation/nukeui/), [NukeExtensions](https://kean-docs.github.io/nukeextensions/documentation/nukeextensions/).\n\nThere are no major source-breaking changes in this release. Instead, it adds dozens of API refinements to make the framework more ergonomic.\n\n- Increase the minimum supported Xcode version to 13.3\n- Increase minimum supported platforms: iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15\n\n### Structured Concurrency\n\nExtend Async/Await APIs to have complete feature parity with the existing completion-based APIs paving the road for its eventual deprecation and removal in the future major versions.\n\n- Add `@MainActor` to the following types: `FetchImage`, `LazyImage`, `LazyImageView`, Nuke `loadImage(into:)` method\n- Add `Sendable` to most of the Nuke types, including `ImagePipeline`, `ImageRequest`,` ImageResponse`, `ImageContainer`, `ImageTask`, and more\n- Add `ImageTaskDelegate` to achieve complete feature-parity with completion-based APIs - [#559](https://github.com/kean/Nuke/pull/559)\n- `ImageRequest` now accepts async/await function to fetch data as a resource\n\nLoading an image and monitoring download progress:\n\n```swift\nfunc loadImage() async throws {\n    let response = try await pipeline.image(for: \"https://example.com/image.jpeg\", delegate: self)\n}\n\nfunc imageTaskCreated(_ task: ImageTask) {\n    // You can capture the task instance here to change priority later, etc\n}\n\nfunc imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress) {\n    // Update progress\n}\n\nfunc imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse) {\n    // Display progressively decoded image\n}\n\n// And more...\n```\n\n### NukeUI and NukeExtensions\n\n**NukeUI** is now part of the main repo and the existing UIKit and AppKit UI extensions were moved from the main module to **NukeExtensions** and soft-deprecated.\n\n- Move [NukeUI](https://github.com/kean/NukeUI) to the main Nuke repo\n- Move `UIImageView` / `NSImageView` extensions to a separate target `NukeExtensions` and soft-deprecated them - [#555](https://github.com/kean/Nuke/pull/555)\n- Remove deprecated APIs from NukeUI\n- Add `ImageResponse` typealias to NukeUI\n- Use new `ImageTask.Progress` in NukeUI\n- NukeUI no longer exposes public Gifu dependency or its APIs\n\n### Error Reporting Improvements\n\nA complete overhaul of `ImagePipeline.Error` with many new cases covering every single point of failure in the pipeline.\n\n- Add `throws` to \"advanced\" `ImageProcessing`\n- Add `throws` to `ImageDecoding`\n- Add support for throwing processing in `ImageProcessors.CoreImageFilter`\n- Add `ImageDecoding` instance, `ImageDecodingContext`, and underlying error to `.decodingFailed` error case\n- Add `ImageProcessingContext` and underlying error to `.processingFailed` error case\n- Add `.dataMissingInCache` error case for a scenario where data is missing in cache and download is disabled using `.returnCacheDataDontLoad`.\n- Add `.dataIsEmpty` error case for a scenario where the data loader doesn't report an error, but the response is empty.\n- Add `.decoderNotRegistered(context:)` error case for a scenario where no decoders are registered for the downloaded data. This should never happen unless you remove the default decoder from the registry.\n- Add `.imageRequestMissing` error case for a scenario when the load image method is called with no image request.\n- Add `cacheType` to `ImageDecodingContext`\n\n### Other Changes\n\n- Fix [#511](https://github.com/kean/Nuke/issues/511) `OSAtomic` deprecation warnings - [#573](https://github.com/kean/Nuke/pull/573)\n- Add `ImageTask.State`. Improve performance when canceling and changing priority of completed tasks.\n- Add `ImageTask.Progress` to simplify progress reporting APIs\n- Add `ImageRequest.Options.skipDecompression`\n- Add public `ImageCacheKey` initializer with ``ImageRequest``\n- Add `imageCache(for:pipeline:)` method to `ImagePipelineDelegate`\n- Add automatic `hashableIdentifier` implementation to `ImageProcessing` types that implement `Hashable` protocol - [#563](https://github.com/kean/Nuke/pull/563)\n- Add a way to customize decompression using `ImagePipelineDelegate`\n- Add `ImageRequest` to `ImageResponse`\n- Improve decompression performance by using [`preparingForDisplay`](https://developer.apple.com/documentation/uikit/uiimage/3750834-preparingfordisplay) on iOS 15 and tvOS 15\n- Add metrics reporting using `DataLoaderObserving` protocol\n- Add custom disk caching for requests backed by data publishers - [#553](https://github.com/kean/Nuke/pull/553)\n- Add `.pipelineInvalidated` error that is thrown for new requests started on the invalidated pipeline\n- Add public write access to `ImageDecodingContext`,  `ImageProcessingContext`, `ImageResponse` properties\n- Add static `default` and `imageIO` functions to `ImageEncoding` protocol for easy creating of encoders\n- Add `sizeLimit` to `withDataCache` `ImagePipeline.Configuration` initializer\n- Make `ImageCache` `ttl` optional instead of using `0` as a \"never expires\" indicator\n\n### Removals and Deprecations\n\n- Soft-deprecate `ImageRequestConvertible` and use `ImageRequest` and `URL` directly in all news APIs for better discoverability and performance  - [#567](https://github.com/kean/Nuke/pull/567)\n- Deprecate `ImageDecoderRegistering`\n- Deprecate `ImageCaching` extension that works with `ImageRequest` \n- Rename `isFinal` in `ImageProcessingContext` to `isCompleted` to match the renaming APIs\n- Rename `ImagePipeline/Configuration/DataCachePolicy` to `ImagePipeline/DataCachePolicy`\n- Remove `ImageRequestConvertible` conformance from `String`\n- Remove `ImageTaskEvent` and consolidate it with the new `ImageTaskDelegate` API - [#564](https://github.com/kean/Nuke/pull/564)\n- Remove progress monitoring using `Foundation.Progress`\n- Remove `WKInterfaceObject` support (in favor of SwiftUI)\n- Remove `ImageType` typealias (deprecated in 10.5)\n- Remove `Cancellable` conformance from `URLSessionTask`\n- Remove public `ImagePublisher` class (make it internal)\n\n### Non-Code Changes\n\n- Automatically discover typos on CI - [#549](https://github.com/kean/Nuke/pull/549)\n- Remove `CocoaPods` support\n\n# Nuke 10\n\n## Nuke 10.11.2\n\n*Jun 9, 2022*\n\n- Revert changes to the deployment targets introduced in Nuke 10.10.0\n\n## Nuke 10.11.1\n\n*Jun 9, 2022*\n\n- Fix an issue with data not always being attached to an error when decoding fails\n\n## Nuke 10.11.0\n\n*Jun 8, 2022*\n\n- Add associated `Data` to `ImagePipeline.Error.decodingFailed` - [#545](https://github.com/kean/Nuke/pull/545), thanks to [Shai Mishali](https://github.com/freak4pc)\n\n> There are other major improvements to error reporting coming in [Nuke 11](https://github.com/kean/Nuke/pull/547)\n\n## Nuke 10.10.0\n\n*May 21, 2022*\n\n- Remove APIs deprecated in Nuke 10.0\n- Increase minimum deployment targets\n\n## Nuke 10.9.0\n\n*May 1, 2022*\n\n- Rename async/await `loadImage(with:)` method to `image(for:)`, and `loadData(with:)` to `data(for:)`\n- Add `Sendable` conformance to some of the types\n\n## Nuke 10.8.0\n\n*Apr 24, 2022*\n\n- Add async/await support (requires Xcode 13.3) – [#532](https://github.com/kean/Nuke/pull/532)\n\n```swift\nextension ImagePipeline {\n    public func loadImage(with request: ImageRequestConvertible) async throws -> ImageResponse\n    public func loadData(with request: ImageRequestConvertible) async throws -> (Data, URLResponse?)\n}\n\nextension FetchImage {\n    public func load(_ action: @escaping () async throws -> ImageResponse)\n}\n```\n\n## Nuke 10.7.2\n\n*Apr 23, 2022*\n\n- Remove code deprecated in Nuke 9.4.1\n\n## Nuke 10.7.1\n\n*Jan 27, 2022*\n\n- Fix intermittent SwiftUI crash in NukeUI/FetchImage \n\n## Nuke 10.7.0\n\n*Jan 24, 2022*\n\n- Fix M4V support – [#523](https://github.com/kean/Nuke/pull/523), thanks to [Son Changwoo](https://github.com/kor45cw)\n- Make `ImagePrefetcher` `didComplete` closure public – [#528](https://github.com/kean/Nuke/pull/515), thanks to [Winston Du](https://github.com/winstondu)\n- Rename internal `didEnterBackground` selector - [#531](https://github.com/kean/Nuke/issues/531)\n\n## Nuke 10.6.1\n\n*Dec 27, 2021*\n\n- Remove async/await support\n\n## Nuke 10.6.0\n\n*Dec 27, 2021*\n\nThis release added async/await, but the change was [reverted](https://github.com/kean/Nuke/issues/526) in 10.6.1 (for CocoaPods) and the release was deleted in GitHub.\n\n## Nuke 10.5.2\n\n*Dec 2, 2021*\n\n- Revert `preparingForDisplay` changes made in [#512](https://github.com/kean/Nuke/pull/512)\n- Add URLSession & URLSessionDataTask descriptions - [#517](https://github.com/kean/Nuke/pull/517), thanks to [Stavros Schizas](https://github.com/sschizas)\n\n## Nuke 10.5.1\n\n*Oct 23, 2021*\n\n- Fix build for Catalyst\n\n## Nuke 10.5.0\n\n*Oct 23, 2021*\n\n- Improve image decompression performance on iOS 15 and tvOS 15 by using [preparingForDisplay()](https://developer.apple.com/documentation/uikit/uiimage/3750834-preparingfordisplay?language=o_5) (requires Xcode 13) - [#512](https://github.com/kean/Nuke/pull/512)\n- On iOS 15, tvOS 15, image decompression now preserves 8 bits per pixel for grayscale images - [#512](https://github.com/kean/Nuke/pull/512)\n- Adopt extended static member lookup ([SE-0299](https://github.com/apple/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md)) (requires Xcode 13) - [#513](https://github.com/kean/Nuke/pull/513)\n\n```swift\n// Before\nImageRequest(url: url, processors: [ImageProcessors.Resize(width: 320)])\n\n// After\nImageRequest(url: url, processors: [.resize(width: 320)])\n```\n\n- `ImageRequest` now takes a *non-optional* array of image processors in its initializers. This change is required to mitigate an Xcode issue where it won't suggest code-completion for [SE-0299](https://github.com/apple/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md) - [#513](https://github.com/kean/Nuke/pull/513)\n- Add `ImageDecoders.Video` (registered by default)\n\n## Nuke 10.4.1\n\n*Aug 30, 2021*\n\n- Fix build on watchOS (needs investigation why xcodebuild returns 0 for failed watchOS builds) - [#505](https://github.com/kean/Nuke/pull/505), thanks to [David Harris](https://github.com/thedavidharris)\n\n## Nuke 10.4.0\n\n*Aug 28, 2021*\n\n- Add an API for efficiently creating image thumbnails or retrieving existing ones - [#503](https://github.com/kean/Nuke/pull/503)\n- Fix an issue with scale (`ImageRequest.UserInfoKey.scaleKey`) not being applied to progressively decoded images\n\n## Nuke 10.3.4\n\n*Aug 26, 2021*\n\n- Fix an issue where if you pass incorrect strings (`String`) in the request, the pipeline eventually start failing silently - [#502](https://github.com/kean/Nuke/pull/502) \n\n## Nuke 10.3.3\n\n*Aug 18, 2021*\n\n- Fix an issue with disk cache images being overwritten in some scenarios (with disk cache policies that enable encoding and storage of the processed images) - [#500](https://github.com/kean/Nuke/pull/500) \n\n## Nuke 10.3.2\n\n*Jul 30, 2021*\n\n- Add podspec\n\n## Nuke 10.3.1\n\n*Jul 8, 2021*\n\n- Fix `ImagePublisher` crash with some Combine operators combinations - [#494](https://github.com/kean/Nuke/pull/494), thanks to [Tyler Nickerson](https://github.com/Nickersoft)\n\n## Nuke 10.3.0\n\n*Jun 10, 2021*\n\n- Add `animation` property to `FetchImage` that significantly simplifies how to animate image appearance\n- Add `imageType` parameter to `ImageDecoders.Empty`\n- Add an option to override image scale (`ImageRequest.UserInfoKey.scaleKey`)\n\n## Nuke 10.2.0\n\n*Jun 6, 2021*\n\n> See also [Nuke 10.0 Release Notes](https://github.com/kean/Nuke/releases/tag/10.0.0)\n\n- `ImageDecoders.Default` now generates previews for GIF\n- Add `onSuccess`, `onFailure`, and other callbacks to `FetchImage` \n- Add progressive previews in memory cache support to `FetchImage`\n- Add a convenience property with an `ImageContainer` to `FechImage`\n- Update `FetchImage` `loadImage()` method that takes publisher to no longer require error to match `ImagePipeline.Error`   \n- Add an option to set default processors via `FetchImage`\n\n## Nuke 10.1.0\n\n*Jun 3, 2021*\n\n- Enable progressive decoding by default – it can now be done without sacrificing performance in any meaningful way. To disable it, set `isProgressiveDecodingEnabled` to `false`.\n- Enable storing progressively decoding previews in the memory cache by default (`isStoringPreviewsInMemoryCache`)\n- Add `isAsynchronous` property to `ImageDecoding` that allows slow decoders (such as custom WebP decoder) to be executed on a dedicated operation queue (the existing `imageDecodingQueue`), while allows fast decoders to be executed synchronously\n- Add `entryCostLimit` property to `ImageCache` that specifies the maximum cost of a cache entry in proportion to the `costLimit`. `0.1`, by default.\n\n## Nuke 10.0.1\n\n*Jun 1, 2021*\n\n- Fix watchOS target\n\n## Nuke 10.0\n\n*Jun 1, 2021*\n\nNuke 10 is extreme in every way. It is faster than the previous version (up to 30% improvement to some operations), more powerful, more ergonomic, and is even easier to learn and use. It brings big additions to the caching infrastructure, great SwiftUI and Combine support, and more ways to adjust the system to fit your needs.\n\nThis release is also a massive step-up in the general quality of the framework. It has many improvements to the docs (for example, a complete rewrite of the [caching guide](https://kean.blog/nuke/guides/caching)), more inline comments, more unit tests (Nuke now has ~100% test coverage with 2x number of lines of code in the test target compared to the main target). It's as reliable as it gets.\n\n> **Migration.** The compiler will assist you with the migration, but if something isn't clear, there is a comprehensive [migration guide](https://github.com/kean/Nuke/blob/master/Documentation/Migrations/Nuke%2010%20Migration%20Guide.md) available.\n>\n> **Switching.** Switching from Kingfisher? There is now a [dedicated guide](https://github.com/kean/Nuke/blob/master/Documentation/Switch/switch-from-kingfisher.md) available to assist you. There is also one for [migrating from SDWebImage](https://github.com/kean/Nuke/blob/master/Documentation/Switch/switch-from-sdwebimage.md).\n\n### Caching\n\n- Add [`DataCachePolicy`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Configuration_DataCachePolicy/) to replace deprecated `DataCacheOptions.storedItems`. The new policy fixes some of the inefficiencies of the previous model and provides more control. For example, one of the additions is an [`.automatic`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Configuration_DataCachePolicy/#imagepipeline.configuration.datacachepolicy.automatic) policy: for requests with processors, encode and store processed images; for requests with no processors, store original image data. You can learn more about the policies and other caching changes in [\"Caching: Cache Policy.\"](https://kean.blog/nuke/guides/caching#cache-policy)\n-  Add [`ImagePipeline.Cache`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Cache/) with a whole range of convenience APIs for managing cached images: read, write, remove images from all cache layers.\n- Add [`ImagePipeline.Configuration.withDataCache`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Configuration/#imagepipeline.configuration.withdatacache) (aggressive disk cache enabled) and [`withURLCache`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Configuration/#imagepipeline.configuration.withurlcache) (HTTP disk cache enabled) to make it easier to set up a pipeline with a configuration you want. Learn more in [\"Caching: Configuration.\"](https://kean.blog/nuke/guides/caching#configuration)\n- Add `removeAll()` method to `ImageCaching` and `DataCaching` protocols\n- Add `containsData(for:)` method to `DataCaching` and `DataCache` which checks if the data exists without bringing it to memory\n- Add [`ImageResponse.CacheType`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageResponse_CacheType/) to address [#361](https://github.com/kean/Nuke/issues/361) and [#435](https://github.com/kean/Nuke/issues/435). It defines the source of the retrieved image.\n- The pipeline no longer stores images fetched using file:// and data:// schemes in the disk cache\n- `ImageCaching` protocols now works with a new [`ImageCacheKey`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageCacheKey/) type (an opaque container) instead of `ImageRequest`. If you are providing a custom implementation of the `ImageCaching` protocol, it needs to be updated. It is now easier because there is no need to come up with a key.\n\n### NukeUI (Beta)\n\n[NukeUI](https://github.com/kean/NukeUI) is a new Swift package. It is a comprehensive solution for displaying lazily loaded images on Apple platforms. \n\nIt uses [Nuke](https://github.com/kean/Nuke) for loading images and has all customization options you can possibly imagine. It also supports animated GIFs rendering thanks to [Gifu](https://github.com/kaishin/Gifu) and caching and displayng short videos as a more efficient alternative to GIF.\n\nThe library contains two types:\n\n- `LazyImage` for SwiftUI\n- `LazyImageView` for UIKit and AppKit\n\nBoth views have an equivalent sets of APIs.\n\n```swift\nstruct ContainerView: View {\n    var body: some View {\n        LazyImage(source: \"https://example.com/image.jpeg\")\n            .placeholder { Image(\"placeholder\") }\n            .transition(.fadeIn(duration: 0.33))\n    }\n}\n```\n\n### SwiftUI\n\nNuke now has first-class SwiftUI support with [FetchImage](https://kean.blog/nuke/guides/swiftui) which is now part of the main repo, no need to install it separately. It also has a couple of new additions:\n\n- Add `result` property (previously you could only access the loaded image)\n- Add `AnyPublisher` support via a new `func load<P: Publisher>(_ publisher: P) where P.Output == ImageResponse, P.Failure == ImagePipeline.Error` method. You can use it with a custom publisher created by combining publishers introduced in [Nuke 9.6](https://github.com/kean/Nuke/releases/tag/9.6.0).\n- Add `ImageRequestConvertible` support\n\n### Combine\n\nNuke 10 goes all-in on Combine. [`ImagePublisher`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePublisher/) was initially introduced in the previous release, [Nuke 9.6](https://github.com/kean/Nuke/releases/tag/9.6.0), and now Combine is supported across the framework.\n\n- `ImageRequest` now supports Combine Publisher via a new initializer `ImageRequest(id:data:)` where `data` is a `Publisher`. It can be used in a variety of scenarios, for example, loading data using `PhotosKit.\n- As mentioned earlier, [`FetchImage`](https://kean-org.github.io/docs/nuke/reference/10.0.0/FetchImage/) now also supports publishers. So when you create a publisher chain, there is now an easy way to display it.\n\n### ImageRequest.Options\n\nNuke 10 has a reworked [`ImageRequest.Options`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequest_Options/) option set replacing removed `ImageRequestOptions`. The name is similar, but the options are slightly different. The new approach has more options while being optimized for performance. `ImageRequest` size in memory reduced from 176 bytes to just 48 bytes (3.7x smaller).\n\n- Deprecate [`ImageRequest.CachePolicy`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequest_CachePolicy/) which is now part of the new [`ImageRequest.Options`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequest_Options/) option set\n- Remove `filteredURL`, you can now pass it using `userInfo` and [`.imageIdKey`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequest_UserInfoKey/#imagerequest.userinfokey.imageidkey) key instead. It's a rarely used option, and this is why it is now less visible.\n- Remove `cacheKey` and `loadKey` (hopefully, nobody is using it because these weren't really designed properly). You can now use the new methods of [`ImagePipeline.Delegate`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipelineDelegate/) that allows customizing the keys.\n- Add more options for granular control over caching and loading. For example, [`ImageRequest.Options`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequest_Options/) has a new [`.disableDiskCache`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequest_Options/#imagerequest.options.disablediskcache) option.\n- Move `userInfo` directly to `ImageRequest`. It's now easier to pass and it allows the framework to perform some additional optimizations.\n- `userInfo` now uses [`ImageRequest.UserInfoKey`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequest_UserInfoKey/) wrapper for keys replacing `AnyHashable`. The new approach is faster and adds type-safety.\n\n### ImagePipeline.Delegate\n\n- Add [`ImagePipeline.Delegate`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipelineDelegate/) with a variety of advanced per-request customization options that were previously not possible. For example, with [`dataCache(for:pipeline:)`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipelineDelegate/) method you can specify a disk cache for each request. With [`will​Cache(data:​image:​for:​pipeline:​completion:​)`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipelineDelegate/#imagepipelinedelegate.willcache(data:image:for:pipeline:completion:)) you can disable caching per-request or modify the cached data. And there are more.\n- Deprecated `ImagePipelineObserving` protocol is now fully covered by `ImagePipeline.Delegate`\n\n### Performance\n\n- `ImageRequest` size in memory reduced from 176 bytes to just 48 bytes (3.7x smaller), which is due to the OptionSet usage and also reordering of properties to take advantage of gaps in memory stride. The size of other types was also reduced, but not as dramatically. For example, `ImageTask` and `ImagePipeline.Configuration` now also take a bit less memory\n- Coalescing now supports even more scenarios. For example, setting `ImageRequest` `options` with a cache policy no longer prevents coalescing of data tasks.\n- The pipeline now performs memory cache lookup of intermediate (not all processors are applied) progressive image previews and apply the remaining processors on demand\n- Extend fast track decoding to the disk cache lookup\n- For cache policies that require image encoding, encode decompressed images instead of uncompressed ones\n\n### Nuke Builder\n\n[NukeBuilder](https://github.com/kean/NukeBuilder/) is a package that adds a convenience API for creating image requests inspired by SwiftUI. It was updated with support Nuke 10 and some quality-of-life improvements.\n\n- Rename package to NukeBuilder\n- Update to Nuke 10.0\n- Add [`ImageRequestConvertible`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequestConvertible/) support which means it now supports more types: `URLRequest` and `String`\n- Add Combine support\n- Add `ImagePipeline` typealias for convenience – you only need to import `NukeBuilder` in many cases\n\n### Other\n\n- Increase minimum required Xcode version to 12; no changes to the supported platforms\n- New releases now come with a pre-compile XCFramework\n- `Nuke.loadImage()` methods now work with optional image requests. If the request is `nil`, it handles the scenario the same way as failure.\n- `ImageRequest` now also works with optional `URL`\n- `String` now also conforms to `ImageRequestConvertible`, closes [#421](https://github.com/kean/Nuke/issues/421)\n- Optional `URL` now also conforms to `ImageRequestConvertible`\n- Streamline pipeline callback closures\n- Pass failing processor to `ImagePipeline.Error.processingFailed`\n- Add type-safe `ImageContainer.UserInfoKey` and `ImageRequest.UserInfoKey`\n- Pass additional parameter `Data?` to `nuke_display` (ImageView extensions)\n- `ImagePrefetcher` now always sets the priority of the requests to its priority\n- `ImagePrefetcher` now works with `ImageRequestConvertible`, adding support for `URLRequest` and `String`\n- `ImagePipeline` can now be invalidated with `invalidate()` method\n\n### Deprecations\n\nThere are deprecation warnings in place to help guide you through the migration process.\n\n- Deprecate `ImageRequestOptions`, use `ImageRequest.Options` instead (it's not just the name change)\n- Deprecate `ImagePipelineObserving`, use `imageTask(_:,didReceiveEvent)` from `ImagePipeline.Delegate` instead\n- Rename `isDeduplicationEnabled` to `isTaskCoalescingEnabled`\n- Deprecate `animatedImageData` associated object for platform images. Use `data` property of `ImageContainer` instead. `animatedImageData` was initially soft-deprecated in Nuke 9.0.\n- Deprecate the default `processors` in `ImagePipeline`; use the new `processors` options in `ImageLoadingOptions` instead\n- Deprecate `ImageEncoder` and `ImageDecoder` typealiases.\n\n\n# Nuke 9\n\n## Nuke 9.6.1\n\n*May 24, 2021*\n\n- Remove some risky `DataLoader` optimizations\n\n## Nuke 9.6.0\n\n*May 2, 2021*\n\n- Add `ImageRequest.CachePolicy.returnCacheDataDontLoad`, [#456](https://github.com/kean/Nuke/pull/456)\n- Add `ImagePublisher` (Combine extensions)\n- Add a convenience `dataLoadingError` property to `ImagePipeline.Error`\n- Remove APIs deprecated in versions 9.0-9.1\n- Add a note on [`waitsForConnectivity`](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/2908812-waitsforconnectivity) in [Nuke Docs](https://kean.blog/nuke/guides/performance#auto-retry)\n- Add [\"Low Data Mode\"](https://kean.blog/nuke/guides/combine#low-data-mode) in Nuke Docs\n\n## Nuke 9.5.1\n\n*Apr 28, 2021*\n\n- Update to Xcode 12.5. Fixes [#454](https://github.com/kean/Nuke/issues/454). \n\n## Nuke 9.5.0\n\n*Apr 3, 2021*\n\n- Add `priority` property to `ImagePrefetcher` which changes the priority of both new and outstanding tasks. By default, `.low`. Use-case: reducing the priority to `.veryLow` when moving to a new screen.\n- Further `ImagePrefetcher` performance improvements: one less allocation per request, `ImageRequest` instances are now created in background, reduce closure capture lists, optimize cancellation\n- `ImagePrefetcher` now automatically sets the proper request priority even when you start prefetching with`ImageRequest`\n\n## Nuke 9.4.1\n\n*Mar 27, 2021*\n\n- Shorter names for parameters in `loadImage()` and `loadData` methods to improve ImagePipeline APIs ergonomics\n- Rename `ImagePreheater` to `ImagePrefetcher` (via deprecation) \n- Rewrite `ImagePrefetcher` documentation\n\n## Nuke 9.4.0\n\n*Mar 26, 2021*\n\n- Reduce the number of context switches in `ImagePrefetcher` and `DataLoader`\n- Atomics are back, improves direct `ImagePipeline` usage performance\n- Fast-track default decoding operations\n- Reduce the number of allocations per task\n- Deprecate typealiases for progress and completion closures to improve auto-completion\n- You can now toggle `ImagePipeline.Configuration.isSignpostLoggingEnabled` while the app is running and without re-creating the pipeline, [#443](https://github.com/kean/Nuke/issues/443)\n- Add convenience `subscript` that takes `URL` to `ImageCaching` protocol as extension\n\n## Nuke 9.3.1\n\n*Mar 21, 2021*\n\n- Fix `DataCache` trim ratio, previously was applying size limit too aggressively.\n- Deprecate `DataCache.countLimit`. The default limit is now `Int.max`.\n- Move demo project to a [separate repo](https://github.com/kean/NukeDemo). Fixes [#442](https://github.com/kean/Nuke/issues/442).\n\n## Nuke 9.3.0\n\n*Feb 22, 2021*\n\n- Improve ImagePipeline background performance by **~40%** (measuring after taking out system calls)\n- Reduce number of allocations per task\n- Improve Task infrastructure, make ImagePipeline vastly easier to read and understand\n- Add more performance and unit tests. Tests are now clocking at 6000 lines of code.\n- Add infrastructure for automated memory management testing\n\n## Nuke 9.2.4\n\n*Jan 16, 2021*\n\n- Add support for image orientation in `ImageProcessors.Resize` - [#429](https://github.com/kean/Nuke/pull/429)\n\n## Nuke 9.2.3\n\n*Dec 30, 2020*\n\n- Fix regression introduced in Nuke 9.2.1 where some image processors would not render transparent background correctly - [#424](https://github.com/kean/Nuke/issues/424)\n\n## Nuke 9.2.2\n\n*Dec 26, 2020*\n\n- Deprecate `crop` parameter in `ImageProcessors.Resize` `init(height:)` and `init(width:)` initializers (crop doesn't make sense in with these parameters)\n\n## Nuke 9.2.1\n\n*Dec 15, 2020*\n\n- Fix `CGBitmapContextCreate: unsupported parameter combination` warnings - [#416](https://github.com/kean/Nuke/issues/416)\n\n## Nuke 9.2.0\n\n*Nov 28, 2020*\n\n### Additions\n\n- Add an option to remove an image from all cache layers `pipeline.removeCachedImage(for:)`\n- Add `ImageRequest.CachePolicy` to `ImageRequest`. Use `.reloadIgnoringCachedData` to reload the image ignoring all cached data - [#411](https://github.com/kean/Nuke/pull/411)\n- Add support for extended color spaces - [#408](https://github.com/kean/Nuke/pull/408)\n- Add `ImageProcessors.Circle` and `ImageProcessors.RoundedCorners` on macOS - [#410](https://github.com/kean/Nuke/pull/410)\n- Add `ImageProcessors.CoreImage` and `ImageProcessors.GaussianBlur` on macOS - [#413](https://github.com/kean/Nuke/pull/413)\n- Add  `ImageType.webp`. WebP is natively supported by the latest Apple platforms - [#412](https://github.com/kean/Nuke/pull/412)\n\n### Improvements\n\n- Introduce `ImageRequestConvertible` protocol to narrow the number of public APIs. For example, if you type `ImagePipeline.shared.loadImage...`, it's now going to suggest twice fewer options.\n- Remove `Image` typealias deprecated in Nuke 8.4\n- Remove public `CGSize: Hashable` conformance - [#410](https://github.com/kean/Nuke/pull/410)\n- Decompression and resizing now preserve image color space and other parameters. For example, grayscale images with 8 bits per component stay images with 8 bits per component.\n- Switch from Travis to GitHub Actions - [#409](https://github.com/kean/Nuke/pull/409)\n- Fix \"Backward matching of the unlabeled trailing closure is deprecated\"  warnings\n\n## Nuke 9.1.3\n\n*Nov 17, 2020*\n\n- Fix an issue where HTTP range for resumable requests would sometimes be sent incorrectly - [#389](https://github.com/kean/Nuke/issues/389)\n- Fix compile time warnings in Xcode 12\n\n## Nuke 9.1.2\n\n*Aug 25, 2020*\n\n- Fix an issue with `ImageCache` memory pressure monitoring where it was clearing it when memory pressure changes to `normal` level - [#392](https://github.com/kean/Nuke/pull/392) by [Eric Jensen](https://github.com/ejensen)\n\n## Nuke 9.1.1\n\n*June 19, 2020*\n\n### Fixes\n\n- Fix how `RateLimiter` clamps the delay – [#374](https://github.com/kean/Nuke/pull/374) by [Tangent](https://github.com/TangentW)\n- Fix an issue where `ImageTask` would stay in memory indefinitely in certain situations - [#377](https://github.com/kean/Nuke/pull/377) by [Ken Bongort](https://github.com/ken-broadsheet)\n- Fix an issue in a demo project where \"Rate Limiter\" demo would use incorrect cell size on first draw \n\n## Nuke 9.1\n\n*June 1, 2020*\n\n### Enhancements\n\n- `ImageCache` now uses `DispatchSourceMemoryPressure` instead `UIApplication.didReceiveMemoryWarningNotification` to improve watchOS support - [#370](https://github.com/kean/Nuke/pull/370), by [Dennis Oberhoff](https://github.com/docterd)\n- Add `tintColor` option to `ImageLoadingOptions` - [#371](https://github.com/kean/Nuke/pull/371) by [Basem Emara](https://github.com/basememara)\n- Minor documentation fixes and improvements\n\n## Nuke 9.0\n\n*May 20, 2020*\n\n**Nuke 9** is the best release so far with refinements across the entire framework and some exciting new additions.\n\n> **SwiftUI** · **Combine** · **Task builder API** · **New advanced set of core protocols for power-users** · **HEIF** · **Transcoding images in disk cache** · **Progressive decoding performance improvements** · **Improved resizing APIs** · **Automatic registering of decoders** · **SVG** · **And More**\n\nMost of the Nuke APIs are source compatible with Nuke 8. There is also a [Nuke 9 Migration Guide](https://github.com/kean/Nuke/blob/9.0.0/Documentation/Migrations/Nuke%209%20Migration%20Guide.md) to help with migration.\n\n### Overview\n\nThe primary focus of this release was to build on top the infrastructure introduced in Nuke 8 to deliver more **advanced features** while keeping the easy things easy. To achieve this, in Nuke 9, all core protocols, like `ImageProcessing`, `ImageEncoding`, `ImageDecoding`, now have a  basic subset of methods that you _must_ implement, and then there are new _advanced_ methods which are optional and give you full control over the pipeline.\n\nAlong with Nuke 9, **three new amazing Swift packages** were introduced:\n\n- [**FetchImage**](https://github.com/kean/FetchImage) which makes it easy to use Nuke with SwiftUI\n- [**ImagePublisher**](https://github.com/kean/ImagePublisher) with Combine publishers for Nuke\n- And finally [**ImageTaskBuilder**](https://github.com/kean/ImageTaskBuilder) which introduces a new fun and convenient way to use Nuke. I really love this package. Just look at these APIs:\n\n```swift\nImagePipeline.shared.image(with: URL(string: \"https://\")!)\n    .resize(width: 320)\n    .blur(radius: 10)\n    .priority(.high)\n    .load { result in\n        print(result)\n    }\n```\n\nI would also like to highlight a few other changes to **improve documentation**.\n\nFirst, there is a completely new [**API Reference**](https://kean-org.github.io/docs/nuke/reference/9.0.0/) available generated using [SwiftDoc](https://github.com/SwiftDocOrg/swift-doc), a new package for generating documentation for Swift projects.\n\nThere is a completely new [**README**](https://github.com/kean/Nuke/tree/9.0.0) and two new guides:\n\n- [**Image Pipeline Guide**](https://github.com/kean/Nuke/blob/9.0.0/Documentation/Guides/image-pipeline.md) with a detailed description of how the pipeline delivers images\n- [**Image Formats Guide**](https://github.com/kean/Nuke/blob/9.0.0/Documentation/Guides/image-formats.md) with an overview of the improved decoding/encoding infrastructure and information how to support variety of image formats: GIF, HEIF, SVG, WeP, and more.\n\nThere is also a new [**Troubleshooting Guide**](https://github.com/kean/Nuke/blob/9.0.0/Documentation/Guides/troubleshooting.md).\n\nAnother small but delightful change the demo project which can now be run by simply clicking on the project and running it, all thanks to Swift Package Manager magic.\n\n### Changelog\n\n#### General Improvements\n\n- Bump minimum platform version requirements. The minimum iOS version is now iOS 11 which is a 64-bit only system. This is great news if you are installing your dependencies using Carthage as Nuke is now going to compile twice as fast: no need to compile for `i386` and `armv7` anymore.\n\n#### Documentation Improvements\n\n- Rewrite most of the README\n- Add a completely new [**API Reference**](https://kean-org.github.io/docs/nuke/reference/9.0.0/) available generated using [SwiftDoc](https://github.com/SwiftDocOrg/swift-doc), a new package for generating documentation for Swift projects\n- Add a completely new [**Image Pipeline Guide**](https://github.com/kean/Nuke/blob/9.0.0/Documentation/Guides/image-pipeline.md) which describes in detail how the pipeline works.\n- Add a new [**Image Formats Guide**](https://github.com/kean/Nuke/blob/9.0.0/Documentation/Guides/image-formats.md)\n\n#### `ImageProcessing` improvements\n\nThere are now two levels of image processing APIs. For the basic processing needs, implement the following method:\n\n```swift\nfunc process(_ image: UIImage) -> UIImage? // NSImage on macOS\n```\n\nIf your processor needs to manipulate image metadata (`ImageContainer`), or get access to more information via the context (`ImageProcessingContext`), there is now an additional method that allows you to do that:\n\n ```swift\nfunc process(_ container: ImageContainer, context: ImageProcessingContext) -> ImageContainer?\n```\n\n- All image processors are now available `ImageProcessors` namespace so it is now easier to find the ones you are looking for. Unrelated types were moved to `ImageProcessingOption`.\n- Add `ImageResponse` to `ImageProcessingContext`\n- New convenience `ImageProcessors.Resize.init(width:)` and `ImageProcessors.Resize.init(height:)` initializers\n\n#### `ImageDecoding` Improvements\n\n- Add a new way to register the decoders in `ImageDecoderRegistry` with `ImageDecoderRegistering` protocol. `public func register<Decoder: ImageDecoderRegistering>(_ decoder: Decoder.Type)` - [#354](https://github.com/kean/Nuke/pull/354)\n\n```swift\n/// An image decoder which supports automatically registering in the decoder register.\npublic protocol ImageDecoderRegistering: ImageDecoding {\n    init?(data: Data, context: ImageDecodingContext)\n    // Optional\n    init?(partiallyDownloadedData data: Data, context: ImageDecodingContext)\n}\n```\n\n- The default decoder now implements `ImageDecoderRegistering` protocol\n- Update the way decoders are created. Now if the decoder registry can't create a decoder for the partially downloaded data, the pipeline will no longer create (failing) decoding operation reducing the pressure on the decoding queue\n- Rework `ImageDecoding` protocol\n- Nuke now supports decompression and processing of images that require image data to work\n- Deprecate `ImageResponse.scanNumber`, the scan number is now passed in `ImageContainer.userInfo[ImageDecodert.scanNumberKey]` (this is a format-specific feature and that's why I made it non-type safe and somewhat hidden). Previously, it was also only working for the default `ImageDecoders.Default`. Now any decoder can pass scan number, or any other information using `ImageContainer.userInfo`\n- All decoders are now defined in `ImageDecoders` namespace\n- Add `ImageDecoders.Empty`\n- Add `ImageType` struct \n\n#### `ImageEncoding` Improvements\n\n[#353](https://github.com/kean/Nuke/pull/353) - There are now two levels of image encoding APIs. For the basic encoding needs, implement the following method:\n\n```swift\nfunc encode(_ image: UIImage) -> UIImage? // NSImage on macOS\n```\n\nIf your encoders needs to manipulate image metadata (`ImageContainer`), or get access to more information via the context (`ImageEncodingContext`), there is now an additional method that allows you to do that:\n\n ```swift\nfunc encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data?\n```\n\n- All image encoders are now available `ImageEncoders` namespace so it is now easier to find the ones you are looking for.\n- Add `ImageEncoders.ImageIO` with HEIF support - [#344](https://github.com/kean/Nuke/pull/344)\n- The default adaptive encoder now uses `ImageEncoders.ImageIO` under the hood and can be configured to support HEIF\n\n#### Progressive Decoding Improvements\n\n- You can now opt-in to store progressively generated previews in the memory cache by setting the pipeline option `isStoringPreviewsInMemoryCache` to `true`. All of the previews have `isPreview` flag set to `true`. - [$352](https://github.com/kean/Nuke/pull/352)\n\n#### Improved Cache For Processed Images - [#345](https://github.com/kean/Nuke/pull/345)\n\nNuke 9 revisits data cache for processed images feature introduced in [Nuke 8.0](https://github.com/kean/Nuke/releases/tag/8.0) and fixes all the rough edges around it.\n\nThere are two primary changes.\n\n#### 1. Deprecate `isDataCachingForOriginalImageDataEnabled` and `isDataCachingForProcessedImagesEnabled` properties.\n\nThese properties were replaced with a new `DataCacheOptions`.\n\n```swift\npublic struct DataCacheOptions {\n    /// Specifies which content to store in the `dataCache`. By default, the\n    /// pipeline only stores the original image data downloaded using `dataLoader`.\n    /// It can be configured to encode and store processed images instead.\n    ///\n    /// - note: If you are creating multiple versions of the same image using\n    /// different processors, it might be worse enabling both `.originalData`\n    /// and `.encodedImages` cache to reuse the same downloaded data.\n    ///\n    /// - note: It might be worth enabling `.encodedImages` if you want to\n    /// transcode downloaded images into a more efficient format, like HEIF.\n    public var storedItems: Set<DataCacheItem> = [.originalImageData]\n}\n\npublic enum DataCacheItem {\n    /// Original image data.\n    case originalImageData\n    /// Final image with all processors applied.\n    case finalImage\n}\n```\n\nNow we no longer rely on documentation to make sure that you disable data cache for original image data when you decide to cache processed images instead.\n\n#### 2. Rework `DataCacheItem.finalImage` behavior.\n\nThe primary reason for deprecation is a significantly changed behavior of data cache for processed images.\n\nThe initial version introduced back in Nuke 8.0 never really made sense. For example, only images for requests with processors were stored, but not the ones without. You can see how this could be a problem, especially if you disable data cache for original image data which was a recommended option.\n\nThe new behavior is much simpler. You set `configuration.dataCacheOptions.storedItems` to `[. finalImage]`, and Nuke encodes and stores all of the downloaded images, regardless of whether they were processed or not.\n\n#### `DataCache` Improvements - [#350](https://github.com/kean/Nuke/pull/350)\n\nNuke 9 realized the original vision for `DataCache`. The updated staging/flushing mechanism now performs flushes on certain intervals instead of on every write. This makes some of the new `DataCache` features possible.\n\n- `flush` not performs synchronously\n- Add `flush(for:)` methods which allows to flush changes on disk only for the given key\n- Add public property `let queue: DispatchQueue`\n- Add public method `func url(for key: Key) -> URL?`\n\n#### `ImageContainer`\n\nThis release introduces `ImageContainer` type. It is integrated throughout the framework instead of `PlatformImage`.\n\n**Reasoning**\n\n- Separate responsibility. `ImageResponse` - result of the current request with information about the current request, e.g. `URLResponse` that was received. `ImageContainer` - the actual downloaded and processed image regardless of the request\n- Stop relying on Objective-C runtime which `animatedImageData` was using\n- Stop relying on extending Objective-C classes like `UIImage`\n- Add type-safe way to attach additional information to downloaded images\n\n**Changes**\n\n- Update `ImageCaching` protocol to store `ImageContainer` instead of `ImageResponse`. `ImageResponse` is a result of the individual request, it should not be saved in caches.\n\n```swift\npublic protocol ImageCaching: AnyObject {\n    subscript(request: ImageRequest) -> ImageContainer?\n}\n```\n\n- Update `ImagePipeline.cachedImage(for:)` method to return `ImageContainer`\n- Deprecate `PlatformImage.animatedImageData`, please use `ImageContainer.data` instead\n- Deprecated `ImagePipelineConfiguration.isAnimatedImageDataEnabled`, the default `ImageDecoder` now set `ImageContainer.data` automatically when it recognizes GIF format\n\n#### Other\n\n- `ImagePreheater` now automatically cancels all of the outstanding tasks on deinit - [#349](https://github.com/kean/Nuke/pull/349)\n- `ImagePipeline` now has `func cacheKey(for request: ImageRequest, item: DataCacheItem) -> String` method which return a key for disk cache\n- Change the type of `ImageRequest.userInfo` from `Any?` to `[AnyHashable: Any]`\n- Remove `DFCache` from demo - [#347](https://github.com/kean/Nuke/pull/347)\n- Remove `FLAnimatedImage` and Carthage usage from demo - [#348](https://github.com/kean/Nuke/pull/348)\n- Migrate to Swift 5.1 - [#351](https://github.com/kean/Nuke/pull/351)\n- Add `ImageType.init(data:)`\n- Add `ImageLoadingOptions.isProgressiveRenderingEnabled`\n- Add public `ImageContainer.map`\n- Add \"Rendering Engines\" section in image-formats.md\n- `ImageDecoder` now attaches `ImageType` to the image\n- `ImageProcessingOptions.Border` now accepts unit as a parameter\n\n### Fixes\n\n- Fix how `ImageProcesors.Resize` compares size when different units are used\n- Fix an issue with `ImageProcessors.Resize` String identifier being equal with different content modes provided\n- Fix TSan warnings - [#365](https://github.com/kean/Nuke/pull/365), by [Luciano Almeida](https://github.com/LucianoPAlmeida)\n\n\n# Nuke 8\n\n## Nuke 8.4.1\n\n*March 19, 2020*\n\n- Podspec now explicitly specifies supported Swift versions - [340](https://github.com/kean/Nuke/pull/340), [Richard Lee](https://github.com/dlackty)\n- Fix a memory leak when the URLSession wasn't deallocated correctly - [336](https://github.com/kean/Nuke/issues/336)\n\n### Announcements\n\nThere are two new Swift packages available in Nuke ecosystem:\n\n- [**FetchImage**](https://github.com/kean/FetchImage) that makes it easy to download images using Nuke and display them in SwiftUI apps. One of the notable features of `FetchImage` is support for iOS 13 Low Data mode.\n- [**ImagePublisher**](https://github.com/kean/ImagePublisher) that provides [Combine](https://developer.apple.com/documentation/combine) publishers for some of the Nuke APIs.\n\nBoth are distributed exclusively via [Swift Package Manager](https://swift.org/package-manager/). And both are API _previews_. Please, try them out, and feel free to [contact me](https://twitter.com/a_grebenyuk) with any feedback that you have. \n\n\n## Nuke 8.4.0\n\n*November 17, 2019*\n\n- Fix an issue with `RoundedCorners` image processor not respecting the `Border` parameter – [327](https://github.com/kean/Nuke/pull/327), [Eric Jensen](https://github.com/ejensen)\n- Add an optional `border` parameter to the `Circle` processor – [327](https://github.com/kean/Nuke/pull/327), [Eric Jensen](https://github.com/ejensen)\n- Add `ImagePipelineObserving` and `DataLoaderObserving` protocols to allow users to tap into the internal events of the subsystems to enable logging and other features – [322](https://github.com/kean/Nuke/pull/322)\n- Deprecate `Nuke.Image` to avoid name clashes with `SwiftUI.Image` in the future , add `PlatformImage` instead – [321](https://github.com/kean/Nuke/pull/321) \n- Make `ImagePipeline` more readable – [320](https://github.com/kean/Nuke/pull/320)\n- Update demo project to use Swift Package Manager instead of CocoaPods – [319](https://github.com/kean/Nuke/pull/319)\n\n## Nuke 8.3.1\n\n*October 26, 2019*\n\n- Add dark mode support to the demo project – [#307](https://github.com/kean/Nuke/pull/307), [Li Yu](https://github.com/yurited)\n\n## Nuke 8.3.0\n\n*October 06, 2019*\n \n - Add `processors` option to `ImagePipeline.Configuration`  – [300](https://github.com/kean/Nuke/pull/300), [Alessandro Vendruscolo](https://github.com/vendruscolo)\n - Add `queue` option to `loadImage` and `loadData` methods of `ImagePipeline` – [304](https://github.com/kean/Nuke/pull/304)\n - Add `callbackQueue` option to `ImagePipeline.Configuration` – [304](https://github.com/kean/Nuke/pull/304)\n\n\n## Nuke 8.2.0\n\n*September 20, 2019*\n\n- Add support for Mac Catalyst – [#299](https://github.com/kean/Nuke/pull/299), [Jonathan Downing](https://github.com/JonathanDowning)\n\n\n## Nuke 8.1.1\n\n*September 1, 2019*\n\n- Switch to a versioning scheme which is compatible with Swift Package Manager\n\n\n## Nuke 8.1\n\n*August 25, 2019*\n\n- Configure dispatch queues with proper QoS – [#291](https://github.com/kean/Nuke/pull/291), [Michael Nisi](https://github.com/michaelnisi)\n- Remove synchronization points in `ImageDecoder` which is not needed starting from iOS 10 – [#277](https://github.com/kean/Nuke/pull/277)\n- Add Swift Package Manager to Installation Guides\n- Improve Travis CI setup: run tests on multiple Xcode versions, run thread safety tests, run SwiftLint validations, build demo project, validate Swift package – [#279](https://github.com/kean/Nuke/pull/279), [#280](https://github.com/kean/Nuke/pull/280), [#281](https://github.com/kean/Nuke/pull/281), [#284](https://github.com/kean/Nuke/pull/284), [#285](https://github.com/kean/Nuke/pull/285)\n\n\n## Nuke 8.0.1\n\n*July 21, 2019*\n\n- Remove synchronization in `ImageDecoder` which is no longer needed – [#277](https://github.com/kean/Nuke/issues/277)\n\n\n## Nuke 8.0\n\n*July 8, 2019*\n\nNuke 8 is the most powerful, performant, and refined release yet. It contains major advancements it some areas and brings some great new features. One of the highlights of this release is the documentation which was rewritten from the ground up.\n\n> **Cache processed images on disk** · **New built-in image processors** · **ImagePipeline v2** · **Up to 30% faster main thread performance** · **`Result` type** · **Improved deduplication** · **`os_signpost` integration** · **Refined ImageRequest API** · **Smart decompression** · **Entirely new documentation**\n\nMost of the Nuke APIs are source compatible with Nuke 7. There is also a [Nuke 8 Migration Guide](https://github.com/kean/Nuke/blob/8.0/Documentation/Migrations/Nuke%208%20Migration%20Guide.md) to help with migration.\n\n### Image Processing\n\n#### [#227 Cache Processed Images on Disk](https://github.com/kean/Nuke/pull/227)\n\n`ImagePipeline` now supports caching of processed images on disk. To enable this feature set `isDataCacheForProcessedDataEnabled` to `true` in the pipeline configuration and provide a `dataCache`. You can use a built-in `DataCache` introduced in [Nuke 7.3](https://github.com/kean/Nuke/releases/tag/7.3) or write a custom one.\n\nImage cache can significantly improve the user experience in the apps that use heavy image processors like Gaussian Blur.\n\n#### [#243 New Image Processors](https://github.com/kean/Nuke/pull/243)\n\nNuke now ships with a bunch of built-in image processors including:\n\n-  `ImageProcessor.Resize`\n-  `ImageProcessor.RoundedCorners`\n-  `ImageProcessor.Circle`\n-  `ImageProcessor.GaussianBlur`\n-  `ImageProcessor.CoreImageFilter`\n\nThere are also `ImageProcessor.Anonymous` to create one-off processors from closures and `ImageProcessor.Composition` to combine two or more processors.\n\n#### [#245 Simplified Processing API](https://github.com/kean/Nuke/pull/245)\n\nPreviously Nuke offered multiple different ways to add processors to the request. Now there is only one, which is also better than all of the previous versions:\n\n```swift\nlet request = ImageRequest(\n    url: URL(string: \"http://...\"),\n    processors: [\n        ImageProcessor.Resize(size: CGSize(width: 44, height: 44), crop: true),\n        ImageProcessor.RoundedCorners(radius: 16)\n    ]\n)\n```\n\n> Processors can also be set using a respective mutable `processors` property.\n\n> Notice that `AnyImageProcessor` is gone! You can simply use `ImageProcessing` protocol directly in places where previously you had to use a type-erased version.\n\n\n#### [#229 Smart Decompression](https://github.com/kean/Nuke/pull/229)\n\nIn the previous versions, decompression was part of the processing API and `ImageDecompressor` was the default processor set for each image request. This was mostly done to simplify implementation but it was confusing for the users.\n\nIn the new version, decompression runs automatically and it no longer a \"processor\". The new decompression is also _smarter_. It runs only when needed – when we know that image is still in a compressed format and wasn't decompressed by one of the image processors.\n\nDecompression runs on a new separate `imageDecompressingQueue`. To disable decompression you can set a new `isDecompressionEnabled` pipeline configuration option to `false`.\n\n#### [#247 Avoiding Duplicated Work when Applying Processors](https://github.com/kean/Nuke/pull/247)\n\nThe pipeline avoids doing any duplicated work when loading images. Now it also avoids applying the same processors more than once. For example, let's take these two requests:\n\n```swift\nlet url = URL(string: \"http://example.com/image\")\npipeline.loadImage(with: ImageRequest(url: url, processors: [\n    ImageProcessor.Resize(size: CGSize(width: 44, height: 44)),\n    ImageProcessor.GaussianBlur(radius: 8)\n]))\npipeline.loadImage(with: ImageRequest(url: url, processors: [\n    ImageProcessor.Resize(size: CGSize(width: 44, height: 44))\n]))\n```\n\nNuke will load the image data only once, resize the image once and apply the blur also only once. There is no duplicated work done at any stage. If any of the intermediate results are available in the data cache, they will be used.\n\n### ImagePipeline v2\n\nNuke 8 introduced a [major new iteration](https://github.com/kean/Nuke/pull/235) of the `ImagePipeline` class. The class was introduced in Nuke 7 and it contained a lot of incidental complexity due to addition of progressive decoding and some other new features. In Nuke 8 it was rewritten to fully embrace progressive decoding. The new pipeline is smaller, simpler, easier to maintain, and more reliable.\n\nIt is also faster.\n\n#### +30% Main Thread Performance\n\nThe image pipeline spends even less time on the main thread than any of the previous versions. It's up to 30% faster than Nuke 7.\n\n#### [#239 Load Image Data](https://github.com/kean/Nuke/pull/239)\n\nAdd a new `ImagePipeline` method to fetch original image data:\n\n```swift\n@discardableResult\npublic func loadData(with request: ImageRequest,\n                     progress: ((_ completed: Int64, _ total: Int64) -> Void)? = nil,\n                     completion: @escaping (Result<(data: Data, response: URLResponse?), ImagePipeline.Error>) -> Void) -> ImageTask\n```\n\nThis method now powers `ImagePreheater` with destination `.diskCache` introduced in [Nuke 7.4](https://github.com/kean/Nuke/releases/tag/7.4) (previously it was powered by a hacky internal API).\n\n#### [#245 Improved ImageRequest API](https://github.com/kean/Nuke/pull/245)\n\nThe rarely used options were extracted into the new `ImageRequestOptions` struct and the request initializer can now be used to customize _all_ of the request parameters.\n\n#### [#255 `filteredURL`](https://github.com/kean/Nuke/pull/255)\n\nYou can now provide a `filteredURL` to be used as a key for caching in case the URL contains transient query parameters:\n\n```swift\nlet request = ImageRequest(\n    url: URL(string: \"http://example.com/image.jpeg?token=123\")!,\n    options: ImageRequestOptions(\n        filteredURL: \"http://example.com/image.jpeg\"\n    )\n)\n```\n\n#### [#241 Adopt `Result` type](https://github.com/kean/Nuke/pull/241)\n\nAdopt the `Result` type introduced in Swift 5. So instead of having a separate `response` and `error` parameters, the completion closure now has only one parameter - `result`.\n\n```swift\npublic typealias Completion = (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void\n```\n\n### Performance\n\nApart from the general performance improvements Nuke now also offers a great way to measure performance and gain visibility into how the system behaves when loading images.\n\n#### [#250 Integrate `os_signpost`](https://github.com/kean/Nuke/pull/250)\n\nIntegrate [os_signpost](https://developer.apple.com/documentation/os/logging) logs for measuring performance. To enable the logs set `ImagePipeline.Configuration.isSignpostLoggingEnabled` (static property) to `true` before accessing the `shared` pipeline.\n\nWith these logs, you have visibility into the image pipeline. For more information see [WWDC 2018: Measuring Performance Using Logging](https://developer.apple.com/videos/play/wwdc2018/405/) which explains `os_signpost` in a great detail.\n\n<img width=\"1375\" alt=\"Screenshot 2019-06-01 at 10 46 52\" src=\"https://user-images.githubusercontent.com/1567433/58753519-8adf7b80-84c0-11e9-806a-eac24ddaa2dd.png\">\n\n### Documentation\n\nAll the documentation for Nuke was rewritten from scratch in Nuke 8. It's now more concise, clear, and it even features some fantastic illustrations:\n\n<img width=\"1158\" alt=\"Screenshot 2019-06-11 at 22 31 18\" src=\"https://user-images.githubusercontent.com/1567433/59304491-aacd2700-8c98-11e9-9630-293d27545b1a.png\">\n\nThe screenshots come the the **reworked demo** project. It gained new demos including *Image Processing* demo and also a way to change `ImagePipeline` configuration in runtime.\n\n### Misc\n\n- Add a cleaner way to set `ImageTask` priority using a new `priority` property – [#251](https://github.com/kean/Nuke/pull/251)\n- [macOS] Implement image cost calculation for `ImageCache` – [#236](https://github.com/kean/Nuke/issues/236)\n- [watchOS] Add `WKInterfaceImage` support\n- Future-proof Objective-C `ImageDisplaying` protocol by adding `nuke_` prefixes to avoid clashes in Objective-C runtime\n- Add convenience `func decode(data: Data) -> Image?` method with a default `isFinal` argument to `ImageDecoding` protocol – [e3ca5e](https://github.com/kean/Nuke/commit/e3ca5e646ddc1939d05a121de20cf88e2c8220cc)\n- Add convenience `func process(image: Image) -> Image?` method to `ImageProcessing` protocol\n- `DataCache` will now automatically re-create its root directory if it was deleted underneath it\n- Add public `flush` method to `DataCache` \n\n\n# Nuke 7\n\n## Nuke 7.6.3\n\n*May 1, 2019*\n\n- Fix [#226](https://github.com/kean/Nuke/issues/226) `ImageTask.setPriority(_:)` sometimes crashes\n\n\n## Nuke 7.6.2\n\n*Apr 24, 2019*\n\n- Fix [Thread Sanitizer warnings](https://github.com/kean/Nuke/issues/224). The issue was related to `unfair_lock` usage which was introduced as a replacement for `OSAtomic` functions in Nuke 7.6. In order to fix the issue, `unfair_lock` was replaced with simple `NSLock`. The performance hit is pretty insignificant and definitely isn't worth introducing this additional level of complexity.\n\n## Nuke 7.6.1\n\n*Apr 13, 2019*\n\n- Fix SwiftPM 5.0 support by adding explicit platform version requirements  – [Vadim Shpakovski](https://github.com/shpakovski) in [#220](https://github.com/kean/Nuke/pull/220)\n- Update [Nuke 7 Migration Guide](https://github.com/kean/Nuke/blob/7.6.1/Documentation/Migrations/Nuke%207%20Migration%20Guide.md)\n\n\n## Nuke 7.6\n\n*Apr 7, 2019*\n\n- Add Swift 5.0 support – [Daniel Storm](https://github.com/DanielStormApps) in [#217](https://github.com/kean/Nuke/pull/217)\n- Add SwiftPM 5.0 support – [Vadim Shpakovski](https://github.com/shpakovski) in [#219](https://github.com/kean/Nuke/pull/219)\n- Remove Swift 4.0 and Swift 4.1 support\n- Remove iOS 9, tvOS 9, watchOS 2.0, macOS 10.10 and macOS 10.11 support\n- Add a single `Nuke` target which can build the framework for any platform\n- Replace deprecated `OSAtomic` functions with `unfair_lock`, there are no performance regressions\n\n\n## Nuke 7.5.2\n\n*Dec 26, 2018*\n\n- [macOS] Fix `Nuke.loadImage` image is not displayed when `.fadeIn` transition is used – [#206](https://github.com/kean/Nuke/issues/206)\n- Add `.alwaysTransition` flag to `ImageLoadingOptions` – [@gabzsa](https://github.com/gabzsa) in [#201](https://github.com/kean/Nuke/pull/201)\n\n\n## Nuke 7.5.1\n\n*Nov 8, 2018*\n\n- Update Swift version in pbxproj to Swift 4.2, [#199](https://github.com/kean/Nuke/issues/199)\n- Update demo to Swift 4.2\n\n\n## Nuke 7.5\n\n*Oct 21, 2018*\n\n### Additions\n\n- [#193](https://github.com/kean/Nuke/pull/193) Add an option to `ImageDecompressor` to allow images to upscale, thanks to [@drkibitz](https://github.com/drkibitz)\n- [#197](https://github.com/kean/Nuke/pull/197) Add a convenience initializer to `ImageRequest` which takes an image processor (`ImageProcessing`) as a parameter, thanks to [@drkibitz](https://github.com/drkibitz)\n\n### Improvements\n\n- Add a guarantee that if you cancel `ImageTask` on the main thread, you won't receive any more callbacks (progress, completion)\n- Improve internal `Operation` performance, images are loading up to 5% faster\n\n### Removals\n\nNuke 7 had a lot of API changes, to make the migration easier it shipped with Deprecated.swift file (536 line of code) which enabled Nuke 7 to be almost 100% source-compatible with Nuke 6. It's been 6 months since Nuke 7 release, so now it's finally a good time to remove all of this code. \n\n\n## Nuke 7.4.2\n\n*Oct 1, 2018*\n\n- #174 Fix an issue with an `ImageView` reuse logic where in rare cases a wrong image would be displayed, thanks to @michaelnisi\n\n\n## Nuke 7.4.1\n\n*Sep 25, 2018*\n\n- Disable automatic `stopPreheating` which was causing some issues\n\n\n## Nuke 7.4\n\n*Sep 22, 2018*\n\n### Prefetching Improvements\n\n- Add an `ImagePreheater.Destination` option to `ImagePreheater`. The default option is `.memoryCache` which works exactly the way `ImagePreheater` used to work before. The more interesting option is `.diskCache`. The preheater with `.diskCache` destination will skip image data decoding entirely to reduce CPU and memory usage. It will still load the image data and store it in disk caches to be used later.\n- Add convenience `func startPreheating(with urls: [URL])` function which creates requests with `.low` requests for you.\n- `ImagePreheater` now automatically cancels all of the managed outstanding requests on deinit.\n- Add `UICollectionViewDataSourcePrefetching` demo on iOS 10+. Nuke still supports iOS 9 so [Preheat](https://github.com/kean/Preheat) is also still around.\n\n### Other Changes\n\n- #187 Fix an issue with progress handler reporting incorrect progress for resumed (206 Partial Content) downloads\n- Remove `enableExperimentalAggressiveDiskCaching` function from `ImagePipeline.Configuration`, please use `DataCache` directly instead\n- Update [Performance Guide](https://github.com/kean/Nuke/blob/7.4/Documentation/Guides/Performance%20Guide.md)\n\n\n## Nuke 7.3.2\n\n*Jul 29, 2018*\n\n- #178 Fix TSan warning being triggered by performance optimization in `ImageTask.cancel()` (false positive)\n- Fix an issue where a request (`ImageRequest`) with a default processor and a request with the same processor but set manually would have different cache keys \n\n## Nuke 7.3.1\n\n*Jul 20, 2018*\n\n- `ImagePipeline` now updates the priority of shared operations when the registered tasks get canceled (was previously only reacting to added tasks)\n- Fix an issue where `didFinishCollectingMetrics` closure wasn't called for the tasks completed with images found in memory cache and the tasks canceled before they got a chance to run. Now _every_ created tasks gets a corresponding `didFinishCollectingMetrics` call.\n\n\n## Nuke 7.3\n\n*Jun 29, 2018*\n\nThis release introduces new `DataCache` type and features some other improvements in custom data caching.\n\n- Add new `DataCache` type - a cache backed by a local storage with an LRU cleanup policy. This type is a reworked version of the experimental data cache which was added in Nuke 7.0. It's now much simpler and also faster. It allows for reading and writing in parallel, it has a simple consistent API, and I hope it's going to be a please to use.\n\n> **Migration note:** The storage format - which is simply a bunch of files in a directory really - is backward compatible with the previous implementation. If you'd like the new cache to continue working with the same path, please create it with \"com.github.kean.Nuke.DataCache\" name and use the same filename generator that you were using before:\n> \n>    try? DataCache(name: \"com.github.kean.Nuke.DataCache\", filenameGenerator: filenameGenerator)\n\n- #160 `DataCache` now has a default `FilenameGenerator` on Swift 4.2 which uses `SHA1` hash function provided by `CommonCrypto` (`CommonCrypto` is not available on the previous version of Swift).\n\n- #171 Fix a regression introduced in version 7.1. where experimental `DataCache` would not perform LRU data sweeps.\n\n- Update `DataCaching` protocol. To store data you now need to implement a synchronous method `func cachedData(for key: String) -> Data?`. This change was necessary to make the data cache fit nicely in `ImagePipeline` infrastructure where each stage is managed by a separate `OperationQueue` and each operation respects the priority of the image requests associated with it.\n\n- Add `dataCachingQueue` parameter to `ImagePipeline.Configuration`. The default `maxConcurrentOperationCount` is `2`.\n\n- Improve internal `Operation` type performance.\n\n\n## Nuke 7.2.1\n\n*Jun 18, 2018*\n\nNuke's [roadmap](https://trello.com/b/Us4rHryT/nuke) is now publicly available. Please feel free to contribute!\n\nThis update addresses tech debt introduces in version 7.1 and 7.2. All of the changes made in these version which improved deduplication are prerequisites for implementing smart prefetching which be able to skip decoding, load to data cache only, etc.\n\n### Enhancements\n\n- Simpler and more efficient model for managing decoding and processing operations (including progressive ones). All operations now take the request priority into account. The processing operations are now created per processor, not per image loading session which leads to better performance.\n- When subscribing to existing session which already started processing, pipeline will try to find existing processing operation.\n- Update `DFCache` integration demo to use new `DataCaching` protocol\n- Added [\"Default Image Pipeline\"](https://github.com/kean/Nuke#default-image-pipeline) section and [\"Image Pipeline Overview\"](https://github.com/kean/Nuke#image-pipeline-overview) sections in README.\n- Update \"Third Party Libraries\" guide to use new `DataCaching` protocol\n\n\n## Nuke 7.2\n\n*Jun 12, 2018*\n\n### Additions\n\n- #163 Add `DataCaching` protocol which can be used to implement custom data cache. It's not documented yet, the documentation going to be updated in 7.2.1.\n\n### Improvements\n\n- Initial iOS 12.0, Swift 4.2 and Xcode 10 beta 1 support\n- #167 `ImagePipeline` now uses `OperationQueue` instead of `DispatchQueue` for decoding images. The queue now respects `ImageRequest` priority. If the task is cancelled the operation added to a queue is also cancelled. The queue can be configured via `ImagePipeline.Configuration`.\n- #167 `ImagePipeline` now updates processing operations' priority.\n\n### Fixes\n\n- Fix a regression where in certain deduplication scenarios a wrong image would be saved in memory cache\n- Fix MP4 demo project\n- Improve test coverage, bring back `DataCache` (internal) tests\n\n\n## Nuke 7.1\n\n*May 27, 2018*\n\n### Improvements\n\n- Improve deduplication. Now when creating two requests (at roughly the same time) for the same images but with two different processors, the original image is going to be downloaded once (used to be twice in the previous implementation) and then two separate processors are going to be applied (if the processors are the same, the processing will be performed once).\n- Greatly improved test coverage.\n\n### Fixes\n\n- Fix an issue when setting custom `loadKey` for the request, the `hashValue` of the default key was still used.\n- Fix warnings \"Decoding failed with error code -1\" when progressively decoding images. This was the result of `ImageDecoder` trying to decode incomplete progressive scans.\n- Fix an issue where `ImageDecoder` could produce a bit more progressive scans than necessary.\n\n\n## Nuke 7.0.1\n\n*May 16, 2018*\n\n### Additions\n\n- Add a section in README about replacing GIFs with video formats (e.g. `MP4`, `WebM`)\n- Add proof of concept in the demo project that demonstrates loading, caching and displaying short `mp4` videos using Nuke\n\n### Fixes\n\n- #161 Fix the contentModes not set when initializing an ImageLoadingOptions object\n\n\n## Nuke 7.0\n\n*May 10, 2018*\n\nNuke 7 is the biggest release yet. It has a lot of  massive new features, new performance improvements, and some API refinements. Check out new [Nuke website](http://kean.blog/nuke) to see quick videos showcasing some of the new features.\n\nNuke 7 is almost completely source-compatible with Nuke 6.\n\n### Progressive JPEG & WebP\n\nAdd support for progressive JPEG (built-in) and WebP (build by the [Ryo Kosuge](https://github.com/ryokosuge/Nuke-WebP-Plugin)). See [README](https://github.com/kean/Nuke) for more info. See demo project to see it in action.\n\nAdd new `ImageProcessing` protocol which now takes an extra `ImageProcessingContext` parameter. One of its properties is `scanNumber` which allows you to do things like apply blur to progressive image scans reducing blur radius with each new scan (\"progressive blur\").\n\n### Resumable Downloads (HTTP Range Requests)\n\nIf the data task is terminated (either because of a failure or a cancellation) and the image was partially loaded, the next load will resume where it was left off. In many use cases resumable downloads are a massive improvement to user experience, especially on the mobile internet. \n\nResumable downloads require the server support for [HTTP Range Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests). Nuke supports both validators (`ETag` and `Last-Modified`).\n\nThe resumable downloads are enabled by default. The resumable data is automatically stored in efficient memory cache. This is a good default, but future versions might add more customization options.\n\n### Loading Images into Views\n\nAdd a new set of powerful methods to load images into views. Here's one of those methods:\n\n```swift\n@discardableResult\npublic func loadImage(with request: ImageRequest,\n                      options: ImageLoadingOptions = ImageLoadingOptions.shared,\n                      into view: ImageDisplayingView,\n                      progress: ImageTask.ProgressHandler? = nil,\n                      completion: ImageTask.Completion? = nil) -> ImageTask?\n```\n\nYou can now pass `progress` and `completion` closures as well as new  `ImageLoadingOptions` struct which offers a range of options:\n\n```swift\npublic struct ImageLoadingOptions {\n    public static var shared = ImageLoadingOptions()\n    public var placeholder: Image?\n    public var transition: Transition?\n    public var failureImage: Image?\n    public var failureImageTransition: Transition?\n    public var isPrepareForReuseEnabled = true\n    public var pipeline: ImagePipeline?\n    public var contentModes: ContentModes?\n\n    /// Content modes to be used for each image type (placeholder, success, failure).\n    public struct ContentModes {\n        public var success: UIViewContentMode\n        public var failure: UIViewContentMode\n        public var placeholder: UIViewContentMode\n    }\n\n    /// An animated image transition.\n    public struct Transition {\n        public static func fadeIn(duration: TimeInterval, options: UIViewAnimationOptions = [.allowUserInteraction]) -> Transition\n        public static func custom(_ closure: @escaping (ImageDisplayingView, Image) -> Void) -> Transition\n    }\n}\n```\n\n`ImageView` will now also automatically prepare itself for reuse (can be disabled via `ImageLoadingOptions`)\n\nInstead of an `ImageTarget` protocol we now have a new simple `ImageDisplaying` protocol which relaxes the requirement what can be used as an image view (it's `UIView & ImageDisplaying` now). This achieves two things:\n\n- You can now add support for more classes (e.g. `MKAnnotationView` by implementing `ImageDisplaying` protocol\n- You can override the `display(image:` method in `UIImageView` subclasses (e.g. `FLAnimatedImageView`)\n\n### Image Pipeline\n\nThe previous  `Manager` + `Loading` architecture (terrible naming, responsibilities are often confusing) was replaced with a  new unified  `ImagePipeline` class. `ImagePipeline` was built from the ground-up to support all of the powerful new features in Nuke 7 (progressive decoding, resumable downloads, performance metrics, etc).\n\nThere is also a new `ImageTask` class which feels the gap where user or pipeline needed to communicate between each other after the request was started. `ImagePipeline` and `ImageTask` offer a bunch of new features:\n\n- To cancel the request you now simply need to call `cancel()` on the task (`ImageTask`) which is a bit more user-friendly than previous `CancellationTokenSource` infrastructure.\n- `ImageTask` offers a new way to track progress (in addition to closures) - native `Foundation.Progress` (created lazily)\n- `ImageTask` can be used to dynamically change the priority of the executing tasks (e.g. the user opens a new screen, you lower the priority of outstanding tasks)\n- In `ImagePipeline.Configuration` you can now provide custom queues (`OperationQueue`) for data loading, decoding and processing (separate queue for each stage).\n- You can set a custom shared `ImagePipeline`.\n\n### Animated Images (GIFs)\n\nAdd built-in support for animated images (everything expect the actual rendering). To enable rendering you're still going to need a third-party library (see [FLAnimatedImage](https://github.com/kean/Nuke-FLAnimatedImage-Plugin) and [Gifu](https://github.com/kean/Nuke-Gifu-Plugin) plugins). The changes made in Nuke dramatically simplified those plugins making both of them essentially obsolete - they both now have 10-30 lines of code, you can just copy this code into your project.\n\n### Memory Cache Improvements\n\n- Add new `ImageCaching` protocol for memory cache which now works with a new `ImageRespone` class. You can do much more intelligent things in your cache implementations now (e.g. make decisions on when to evict image based on HTTP headers).\n- Improve cache write/hit/miss performance by 30% by getting rid of AnyHashable overhead. `ImageRequest` `cacheKey` and `loadKey` are now optional. If you use them, Nuke is going to use them instead of built-in internal `ImageRequest.CacheKey` and `ImageRequest.LoadKey`.\n- Add `TTL` support in `ImageCache`\n\n### Aggressive Disk Cache (Experimental)\n\nAdd a completely new custom LRU disk cache which can be used for fast and reliable *aggressive* data caching (ignores [HTTP cache control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)). The new cache lookups are up to 2x faster than `URLCache` lookups. You can enable it using pipeline's configuration:\n\nWhen enabling disk cache you must provide a `keyEncoder` function which takes image request's url as a parameter and produces a key which can be used as a valid filename. The [demo project uses sha1](https://gist.github.com/kean/f5e1975e01d5e0c8024bc35556665d7b) to generate those keys.\n\n```swift\n$0.enableExperimentalAggressiveDiskCaching(keyEncoder: {\n    guard let data = $0.cString(using: .utf8) else { return nil }\n    return _nuke_sha1(data, UInt32(data.count))\n})\n```\n\nThe public API for disk cache and the API for using custom disk caches is going to be available in the future versions.\n\n> Existing API already allows you to use custom disk cache by implementing `DataLoading` protocol, but this is not the most straightforward option.\n\n### Performance Metrics\n\nWhen optimizing performance, it's important to measure. Nuke collects detailed performance metrics during the execution of each image task:\n\n```swift\nImagePipeline.shared.didFinishCollectingMetrics = { task, metrics in\n    print(metrics)\n}\n```\n\n![timeline](https://user-images.githubusercontent.com/1567433/39193766-8dfd81b2-47dc-11e8-86b3-f3f69dc73d3a.png)\n\n```\n(lldb) po metrics\n\nTask Information {\n    Task ID - 1\n    Duration - 22:35:16.123 – 22:35:16.475 (0.352s)\n    Was Cancelled - false\n    Is Memory Cache Hit - false\n    Was Subscribed To Existing Session - false\n}\nSession Information {\n    Session ID - 1\n    Total Duration - 0.351s\n    Was Cancelled - false\n}\nTimeline {\n    22:35:16.124 – 22:35:16.475 (0.351s) - Total\n    ------------------------------------\n    nil – nil (nil)                      - Check Disk Cache\n    22:35:16.131 – 22:35:16.410 (0.278s) - Load Data\n    22:35:16.410 – 22:35:16.468 (0.057s) - Decode\n    22:35:16.469 – 22:35:16.474 (0.005s) - Process\n}\nResumable Data {\n    Was Resumed - nil\n    Resumable Data Count - nil\n    Server Confirmed Resume - nil\n}\n```\n\n### Other Changes\n\n- `ImagePreheater` now checks `ImageCache` synchronously before creating tasks which makes it slightly more efficient.\n- `RateLimiter` now uses the same sync queue as the `ImagePipeline` reducing a number of dispatched blocks\n- Smarter `RateLimiter` which no longer attempt to execute pending tasks when the bucket isn't full resulting in idle dispatching of blocks. I've used a CountedSet to see how well it works in practice and it's perfect. Nice small win.\n- Add `ImageDecoderRegistry` to configure decoders globally.\n- Add `ImageDecodingContext` to provide as much information as needed to select a decoder.\n- `ImageTask.Completion` now contains `ImageResponse` (image + URLResponse) instead of just plain image.\n\n### Deprecations\n\n- `CancellationToken`, `CancellationTokenSource` - continued to be used internally, If you'd like to continue using cancellation tokens please consider copying this code into your project.\n- `DataDecoding`, `DataDecoder`, `DataDecoderComposition` - replaced by a new image decoding infrastructure (`ImageDecoding`, `ImageDecoder`, `ImageDecodingRegistry` etc).\n- `Request` renamed to `ImageRequest`.\n- Deprecate `Result` type. It was only used in a single completion handler so it didn't really justify its existence. More importantly, I wasn't comfortable providing a public `Result` type as part of the framework.\n\n\n# Nuke 6\n\n## Nuke 6.1.1\n\n*Apr 9, 2018*\n\n- Lower macOS deployment target to 10.10. #156.\n- Improve README: add detailed *Image Pipeline* section, *Performance* section, rewrite *Usage* guide\n\n\n## Nuke 6.1\n\n*Feb 24, 2018*\n\n### Features\n\n- Add `Request.Priority` with 5 available options ranging from `.veryLow` to `.veryHigh`. One of the use cases of `Request.Priority` is to lower the priority of preheating requests. In case requests get deduplicated the task's priority is set to the highest priority of registered requests and gets updated when requests are added or removed from the task.\n\n### Improvements\n\n- Fix warnings on Xcode 9.3 beta 3\n- `Loader` implementation changed a bit, it is less clever now and is able to accommodate new features like request priorities\n- Minor changes in style guide to make codebase more readable\n- Switch to native `NSLock`, there doesn't seem to be any performance wins anymore when using `pthread_mutex` directly\n\n### Fixes\n\n- #146 fix disk cache path for macOS, thanks to @willdahlberg\n\n\n## Nuke 6.0\n\n*Dec 23, 2017*\n\n> About 8 months ago I finally started using Nuke in production. The project has matured from a playground for experimenting with Swift features to something that I rely on in my day's job.\n\nThere are three main areas of improvements in Nuke 6:\n\n- Performance. Nuke 6 is fast! The primary `loadImage(with:into:)` method is now **1.5x** faster thanks to performance improvements of [`CancellationToken`](https://kean.blog/post/cancellation-token), `Manager`, `Request` and `Cache` types. And it's not just main thread performance, many of the background operations were also optimized.\n- API refinements. Some common operations that were surprisingly hard to do are not super easy. And there are no more implementation details leaking into a public API (e.g. classes like `Deduplicator`).\n- Fixes some inconveniences like Thread Sanitizer warnings (false positives!). Improved compile time. Better documentation.\n\n### Features\n\n- Implements progress reporting https://github.com/kean/Nuke/issues/81\n- Scaling images is now super easy with new convenience `Request` initialisers (`Request.init(url:targetSize:contentMode:` and `Request.init(urlRequest:targetSize:contentMode:`)\n- Add a way to add anonymous image processors to the request (`Request.process(key:closure:)` and `Request.processed(key:closure:)`)\n- Add `Loader.Options` which can be used to configure `Loader` (e.g. change maximum number of concurrent requests, disable deduplication or rate limiter, etc).\n\n### Improvements\n\n- Improve performance of [`CancellationTokenSource`](https://kean.blog/post/cancellation-token), `Loader`, `TaskQueue`\n- Improve `Manager` performance by reusing contexts objects between requests\n- Improve `Cache` by ~30% for most operations (hits, misses, writes)\n- `Request` now stores all of the parameters in the underlying reference typed container (it used to store just reference typed ones). The `Request` struct now only has a single property with a reference to an underlying container.\n- Parallelize image processing for up to 2x performance boost in certain scenarios. Might increase memory usage. The default maximum number of concurrent tasks is 2 and can be configured using `Loader.Options`.\n- `Loader` now always call completion on the main thread.\n- Move `URLResponse` validation from `DataDecoder` to `DataLoader`\n- Make use of some Swift 4 feature like nested types inside generic types.\n- Improve compile time.\n- Wrap `Loader` processing and decoding tasks into `autoreleasepool` which reduced memory footprint.\n\n### Fixes\n\n- Get rid of Thread Sanitizer warnings in `CancellationTokenSource` (false positive)\n- Replace `Foundation.OperationQueue` & custom `Foundation.Operation` subclass with a new `Queue` type. It's simpler, faster, and gets rid of pesky Thread Sanitizer warnings https://github.com/kean/Nuke/issues/141\n\n### Removed APIs\n\n- Remove global `loadImage(...)` functions https://github.com/kean/Nuke/issues/142\n- Remove static `Request.loadKey(for:)` and `Request.cacheKey(for:)` functions. The keys are now simply returned in `Request`'s `loadKey` and `cacheKey` properties which are also no longer optional now.\n- Remove `Deduplicator` class, make this functionality part of `Loader`. This has a number of benefits: reduced API surface, improves performance by reducing the number of queue switching, enables new features like progress reporting.\n- Remove `Scheduler`, `AsyncScheduler`, `Loader.Schedulers`, `DispatchQueueScheduler`, `OperationQueueScheduler`. This whole infrastructure was way too excessive.\n- Make `RateLimiter` private.\n- `DataLoader` now works with `URLRequest`, not `Request`\n\n\n# Nuke 5\n\n## Nuke 5.2\n\n*Sep 1, 2017*\n\nAdd support for both Swift 3.2 and 4.0.\n\n\n## Nuke 5.1.1\n\n*Jun 11, 2017*\n\n- Fix Swift 4 warnings\n- Add `DataDecoder.sharedUrlCache` to easy access for shared `URLCache` object\n- Add references to [RxNuke](https://github.com/kean/RxNuke)\n- Minor improvements under the hood\n\n\n## Nuke 5.1\n\n*Feb 23, 2017*\n\n- De facto `Manager` has already implemented `Loading` protocol in Nuke 5 (you could use it to load images directly w/o targets). Now it also conforms to `Loading` protocols which gives access to some convenience functions available in `Loading` extensions.\n- Add `static func targetSize(for view: UIView) -> CGSize` method to `Decompressor`\n- Simpler, faster `Preheater`\n- Improved documentation\n\n\n## Nuke 5.0.1\n\n*Feb 2, 2017*\n\n- #116 `Manager` can now be used to load images w/o specifying a target\n- `Preheater` is now initialized with `Manager` instead of object conforming to `Loading` protocol\n\n\n## Nuke 5.0\n\n*Feb 1, 2017*\n\n### Overview\n\nNuke 5 is a relatively small release which removes some of the complexity from the framework.\n\nOne of the major changes is the removal of promisified API as well as `Promise` itself. Promises were briefly added in Nuke 4 as an effort to simplify async code. The major downsides of promises are compelex memory management, extra complexity for users unfamiliar with promises, complicated debugging, performance penalties. Ultimately I decided that promises were adding more problems that they were solving. \n\n### Changes\n\n#### Removed promisified API and `Promise` itself\n\n- Remove promisified API, use simple closures instead. For example, `Loading` protocol's method `func loadImage(with request: Request, token: CancellationToken?) -> Promise<Image>` was replaced with a method with a completion closure `func loadImage(with request: Request, token: CancellationToken?, completion: @escaping (Result<Image>) -> Void)`. The same applies to `DataLoading` protocol.\n- Remove `Promise` class\n- Remove `PromiseResolution<T>` enum\n- Remove `Response` typealias\n- Add `Result<T>` enum which is now used as a replacement for `PromiseResolution<T>` (for instance, in `Target` protocol, etc)\n\n#### Memory cache is now managed exclusively by `Manager`\n\n- Remove memory cache from `Loader`\n- `Manager` now not only reads, but also writes to `Cache`\n- `Manager` now has new methods to load images w/o target (Nuke 5.0.1) \n\nThe reason behind this change is to reduce confusion about `Cache` usage. In previous versions the user had to pass `Cache` instance to both `Loader` (which was both reading and writing to cache asynchronously), and to `Manager` (which was just reading from the cache synchronously). In a new setup it's clear who's responsible for managing memory cache.\n\n#### Removed `DataCaching` and `CachingDataLoader`\n\nThose two types were included in Nuke to make integrating third party caching libraries a bit easier. However, they were actually not that useful. Instead of using those types you could've just wrapped `DataLoader` yourself with a comparable amount of code and get much more control. For more info see [Third Party Libraries: Using Other Caching Libraries](https://github.com/kean/Nuke/blob/5.0/Documentation/Guides/Third%20Party%20Libraries.md). \n\n#### Other Changes\n\n- `Loader` constructor now provides a default value for `DataDecoding` object\n- `DataLoading` protocol now works with a `Nuke.Request` and not `URLRequest` in case some extra info from `URLRequest` is required\n- Reduce default `URLCache` disk capacity from 200 MB to 150 MB\n- Reduce default `maxConcurrentOperationCount` of `DataLoader` from 8 to 6\n- Shared objects (like `Manager.shared`) are now constants.\n- `Preheater` is now initialized with `Manager` instead of `Loading` object\n- Add new [Third Party Libraries](https://github.com/kean/Nuke/blob/5.0/Documentation/Guides/Third%20Party%20Libraries.md) guide.\n- Improved documentation\n\n\n# Nuke 4\n\n## Nuke 4.1.2\n\n*Oct 22, 2016*\n\nBunch of improvements in built-in `Promise`:\n- `Promise` now also uses new `Lock` - faster creation, faster locking\n- Add convenience `isPending`, `resolution`, `value` and `error` properties\n- Simpler implementation migrated from [Pill.Promise](https://github.com/kean/Pill)*\n\n*`Nuke.Promise` is a simplified variant of [Pill.Promise](https://github.com/kean/Pill) (doesn't allow `throws`, adds `completion`, etc). The `Promise` is built into Nuke to avoid fetching external dependencies.\n\n\n## Nuke 4.1.1\n\n*Oct 4, 2016*\n\n- Fix deadlock in `Cache` - small typo, much embarrassment  😄 (https://github.com/kean/Nuke-Alamofire-Plugin/issues/8)\n\n\n## Nuke 4.1 ⚡️\n\n*Oct 4, 2016*\n\nNuke 4.1 is all about **performance**. Here are some notable performance improvements:\n\n- `loadImage(with:into:)` method with a default config is **6.3x** faster\n- `Cache` operations (write/hit/miss) are from **3.1x** to **4.5x** faster\n\nNuke 4.0 focused on stability first, naturally there were some performance regressions. With the version 4.1 Nuke is again [the fastest framework](https://github.com/kean/Image-Frameworks-Benchmark) out there. The performance is ensured by a new set of performance tests.\n\n<img src=\"https://cloud.githubusercontent.com/assets/1567433/19019388/26463bb2-888f-11e6-87dd-42c2d82c5dae.png\" width=\"500\"/>\n\nIf you're interested in the types of optimizations that were made check out recent commits. There is a lot of awesome stuff there!\n\nNuke 4.1 also includes a new [Performance Guide](https://github.com/kean/Nuke/blob/4.1/Documentation/Guides/Performance%20Guide.md) and a collection of [Tips and Tricks](https://github.com/kean/Nuke/blob/4.1/Documentation/Guides/Tips%20and%20Tricks.md).\n\n### Other Changes\n\n- Add convenience method `loadImage(with url: URL, into target: AnyObject, handler: @escaping Handler)` (more useful than anticipated).\n- #88 Add convenience `cancelRequest(for:)` function\n- Use `@discardableResult` in `Promise` where it makes sense\n- Simplified `Loader` implementation\n- `Cache` nodes are no longer deallocated recursively on `removeAll()` and `deinit` (I was hitting stack limit in benchmarks, it's impossible in real-world use).\n- Fix: All `Cache` public `trim()` methods are now thread-safe too.\n\n\n## Nuke 4.0 🚀\n\n*Sep 19, 2016*\n\n### Overview\n \nNuke 4 has fully adopted the new **Swift 3** changes and conventions, including the new [API Design Guidelines](https://swift.org/documentation/api-design-guidelines/).\n \nNuke 3 was already a slim framework. Nuke 4 takes it a step further by simplifying almost all of its components.\n \nHere's a few design principles adopted in Nuke 4:\n \n- **Protocol-Oriented Programming.** Nuke 3 promised a lot of customization by providing a set of protocols for loading, caching, transforming images, etc. However, those protocols were vaguely defined and hard to implement in practice. Protocols in Nuke 4 are simple and precise, often consisting of a single method.\n- **Single Responsibility Principle.** For example, instead of implementing preheating and deduplicating of equivalent requests in a single vague `ImageManager` class, those features were moved to separate classes (`Preheater`, `Deduplicator`). This makes core classes much easier to reason about.\n- **Principle of Least Astonishment**. Nuke 3 had a several excessive protocols, classes and methods which are *all gone* now (`ImageTask`, `ImageManagerConfiguration` just to name a few). Those features are much easier to use now.\n- **Simpler Async**. Image loading involves a lot of asynchronous code, managing it was a chore. Nuke 4 adopts two design patterns ([**Promise**](https://github.com/kean/Promise) and **CancellationToken**) that solve most of those problems.\n \nThe adoption of those design principles resulted in a simpler, more testable, and more concise code base (which is now under 900 slocs, compared to AlamofireImage's 1426, and Kingfisher's whopping 2357).\n \nI hope that Nuke 4 is going to be a pleasure to use. Thanks for your interest 😄\n \nYou can learn more about Nuke 4 in an in-depth [**Nuke 4 Migration Guide**](https://github.com/kean/Nuke/blob/4.1/Documentation/Migrations/Nuke%204%20Migration%20Guide.md).\n\n### Highlighted New Features\n \n#### LRU Memory Cache\n \nNuke 4 features a new custom LRU memory cache which replaced `NSCache`. The primary reason behind this change was the fact that `NSCache` is not LRU]. The new `Nuke.Cache` has some other benefits like better performance, and more control which would enable some new advanced features in future versions.\n\n#### Rate Limiter\n \nThere is a known problem with `URLSession` that it gets trashed pretty easily when you resume and cancel `URLSessionTasks` at a very high rate (say, scrolling a large collection view with images). Some frameworks combat this problem by simply never cancelling `URLSessionTasks` which are already in `.running` state. This is not an ideal solution, because it forces users to wait for cancelled requests for images which might never appear on the display.\n \nNuke has a better, classic solution for this problem - it introduces a new `RateLimiter` class which limits the rate at which `URLSessionTasks` are created. `RateLimiter` uses a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm. The implementation supports quick bursts of requests which can be executed without any delays when \"the bucket is full\". This is important to prevent the rate limiter from affecting \"normal\" requests flow. `RateLimiter` is enabled by default.\n \nYou can see `RateLimiter` in action in a new `Rate Limiter Demo` added in the sample project.\n\n#### Toucan Plugin\n\nMake sure to check out new [Toucan plugin](https://github.com/kean/Nuke-Toucan-Plugin) which provides a simple API for processing images. It supports resizing, cropping, rounded rect masking and more.\n \n\n# Nuke 3\n\n## Nuke 3.2\n\n*Sep 8, 2016*\n \n - Swift 2.3 support\n - Preheating is now thread-safe #75\n\n\n## Nuke 3.1.3\n\n*Jul 17, 2016*\n\n#72 Fix synchronization issue in ImageManager loader:task:didFinishWithImage... method\n\n\n## Nuke 3.1.2\n\n*Jul 14, 2016*\n\n- #71 ImageViewLoadingController now cancels tasks synchronously, thanks to @adomanico\n\n\n## Nuke 3.1.1\n\n*Jun 7, 2016*\n\n- Demo project update to support CocoaPods 1.0\n- #69 Bitcode support for Carthage builds, thanks to @vincentsaluzzo\n\n\n## Nuke 3.1\n\n*Apr 15, 2016*\n\n- #64 Fix a performance regression: images are now decoded once per DataTask like they used to\n- #65 Fix an issue custom on-disk cache (`ImageDiskCaching`) was called `setData(_:response:forTask:)` method when the error wasn't nil\n- Add notifications for NSURLSessionTask state changes to enable activity indicators (based on https://github.com/kean/Nuke-Alamofire-Plugin/issues/4)\n\n\n## Nuke 3.0\n\n*Mar 26, 2016*\n\n- Update for Swift 2.2\n- Move `ImagePreheatController` to a standalone package [Preheat](https://github.com/kean/Preheat)\n- Remove deprecated `suspend` method from `ImageTask`\n- Remove `ImageFilterGaussianBlur` and Core Image helper functions which are now part of [Core Image Integration Guide](https://github.com/kean/Nuke/wiki/Core-Image-Integration-Guide)\n- Cleanup project structure (as expected by SPM)\n- `Manager` constructor now has a default value for configuration\n- `nk_setImageWith(URL:)` method no longer resizes images by default, because resizing is not effective in most cases\n- Remove `nk_setImageWith(request:options:placeholder:)` method, it's trivial\n- `ImageLoadingView` default implementation no longer implements \"Cross Dissolve\" animations, use `ImageViewLoadingOptions` instead (see `animations` or `handler` property)\n- Remove `ImageViewDefaultAnimationDuration`, use `ImageViewLoadingOptions` instead (see `animations` property)\n- `ImageDisplayingView` protocol now has a single `nk_displayImage(_)` method instead of a `nk_image` property\n- Remove `nk_targetSize` property from `UI(NS)View` extension\n\n\n# Nuke 2\n\n## Nuke 2.3\n\n*Mar 19, 2016*\n\n- #60 Add custom on-disk caching support (see `ImageDiskCaching` protocol)\n- Reduce dynamic dispatch\n\n\n## Nuke 2.2\n\n*Mar 11, 2016*\n\n- `ImageTask` `suspend` method is deprecated, implementation does nothing\n- `ImageLoader` now limits a number of concurrent `NSURLSessionTasks`\n- Add `maxConcurrentSessionTaskCount` property to `ImageLoaderConfiguration`\n- Add `taskReusingEnabled` property to `ImageLoaderConfiguration`\n- Add [Swift Package Manager](https://swift.org/package-manager/) support\n- Update documentation\n\n\n## Nuke 2.1\n\n*Feb 27, 2016*\n \n- #57 `ImageDecompressor` now uses `CGImageAlphaInfo.NoneSkipLast` for opaque images \n- Add `ImageProcessorWithClosure` that can be used for creating anonymous image filters\n- `ImageLoader` ensures thread safety of image initializers by running decoders on a `NSOperationQueue` with `maxConcurrentOperationCount=1`. However, `ImageDecoder` class is now also made thread safe.\n\n\n## Nuke 2.0.1\n\n*Feb 10, 2016*\n\n- #53 ImageRequest no longer uses NSURLSessionTaskPriorityDefault, which requires CFNetwork that doesn't get added as a dependency automatically\n\n\n## Nuke 2.0\n\n*Feb 6, 2016*\n\nNuke now has an [official website](http://kean.blog/Nuke/)!\n\n#### Main Changes\n\n- #48 Update according to [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/). All APIs now just feel right.\n- Add `UIImage` extension with helper functions for `Core Image`: `nk_filter(_:)`, etc.\n- Add `ImageFilterGaussianBlur` as an example of a filter on top of `Core Image` framework\n- Add `ImageRequestMemoryCachePolicy` enum that specifies the way `Manager` interacts with a memory cache; `NSURLRequestCachePolicy` no longer affects memory cache\n- #17 Add `priority` to `ImageRequest`\n- Add `removeResponseForKey()` method to `ImageMemoryCaching` protocol and the corresponding method to `Manager`\n- Implement congestion control for `ImageLoader` that prevents `NSURLSession` trashing\n- Simplify `ImageLoaderDelegate` by combining methods that were customizing processing in a single high-level method: `imageLoader(_:processorFor:image:)`. Users now have more control over processing\n- Add `NSURLResponse?` parameter to `decode` method from `ImageDecoding` protocol\n- `DataLoading` protocol no longer has `isLoadEquivalentRequest(_:toRequest)` and `isCacheEquivalentRequest(_:toRequest)`. Those methods are now part of `ImageLoaderDelegate` and they have default implementation\n- `ImageResponseInfo` is now a struct\n- Improved error reporting (codes are now stored in enum, more codes were added, error is now created with a failure reason)\n\n#### UI Extensions Changes\n- Move `nk_ImageTask(_:didFinishWithResponse:options)` method to `ImageLoadingView` protocol, that's really where it belongs to\n- Add `handler` property to `ImageViewLoadingOptions` that allows you to completely override display/animate logic in `ImageLoadingView`\n- Remove `nk_prepareForReuse` method from `ImageLoadingView` extensions (useless)\n- Remove `placeholder` from `ImageViewLoadingOptions`, move it to a separate argument which is only available on `ImageDisplayingView`s\n- Add `animated`, `userInfo` to `ImageViewLoadingOptions`\n- `ImageViewLoadingOptions` is now nonull everywhere\n- Add `setImageWith(task:options:)` method to `ImageViewLoadingController`\n\n#### Other Changes\n\n- If you add a completion handler for completed task, the response is now marked as `isFastResponse = true`\n- Fix an issue that allowed incomplete image downloads to finish successfully when using built-in networking\n- `equivalentProcessors(rhs:lhs:)` function is now private (and it also is renamed)\n- Remove public `isLoadEquivalentToRequest(_:)` and `isCacheEquivalentToRequest(_:)` methods from `ImageRequest` extension\n- Add `ImageTaskProgress` struct that represents load progress, move `fractionCompleted` property from `ImageTask` to `ImageTaskProgress`\n- Remove public helper function `allowsCaching` from `ImageRequest` extension\n- Remove deprecated `XCPSetExecutionShouldContinueIndefinitely` from playground\n\n\n# Nuke 1\n\n## Nuke 1.4\n\n*Jan 9, 2016*\n\n- #46 Add option to disable memory cache storage, thanks to @RuiAAPeres\n\n\n## Nuke 1.3\n\n*Dec 7, 2015*\n\n- Add [Core Image Integration Guide](https://github.com/kean/Nuke/wiki/Core-Image-Integration-Guide)\n- Fill most of the blanks in the documentation\n- #47 Fix target size rounding errors in image downscaling (Pyry Jahkola @pyrtsa)\n- Add `imageScale` property to `ImageDecoder` class that returns scale to be used when creating `UIImage` (iOS, tvOS, watchOS only)\n- Wrap each iteration in `ProcessorComposition` in an `autoreleasepool`\n\n\n## Nuke 1.2\n\n*Nov 15, 2015*\n\n- #20 Add preheating for UITableView (see ImagePreheatControllerForTableView class)\n- #41 Enhanced tvOS support thanks to @joergbirkhold\n- #39 UIImageView: ImageLoadingView extension no available on tvOS\n- Add factory method for creating session tasks in DataLoader\n- Improved documentation\n\n\n## Nuke 1.1.1\n\n*Oct 30, 2015*\n\n- #35 ImageDecompressor now uses `32 bpp, 8 bpc, CGImageAlphaInfo.PremultipliedLast` pixel format which adds support for images in an obscure formats, including 16 bpc images.\n- Improve docs\n\n\n## Nuke 1.1\n\n*Oct 23, 2015*\n\n- #25 Add tvOS support\n- #33 Add app extensions support for OSX target (other targets were already supported)\n\n\n## Nuke 1.0\n\n*Oct 18, 2015*\n\n- #30 Add new protocols and extensions to make it easy to add full featured image loading capabilities to custom UI components. Here's how it works:\n```swift\nextension MKAnnotationView: ImageDisplayingView, ImageLoadingView {\n    // That's it, you get default implementation of all the methods in ImageLoadingView protocol\n    public var nk_image: UIImage? {\n        get { return self.image }\n        set { self.image = newValue }\n    }\n}\n```\n- #30 Add UIImageView extension instead of custom UIImageView subclass\n- Back to the Mac! All new protocol and extensions for UI components (#30) are also available on a Mac, including new NSImageView extension.\n- #26 Add `getImageTaskWithCompletion(_:)` method to Manager\n- Add essential documentation\n- Add handy extensions to ImageResponse\n\n\n# Nuke 0.x\n\n## Nuke 0.5.1\n\n*Oct 13, 2015*\n\nNuke is now available almost [everywhere](https://github.com/kean/Nuke/wiki/Supported-Platforms). Also got rid of CocoaPods subspecs.\n\n### New Supported Platforms\n\n- CocoaPods, Nuke, watchOS\n- CocoaPods, Nuke, OSX\n- CocoaPods, NukeAlamofirePlugin, watchOS\n- CocoaPods, NukeAlamofirePlugin, OSX\n- Carthage, Nuke, watchOS\n- Carthage, Nuke, OSX\n- Carthage, NukeAlamofirePlugin, iOS\n- Carthage, NukeAlamofirePlugin, watchOS\n- Carthage, NukeAlamofirePlugin, OSX\n\n### Repo Changes\n\n- Remove Nuke/Alamofire subspec, move sources to separate repo [Nuke-Alamofire-Plugin](https://github.com/kean/Nuke-Alamofire-Plugin)\n- Remove Nuke/GIF subspec, move sources to separate repo [Nuke-AnimatedImage-Plugin](https://github.com/kean/Nuke-AnimatedImage-Plugin)\n\n### Code Changes\n\n- #9, #19 ImageTask now has a closure for progress instead of NSProgress\n- Rename ImageLoadingDelegate to ImageLoadingManager\n- Add ImageLoaderDelegate with factory method to construct image decompressor, and `shouldProcessImage(_:)` method\n- Make ImageRequest extensions public\n- Make ImageTask constructor public; Annotate abstract methods.\n\n\n## Nuke 0.5\n\n*Oct 9, 2015*\n\nThis is a pre-1.0 version, first major release which is going to be available soon.\n\n### Major\n\n- #18 ImageTask can now be suspended (see `suspend()` method). Add `suspendLoadingForImageTask(_:)` method to `ImageLoading` protocol\n- #24 ImageRequest can now be initialized with NSURLRequest. ImageDataLoading `imageDataTaskWithURL(_:progressHandler:completionHandler:)` method changed to `imageDataTaskWithRequest(_:progressHandler:completionHandler:)`. First parameter is ImageRequest, return value change from NSURLSessionDataTask to NSURLSessionTask.\n- ImageLoader no longer limits number of concurrent NSURLSessionTasks (which had several drawbacks)\n- Add base ImagePreheatingController class\n- Multiple preheating improvements: significantly simplified implementation; visible index paths are now subtracted from preheat window; performance improvements.\n\n### Minor\n\n- BUGFIX: When subscribing to an existing NSURLSessionTask user was receiving progress callback with invalid totalUnitCount\n- Add public `equivalentProcessors(lhs:rhs:) -> Bool` function that works on optional processors\n- Add essential documentation\n\n\n## Nuke 0.4\n\n*Oct 4, 2015*\n\n### Major\n\n- Make ImageLoading protocol and ImageLoader class public\n- Make ImageManager `cachedResponseForRequest(_:)` and `storeResponse(_:forRequest:)` methods public\n- Make ImageRequestKey class and ImageRequestKeyOwner protocol public\n- Remove unused ImageManaging and ImagePreheating protocols \n\n### Minor\n\n- #13 BUGFIX: Crash on 32-bit iOS simulator on Mac with 16Gb+ of RAM (@RuiAAPeres)\n- BUGFIX: Processing operation might not get cancelled in certain situations\n- ImageProcessing protocol now provides default implementation for `isEquivalentToProcessor(_:)` method including separate implementation for processors that also conform to Equatable protocol.\n- Add identifier: Int property to ImageTask\n- ImageRequestKey now relies on Hashable and Equatable implementation provided by NSObject\n- ImageMemoryCaching protocol now works with ImageRequestKey class\n\n### Plumbing\n\n- Adopt multiple Swift best practices (tons of them in fact)\n- ImageManager is now fully responsible for memory caching and preheating, doesn't delegate any work to ImageLoader (simplifies its implementation and limits dependencies)\n- Remove ImageRequestKeyType enum\n- Rename ImageManagerLoader to ImageLoader\n- Simply ImageManagerLoader (now ImageLoader) implementation\n- Add multiple unit tests\n\n\n## Nuke 0.3.1\n\n*Sep 22, 2015*\n\n#10 Fix Carthage build\n\n\n## Nuke 0.3\n\n*Sep 21, 2015*\n\n- ImageTask now acts like a promise\n- ImageManager.shared is now a property instead of a method\n- ImageTask progress is now created lazily\n- Add maxConcurrentTaskCount to ImageManagerConfiguration\n- Move taskWithURL method to ImageManaging extension\n- Add ImagePreheating protocol\n- Multiple improvements across the board\n\n\n## Nuke 0.2.2\n\n*Sep 20, 2015*\n\n- Add limited Carthage support (doesn't feature [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) and [Alamofire](https://github.com/Alamofire/Alamofire) integration yet, you'll have to stick with CocoaPods for that)\n- ImageTask resume() and cancel() methods now return Self\n- ImageTask completion property is now public\n- Minor implementation improvements\n\n\n## Nuke 0.2.1\n\n*Sep 19, 2015*\n\n- Add ImageCollectionViewPreheatingController (yep)\n- Add [Image Preheating Guide](https://github.com/kean/Nuke/wiki/Image-Preheating-Guide)\n- Add preheating demo\n\n\n## Nuke 0.2\n\n*Sep 18, 2015*\n\n#### Major\n\n- Optional [Alamofire](https://github.com/Alamofire/Alamofire) integration via 'Nuke/Alamofire' subspec\n- Optional [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) integration via 'Nuke/GIF' subspec\n- More concise API\n- Add image filters to ImageRequest\n- Add ImageManaging protocol\n- Add ImageDecoderComposition\n- Add ImageProcessorComposition\n- watchOS\n- Add convenience functions that forward calls to the shared manager\n\n#### Minor\n\n- Use ErrorType\n- Add removeAllCachedImages method\n- ImageRequest userInfo is now Any? so that you can pass anything including closures\n- ImageResponseInfo now has userInfo: Any? property\n- ImageResponseInfo is now a struct\n- CachedImageResponse renamed to ImageCachedResponse; userInfo is now Any?\n- Multiple improvements across the board\n\n\n## Nuke 0\n\n*Mar 11, 2015*\n\n- Initial commit\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 10 Migration Guide.md",
    "content": "# Nuke 10 Migration Guide\n\nThis guide eases the transition of the existing apps that use Nuke 9.x to the latest version of the framework.\n\n> To learn about the new features in Nuke 10, see the [release notes](https://github.com/kean/Nuke/releases/tag/10.0.0).\n\n## Minimum Requirements\n\n- iOS 11.0, tvOS 11.0, macOS 10.13, watchOS 4.0\n- Xcode 12.0\n- Swift 5.3\n\n## Overview\n\nNuke 10 contains a ton of new features, refinements, and performance improvements. There are some breaking changes and deprecations that the compiler will guide you through as you update. Most users are not going to need this guide.\n\n## loadImage() Signature\n\nThe completion callback is now required.\n\n```swift\n// Before (Nuke 9)\npipeline.loadImage(with: request)\n\n// After (Nuke 10)\npipeline.loadImage(with: request) { _ in }\n```\n\n## Disk Cache Policy\n\nReplace deprecated `DataCacheOptions.storedItems` in the pipeline configuration with [`DataCachePolicy`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Configuration_DataCachePolicy/).\n\n```swift\nvar configuration = ImagePipeline.Configuration()\n\n// Before (Nuke 9)\nconfiguration.dataCacheOptions.storedItems = [.finalImage]\n\n// After (Nuke 10)\nconfiguration.dataCachePolicy = .storeEncodedImages\n```\n\n```swift\n// Before (Nuke 9) \nconfiguration.dataCacheOptions.storedItems = [.originalImageData]\n\n// After (Nuke 10)\nconfiguration.dataCachePolicy = .storeOriginalData\n```\n\n```swift\n// Before (Nuke 9) \nconfiguration.dataCacheOptions.storedItems = [.finalImage, .originalImageData]\n\n// After (Nuke 10)\nconfiguration.dataCachePolicy = .storeAll\n```\n\nOr use a new [`.automatic`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Configuration_DataCachePolicy/#imagepipeline.configuration.datacachepolicy.automatic) policy if it best fits your needs: for requests with processors, encode and store processed images; for requests with no processors, store original image data. \n\n> Learn more about the policies and other caching changes in [\"Caching: Cache Policy.\"](https://kean.blog/nuke/guides/caching#cache-policy)\n\n## Disk Cache Configuration\n\nNuke 10 simplifies disk cache configuration by introducing two built-in configurations: [`ImagePipeline.Configuration.withDataCache`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Configuration/#imagepipeline.configuration.withdatacache) (aggressive disk cache enabled) and [`withURLCache`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Configuration/#imagepipeline.configuration.withurlcache) (HTTP disk cache enabled)\n\n```swift\n// Before (Nuke 9)\nlet dataLoader: DataLoader = {\n    let config = URLSessionConfiguration.default\n    config.urlCache = nil\n    return DataLoader(configuration: config)\n}()\n\nvar config = ImagePipeline.Configuration()\nconfig.dataLoader = dataLoader\nconfig.dataCache = try? DataCache(name: \"com.github.kean.Nuke.DataCache\")\n```\n\n```swift\n// After (Nuke 10)\nlet config = ImagePipeline.Configuration.withDataCache\n```\n\n> Learn more in [\"Caching: Configuration.\"](https://kean.blog/nuke/guides/caching#configuration)\n\n## Direct Access to Cache\n\nIn the previous versions, there was no clear way to access underlying cache layers: you could either access each individual layer directly, or use some `ImagePipeline` APIs. Nuke 10 introduces a new cohesive model for working with caches:  [`ImagePipeline.Cache`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Cache/). It has a whole range of convenience APIs for managing cached images: read, write, remove images from all cache layers.\n\n```swift\n// Before (Nuke 9) (reading memory cache)\nlet image = pipeline.cachedImage(for: request)\n\n// After (Nuke 10) (now readwrite)\nlet image = pipeline.cache[request]\npipeline.cache[request] = image\npipeline.cache[request] = nil\n```\n\n```swift\n// Before (Nuke 9) (reading disk cache)\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\")!)\nlet key = pipeline.cacheKey(for: request, item: .originalImageData)\nlet data = pipeline.dataCache.cachedData(for: key)\n\n// After (Nuke 10)\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\"))\nlet data = pipeline.cache.cachedData(for: request)\n```\n\n```swift\n// New (Nuke 10)\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\"))\nlet cache = pipeline.cache\nlet image = cache.cachedImage(for: request) // caches: [.all]\nlet image = cache.cachedImage(for: request, caches: [.disk])\n\nif cache.containsData(for: request) {\n    // ...\n}\n\ncache.removeAll()\n```\n\nIf you still need access to cache keys, [`ImagePipeline.Cache`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipeline_Cache/) provides it as well.\n\n```swift\n// Before (Nuke 9)\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\"), processors: [ImageProcessors.Resize(width: 44)])\nlet originalDataKey = pipeline.cacheKey(for: request, item: .originalImageData)\nlet processedDataKey = pipeline.cacheKey(for: request, item: .finalImage)\n\n// After (Nuke 10)\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\"))\nlet originalDataKey = pipeline.cache.makeDataCacheKey(for: request)\n\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\"), processors: [ImageProcessors.Resize(width: 44)])\nlet processedDataKey = pipeline.cache.makeDataCacheKey(for: request)\n```\n\nAnd you can now also access image (memory) cache keys:\n\n```swift\n// New (Nuke 10)\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\"))\nlet originalDataKey = pipeline.cache.makeImageCacheKey(for: request)\n```\n\n> Learn more about the new cache APIs in [\"Caching: Direct Access.\"](https://kean.blog/nuke/guides/caching#direct-access)\n\n## ImageRequestOptions\n\nNuke 10 has a reworked [`ImageRequest.Options`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImageRequest_Options/) option set replacing removed `ImageRequestOptions`. The name is similar, but the options are slightly different. \n\n```swift\nvar request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\")!)\n\n// Before (Nuke 9)\nrequest.cachePolicy = .reloadIgnoringCachedData\nrequest.options.filteredURL = \"example.com/image.jpeg\"\n\n// After (Nuke 10)\nrequest.options = [.reloadIgnoringCachedData]\nrequest.userInfo[.imageIdKey] = \"example.com/image.jpeg\" \n```\n\n`MemoryCacheOptions` are now also part of the same options set.\n\n```swift\n// Before (Nuke 9)\nrequest.options.memoryCacheOptions.isReadAllowed = false\n\n// After (Nuke 10)\nrequest.options = [.disableMemoryCacheRead]\n\n// New (Nuke 10)\nrequest.options = [.disableDiskCache]\n```\n\nThanks to these changes, `ImageRequest` now has more options and at the same time uses much less memory (the size reduced from 176 bytes to just 48 bytes).\n\nA deprecated `cacheKey` was a poorly designed option. It was serving a role similar to `filteredURL` but only worked for the memory cache. A better approach is to use the new `.imageIdKey`.\n\n```swift\n// Before (Nuke 9)\nrequest.options.cacheKey = \"example.com/image.jpeg\"\n\n// After (Nuke 10)\nrequest.userInfo[.imageIdKey] = \"example.com/image.jpeg\"\n```\n\nAnd new in Nuke 10, you can now customize the cache keys using the new [`ImagePipelineDelegate`](https://kean-org.github.io/docs/nuke/reference/10.0.0/ImagePipelineDelegate/) protocol.\n\n```swift\n// New (Nuke 10)\nfinal class YourImagePipelineDelegate: ImagePipelineDelegate {\n    func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? {\n        request.userInfo[\"someKey\"] as? String // If `nil`, use default key\n    }\n}\n```\n\nAnd `loadKey` is straight up removed. This API does nothing starting with Nuke 10. If you found an issue with task coalescing, please report it on GitHub and consider disabling it using `ImagePipeline.Configuration`.\n\n## Optional Request\n\nThe request in image view extensions is now optional.\n\n```swift\n// Before (Nuke 9)\nif let url = URL(string: string) {\n    Nuke.loadImage(with: url, into: imageView)\n} else {\n    imageView.image = failureImagePlaceholder\n}\n\n// After (Nuke 10)\nNuke.loadImage(with: URL(string: string), into: imageView)\n```\n\nIf the request is `nil`, it's handled as a failure scenario.\n\n## Extended ImageRequestConvertible\n\n`ImageRequestConvertible` now supports `String` types.\n\n```swift\n// Before (Nuke 9)\npipeline.loadImage(with: URL(string: \"https://example.com/image.jpeg\")!) { _ in }\n\n// New (Nuke 10)\npipeline.loadImage(with: \"https://example.com/image.jpeg\") { _ in }\n```\n\nAnd, of course, you can still pass `URL` values as usual.\n\n## Animated Images\n\n```swift\n// Before (Nuke 9)\nconfiguration.isAnimatedImageDataEnabled = true\nlet data = response.image.animatedImageData // ObjC associated object\n\n// After (Nuke 10)\nlet data = response.container.data // Attached automatically for GIFs\n```\n\n`Nuke_ImageDisplaying` protocol was also updated: you now receive `data` in the callback that you can use for rendering (data is only attached when needed).  \n\n## ImagePipelineObserving\n\n`ImagePipelineObserving` protocol is now part of the new `ImagePipelineDelegate` protocol.\n\n```swift\n// Before (Nuke 9)\nlet pipeline = ImagePipeline()\npipeline.observer = MockImagePipelineObserver()\n\nclass YourImagePipelineObserver: ImagePipelineObserving {\n    func pipeline(_ pipeline: ImagePipeline, imageTask: ImageTask, didReceiveEvent event: ImageTaskEvent) {\n        // ...\n    }\n}\n```\n\n```swift\n// After (Nuke 10)\nlet pipeline = ImagePipeline(delegate: MockImagePipelineObserver())\n\nclass YourImagePipelineObserver: ImagePipelineDelegate {\n    func pipeline(_ pipeline: ImagePipeline, imageTask: ImageTask, didReceiveEvent event: ImageTaskEvent) {\n        // ...\n    }\n}\n\nImagePipeline.shared = pipeline // To set the default pipeline\n```\n\n---\n\nThere are a lot more changes in Nuke 10. You can learn about all of them in the [release notes](https://github.com/kean/Nuke/releases/tag/10.0.0).\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 11 Migration Guide.md",
    "content": "# Nuke 11 Migration Guide\n\nThis guide eases the transition of the existing apps that use Nuke 10.x to the latest version of the framework.\n\n> To learn about the new features in Nuke 11, see the [release notes](https://github.com/kean/Nuke/releases/tag/11.0.0).\n\n## Minimum Requirements\n\n- iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0\n- Xcode 13.3\n- Swift 5.6\n\n## Error Reporting Improvements\n\nIf you are implementing custom image decoders or processors, their primary APIs are now throwing to allow the users to provide more information in case something goes wrong:\n\n```swift\n// Before (Nuke 10)\npublic protocol ImageDecoding {\n    func decode(_ data: Data) -> ImageContainer?\n}\n\n// After (Nuke 11)\npublic protocol ImageDecoding {\n    func decode(_ data: Data) throws -> ImageContainer\n}\n```\n\n> You can use a new `ImageDecodingContext.unknown` in case there is nothing to report.\n\n```swift\n// Before (Nuke 10)\npublic protocol ImageProcessing {\n    // This method has no changes.\n    func process(_ image: PlatformImage) -> PlatformImage?\n\n    func process(_ container: ImageContainer, context: ImageProcessingContext) -> ImageContainer?\n}\n\n// After (Nuke 11)\npublic protocol ImageProcessing {\n    // This method has no changes.\n    func process(_ image: PlatformImage) -> PlatformImage?\n\n    // This is now throwing.\n    func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer\n}\n```\n\n## ImageProcessing and Hashable\n\nIf you are implementing custom image processors `ImageProcessing` that implement `hashableIdentifier` and return self, you can remove the `hashableIdentifier` implementation and use the one provided by default.\n\n```swift\n// Before (Nuke 10)\nextension ImageProcessors {\n    /// Scales an image to a specified size.\n    public struct Resize: ImageProcessing, Hashable {\n        private let size: CGSize\n        \n        var hashableIdentifier: AnyHashable { self }\n    }\n}\n\n// After (Nuke 11)\nextension ImageProcessors {\n    /// Scales an image to a specified size.\n    public struct Resize: ImageProcessing, Hashable {\n        private let size: CGSize\n    }\n}\n```\n\n## Invalidation\n\nIf you invalidate the pipeline, any new requests will immediately fail with `ImagePipeline/Error/pipelineInvalidated` error.\n\n## ImageRequestConvertible\n\n`ImageRequestConvertible` was originally introduced in [Nuke 9.2](https://github.com/kean/Nuke/releases/tag/9.2.0) to reduce the number of `loadImage(:)` APIs in code completion, but it's no longer an issue with the new async/await APIs.\n\n`ImageRequestConvertible` is soft-deprecated in Nuke 11. The other soft-deprecated APIs, such as a closure-based `ImagePipeline/loadImage(:)` will continue working with it. The new APIs, such as async/await `ImagePipeline/image(for:)` will work with `URL` and `ImageRequest` which is better for discoverability and performance.\n\nIf you are using `ImageRequestConvertible` in your code, consider removing it now. But it won't be officially deprecated until the next major release.\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 12 Migration Guide.md",
    "content": "# Nuke 12 Migration Guide\n\nThis guide eases the transition of the existing apps that use Nuke 11.x to the latest version of the framework.\n\n> To learn about the new features in Nuke 12, see the [release notes](https://github.com/kean/Nuke/releases/tag/12.0.0).\n\n## Minimum Requirements\n\nThe minimum requirements are unchanged.\n\n- iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0\n- Xcode 13.3\n- Swift 5.6\n\n## Async/Await\n\nThe existing convenience `ImagePipeline/image(for:)` methods now return an image (`UIImage` or `NSImage`)  instead of `ImageResponse`.\n\n```swift\n// Before (Nuke 11)\nlet image: ImageResponse = try await ImagePipeline.shared.image(for: url)\n\n// After (Nuke 12)\nlet image: UIImage = try await ImagePipeline.shared.image(for: url)\n\n// To retrieve `ImageResponse` use new `imageTask(with:)` method (Nuke 12)\nlet response = try await ImagePipeline.shared.imageTask(with: url).response\n```\n\nThe existing `ImagePipeline/image(for:)` method also no longer has an `ImageTaskDelegate` parameter – there was no way to make it `Sendable` without compromises. Instead, use the new `ImagePipeline.imageTask(with:)` method that returns an instance of a new `AsyncImageTask` type.\n\n```swift\n// Before (Nuke 11)\nlet response = try await ImagePipeline.shared.image(for: url, delegate: self)\n\nfunc imageTaskCreated(_ task: ImageTask) {\n    self.imageTask = task\n}\n\nfunc imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse) {\n    // Gets called for images that support progressive decoding.\n}\n\nfunc imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress) {\n    // Gets called when the download progress is updated.\n}\n```\n\n```swift\n// After (Nuke 12)\nlet imageTask = ImagePipeline.shared.imageTask(with: url)\nfor await progress in imageTask.progress {\n    // Update progress\n}\nimageView.image = try await imageTask.image\n```\n\nThe new API is both easier to use, _and_ it's fully `Sendable` compliant.\n\nIf you implement `ImageTaskDelegate` as part of the pipeline's delegate, make sure to update the method to include the new `pipeline` parameter:\n\n```swift\n// Before (Nuke 11)\nfinal class YourPipelineDelegate: ImagePipelineDelegate {\n    func imageTaskCreated(_ task: ImageTask) {\n        // ...\n    }\n}\n\n// After (Nuke 12)\nfinal class YourPipelineDelegate: ImagePipelineDelegate {\n    func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {\n        // ...\n    }\n}\n```\n\n## LazyImage\n\nIn addition to the changes to the `LazyImage` interface, there are a couple of important internal changes:\n\n- It now uses `SwiftUI.Image` for displayed fetched images, which changes its self-sizing and layout behavior that now exactly matches `AsyncImage`\n- It no longer plays GIFs and videos\n- Transition animations are disabled by default\n- Progress updates no longer trigger `content` reload\n\nSo it's best to think about it as an entirely new component, rather than an improvement of the previous one. This is one of the major reasons that instead of API deprecations, this release straight-up changes the existing APIs.\n\nTo achieve the previous sizing behavior, you'll now need to provide a `content` closure – just like with `AsyncImage`. It's slightly more code, but it provides you complete access to the underlying image.\n\n```swift\n// Before (Nuke 11)\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\"), resizingMode: .aspectFill) \n\n// After (Nuke 12)\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\")) { state in\n    if let image = state.image {\n        image\n            .resizable()\n            .aspectRatio(contentMode: .fill)\n    }\n}\n```\n\nTo display an animated image, use one of the GIF rendering frameworks, such as [Gifu](https://github.com/kaishin/Gifu), directly:\n\n```swift\n// After (Nuke 12)\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\")) { state in\n    if let container = state.imageContainer {\n        if container.type == .gif {\n            // Use a view capable of displaying animated images\n        } else {\n            state.image // Use the default view\n        }\n    }\n}\n```\n\nThe same approach applies to videos, but you can use the built-in `NukeVideo` module to render them.\n\nThe way you enable animations has also been updated and matches `AsyncImage`:\n\n```swift\n// Before (Nuke 11)\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\"))\n    .animation(.default)\n    \n// After (Nuke 12)\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\"),\n          transaction: .init(animation: .default)) {\n    $0.image\n}\n```\n\nAnd progress updates are no longer bundled with the rest of the state updates, which significantly reduces the number of `LazyImage` `content` reloads.\n\n```swift\n// Before (Nuke 11)\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\")) { state in \n    if state.isLoading {\n        Text(\"\\(state.progress.fraction * 100) %\")\n    }\n}\n    \n// After (Nuke 12)\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\")) { state in\n    if state.isLoading {\n        ProgressView(state.progress)\n    }\n}\n\nstruct ProgressView: View {\n    @ObservedObject var progress: FetchImage.Progress\n\n    var body: some View {\n        Text(\"progress.fraction * 100 %\")\n    }\n}\n```\n\n## Image Scale\n\n`ImageDecoder` now defaults to scale 1 for images (configurable using [UserInfoKey/scaleKey](https://kean-docs.github.io/nuke/documentation/nuke/imagerequest/userinfokey/scalekey/)).\n\n## NukeVideo\n\nTo enable video support, you'll now need to import the new `NukeVideo` framework to your project and register the video decoder.\n\n```swift\nImageDecoderRegistry.shared.register(ImageDecoders.Video.init)\n```\n\nHere is an example of playing the video using `LazyImageView` (NukeUI) and `VideoPlayerView` (NukeVideo):\n\n```swift\nlet imageView = LazyImageView()\nimageView.makeImageView = { container in\n    if let type = container.type, type.isVideo, let asset = container.userInfo[.videoAssetKey] as? AVAsset {\n        let view = VideoPlayerView()\n        view.asset = asset\n        view.play()\n        return view\n    }\n    return nil\n}\n\nimageView.url = /* video URL */\n```\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 13 Migration Guide.md",
    "content": "# Nuke 13 Migration Guide\n\nThis guide eases the transition of the existing apps that use Nuke 12.x to the latest version of the framework.\n\n## Minimum Requirements\n\nThe minimum supported platforms have been raised.\n\n- iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, visionOS 1.0\n- Xcode 26.0\n- Swift 6.2\n\n## DataLoading Protocol\n\nThe `DataLoading` protocol has been rewritten to use async/await and `AsyncThrowingStream` instead of callbacks. The `Cancellable` protocol has been removed.\n\n```swift\n// Before (Nuke 12)\npublic protocol DataLoading: Sendable {\n    func loadData(\n        with request: URLRequest,\n        didReceiveData: @escaping @Sendable (Data, URLResponse) -> Void,\n        completion: @escaping @Sendable (Error?) -> Void\n    ) -> any Cancellable\n}\n\npublic protocol Cancellable: AnyObject, Sendable {\n    func cancel()\n}\n\n// After (Nuke 13)\npublic protocol DataLoading: Sendable {\n    func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse)\n}\n```\n\nIf you have a custom `DataLoading` implementation, update it to return an `AsyncThrowingStream` that delivers data chunks, along with the initial `URLResponse`. The built-in `DataLoader` has been updated accordingly.\n\n## Typed Throws\n\n`ImageTask.image`, `ImageTask.response`, `ImagePipeline.image(for:)`, and `ImagePipeline.data(for:)` now use typed throws (`throws(ImagePipeline.Error)`). Two new error cases have also been added:\n\n- `ImagePipeline.Error.cancelled` — thrown when a task is cancelled (previously `CancellationError`)\n- `ImagePipeline.Error.dataDownloadExceededMaximumSize` — thrown when a download exceeds `Configuration.maximumResponseDataSize`\n\n```swift\n// Before (Nuke 12)\ndo {\n    let image = try await ImagePipeline.shared.image(for: url)\n} catch let error as ImagePipeline.Error {\n    // handle pipeline error\n} catch {\n    // handle other errors, including CancellationError\n}\n\n// After (Nuke 13)\ndo {\n    let image = try await ImagePipeline.shared.image(for: url)\n} catch {\n    // error is always ImagePipeline.Error\n    switch error {\n    case .cancelled: break\n    case .dataLoadingFailed: break\n    // ...\n    }\n}\n```\n\n## ImageRequest: Type-Safe Properties\n\nThree `userInfo` dictionary keys have been replaced with dedicated, type-safe properties on `ImageRequest`. The old keys are deprecated.\n\n```swift\n// Before (Nuke 12)\nvar request = ImageRequest(url: url)\nrequest.userInfo[.imageIdKey] = \"http://example.com/image.jpeg\"\nrequest.userInfo[.scaleKey] = 2.0 as Float\nrequest.userInfo[.thumbnailKey] = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n\n// After (Nuke 13)\nvar request = ImageRequest(url: url)\nrequest.imageID = \"http://example.com/image.jpeg\"\nrequest.scale = 2.0\nrequest.thumbnail = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n```\n\nNote that the existing `imageId` property has been renamed to `imageID` (uppercase \"ID\") and is now writable. The old `imageId` is deprecated.\n\n```swift\n// Before (Nuke 12)\nlet id: String? = request.imageId // read-only\n\n// After (Nuke 13)\nvar id: String? = request.imageID // read/write\n```\n\nThe `userInfo` dictionary type has also changed from `[UserInfoKey: Any]` to `[UserInfoKey: any Sendable]`.\n\n## ImagePipeline.Delegate (Renamed from ImagePipelineDelegate)\n\n`ImagePipelineDelegate` has been renamed to `ImagePipeline.Delegate` and is now defined as a nested type. A deprecated typealias is provided for backward compatibility, but you should update your code.\n\n```swift\n// Before (Nuke 12)\nfinal class MyDelegate: ImagePipelineDelegate { ... }\n\n// After (Nuke 13)\nfinal class MyDelegate: ImagePipeline.Delegate { ... }\n```\n\nSeveral soft-deprecated per-event delegate methods have been fully removed. Use `imageTask(_:didReceiveEvent:pipeline:)` instead:\n\n```swift\n// Before (Nuke 12) — these methods no longer exist\nfunc imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) { ... }\nfunc imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline) { ... }\nfunc imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline) { ... }\nfunc imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline) { ... }\nfunc imageTask(_ task: ImageTask, didCompleteWithResult result: Result<ImageResponse, ImagePipeline.Error>, pipeline: ImagePipeline) { ... }\n\n// After (Nuke 13)\nfunc imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) {\n    switch event {\n    case .started: break\n    case .progress(let progress): break\n    case .preview(let response): break\n    case .finished(let result): break\n    }\n}\n```\n\n## ImageTask.Event Changes\n\n`ImageTask.Event.cancelled` has been removed. Cancellation is now uniformly represented as a failure result. Additionally, a new `.started` case has been added.\n\n```swift\n// Before (Nuke 12)\npublic enum Event: Sendable {\n    case progress(Progress)\n    case preview(ImageResponse)\n    case cancelled\n    case finished(Result<ImageResponse, ImagePipeline.Error>)\n}\n\n// After (Nuke 13)\n@frozen public enum Event: Sendable {\n    case started\n    case progress(Progress)\n    case preview(ImageResponse)\n    case finished(Result<ImageResponse, ImagePipeline.Error>) // .cancelled → .finished(.failure(.cancelled))\n}\n```\n\nIf you were handling `.cancelled` explicitly, switch to checking for `.finished(.failure(.cancelled))`:\n\n```swift\n// Before (Nuke 12)\ncase .cancelled:\n    handleCancellation()\n\n// After (Nuke 13)\ncase .finished(.failure(.cancelled)):\n    handleCancellation()\n```\n\n## Configuration: Queue Types Changed\n\nThe pipeline's work queues have changed type from `OperationQueue` to `TaskQueue`, a new custom type synchronized on `ImagePipelineActor`. It preserves some of the existing API signatures.\n\n```swift\n// Before (Nuke 12)\nconfiguration.dataLoadingQueue.maxConcurrentOperationCount = 4\nconfiguration.imageDecodingQueue.maxConcurrentOperationCount = 2\n\n// After (Nuke 13)\nconfiguration.dataLoadingQueue = TaskQueue(maxConcurrentOperationCount: 4)\nconfiguration.imageDecodingQueue = TaskQueue(maxConcurrentOperationCount: 2)\n```\n\nThe deprecated `callbackQueue` and `dataCachingQueue` properties have been fully removed.\n\n## Configuration: New Properties\n\nSeveral new configuration properties have been added:\n\n- `progressiveDecodingInterval` — minimum interval between progressive decoding attempts (default: 0.5s)\n- `maximumResponseDataSize` — downloads exceeding this limit are cancelled automatically (default: 10% of physical memory, capped at 200 MB)\n- `maximumDecodedImageSize` — images whose decoded bitmap exceeds this limit are downscaled automatically (default: based on physical memory)\n\nIf you previously set no limits and want to preserve that behavior, set these to `nil`:\n\n```swift\nconfiguration.maximumResponseDataSize = nil\nconfiguration.maximumDecodedImageSize = nil\n```\n\n## Callback Closures: @MainActor @Sendable\n\nAll callback closures in the public API are now annotated with `@MainActor @Sendable`. This is a source-breaking change if you pass closures that are not already main-actor isolated.\n\n**ImagePipeline closure-based API:**\n\n```swift\n// Before (Nuke 12)\npipeline.loadImage(with: request) { result in\n    self.imageView.image = try? result.get().image\n}\n\n// After (Nuke 13) — closure is @MainActor, self access is safe\npipeline.loadImage(with: request) { result in\n    self.imageView.image = try? result.get().image\n}\n```\n\nThe closures are implicitly `@MainActor`, so capturing `self` without `[weak self]` will now produce a warning if `self` is not `@MainActor`. Update your closures accordingly.\n\n**NukeUI callbacks** (`FetchImage`, `LazyImage`, `LazyImageView`):\n\n```swift\n// Before (Nuke 12)\nlazyImageView.onSuccess = { response in\n    print(response.image)\n}\n\n// After (Nuke 13) — same syntax, but now @MainActor @Sendable\nlazyImageView.onSuccess = { response in\n    print(response.image)\n}\n```\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 4 Migration Guide.md",
    "content": "# Nuke 4 Migration Guide\n\nNuke 4 features **Swift 3 compatibility**, and has a lot of improvements both in the API and under the hood. As a major release, it introduces several API-breaking changes.\n\nThis guide is provided in order to ease the transition of existing applications using Nuke 3.x to the latest APIs, as well as explain the design and structure of new and changed functionality.\n\n## Requirements\n\n- iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0\n- Xcode 8\n- Swift 3\n\n> For those of you that would like to use Nuke on iOS 8.0 or macOS 10.9, please use the latest [tagged 3.x release](https://github.com/kean/Nuke/releases) which supports Swift 2.3.\n\n## Overview\n\nNuke 4 has fully adopted the new **Swift 3** changes and conventions, including the new [API Design Guidelines](https://swift.org/documentation/api-design-guidelines/).\n\nNuke 3 was already a slim framework. Nuke 4 takes it a step further by simplifying almost all of its components.\n\nHere's a few design principles adopted in Nuke 4:\n\n- **Protocol-Oriented Programming.** Nuke 3 promised a lot of customization by providing a set of protocols for loading, caching, transforming images, etc. However, those protocols were vaguely defined and hard to implement in practice. Protocols in Nuke 4 are simple and precise, often consisting of a single method.\n- **Single Responsibility Principle.** For example, instead of implementing preheating and deduplicating of equivalent requests in a single vague `ImageManager` class, those features were moved to separate classes (`Preheater`, `Deduplicator`). This makes core classes much easier to reason about.\n- **Principle of Least Astonishment**. Nuke 3 had several excessive protocols, classes and methods which are *all gone* now (`ImageTask`, `ImageManagerConfiguration` just to name a few). Those features are much easier to use now.\n- **Simpler Async**. Image loading involves a lot of asynchronous code, managing it was a chore. Nuke 4 adopts two design patterns ([**Promise**](https://github.com/kean/Promise) and **CancellationToken**) that solve most of those problems.\n\nThe adoption of those design principles resulted in a simpler, more testable, and more concise code base (which is now under 900 slocs, compared to AlamofireImage's 1426, and Kingfisher's whopping 2357).\n\nI hope that Nuke 4 is going to be a pleasure to use. Thanks for your interest 😄\n\n## New in Nuke 4\n\n### LRU Memory Cache\n\nNuke 4 features a new custom LRU memory cache which replaced `NSCache`. The primary reason behind this change was the fact that `NSCache` is not LRU. The new `Nuke.Cache` has some other benefits like better performance, and more control which would enable some new advanced features in future versions.\n\n### Rate Limiter\n\nThere is [a known problem](https://github.com/kean/Nuke/issues/59) with `URLSession` that it gets trashed pretty easily when you resume and cancel `URLSessionTasks` at a very high rate (say, scrolling a large collection view with images). Some frameworks combat this problem by simply never cancelling `URLSessionTasks` which are already in `.running` state. This is not an ideal solution, because it forces users to wait for cancelled requests for images which might never appear on the display.\n\nNuke has a better, classic solution for this problem - it introduces a new `RateLimiter` class which limits the rate at which `URLSessionTasks` are created. `RateLimiter` uses a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm. The implementation supports quick bursts of requests which can be executed without any delays when \"the bucket is full\". This is important to prevent the rate limiter from affecting \"normal\" requests flow. `RateLimiter` is enabled by default.\n\nYou can see `RateLimiter` in action in a new `Rate Limiter Demo` added in the sample project.\n\n### Toucan Plugin\n\nMake sure to check out new [Toucan plugin](https://github.com/kean/Nuke-Toucan-Plugin) which provides a simple API for processing images. It supports resizing, cropping, rounded rect masking and more.\n\n## Changes in Nuke 4\n\nAlmost every API in Nuke has been modified in some way. It's impossible to document every single change, so here's a list of some of the major and mostly user-visible changes.\n\n### Basics\n\n#### Drop `Image` Prefix\n\n```swift\nImageRequest -> Request\nImageLoading -> Loading\nImageMemoryCaching -> Caching\nImageDataLoading -> DataLoading\nImageDiskCaching -> DataCaching\nImageDecompressor -> DataDecompressor\n... etc\n```\n\n#### Loading Images into Targets\n\nInstead of adding extensions to UI components Nuke now has a `Manager` class (similar to [Picasso](https://github.com/square/picasso)) which loads images into specific targets (see new `Target` protocol which replaced `ImageLoadingView` and `ImageDisplayingView` protocols).\n\n```swift\n// Nuke 3\nlet request = ImageRequest(URLRequest: NSURLRequest(NSURL(URL: \"http://...\")!))\nimageView.nk_setImageWith(request)\n\n// Nuke 4\nlet request = Request(urlRequest: URLRequest(url: URL(string: \"http://...\")!))\nNuke.loadImage(with: request, into: imageView)\n\n// Nuke 4 (NEW)\n// Use custom handler, target doesn't have to implement `Target` protocol.\nNuke.loadImage(with: request, into: imageView) { response, isFromMemoryCache in\n    // Handle response\n}\n```\n\nThere are many reasons behind the change, just to name a few:\n\n- `Manager` class has context about all the requests per all targets (or just targets per screen if you create a `Manager` per screen). It will allow to add features like: _lower the priority of the requests when the UIVC goes off screen_ - something that works really well in practice.\n- `ImageView` no longer \"loads images into itself\". So Nuke doesn't break MVC.\n- No need to prefix methods.\n- Nuke 3 had elaborate `ImageLoadingView` and `ImageDisplayingView` protocols. They had lots of methods, some implemented by default, some added in extensions. It was a mess. New `Manager` -> `Target` relation is super simple and feels natural.\n- If you want to use custom manager for a specific target you no longer have to inject it anywhere - just use it.\n\nAdding extensions to `UIImageView` that would do something as complicated as loading images is an abuse of extensions. The reason why other frameworks do this is because this is how it was initially implemented in `SDWebImage`.\n\n#### Request\n\nMemory caching options were simplified to a single struct nested in a `Request`.\n\n```swift\n// Nuke 3\npublic enum ImageRequestMemoryCachePolicy {\n    case ReturnCachedImageElseLoad\n    case ReloadIgnoringCachedImage\n}\n\npublic struct ImageRequest {\n    public var memoryCacheStorageAllowed = true\n    public var memoryCachePolicy = ImageRequestMemoryCachePolicy.ReturnCachedImageElseLoad\n}\n\n// Nuke 4\npublic struct Request {\n    public struct MemoryCacheOptions {\n        public var readAllowed = true\n        public var writeAllowed = true\n    }\n    public var memoryCacheOptions = MemoryCacheOptions()\n}\n```\n\nInstead of providing a `shouldDecompressImage`, `contentMode`, `targetSize`  property `Request` now simply sets `Decompressor` as a default processor.\n\n```swift\n// Nuke 3\npublic struct ImageRequest {\n    public var processor: ImageProcessing?\n\n    public var targetSize: CGSize = ImageMaximumSize\n    public var contentMode: ImageContentMode = .AspectFill\n    public var shouldDecompressImage = true\n}\n\n// Nuke 4\npublic struct Request {\n    public var processor: AnyProcessor? = AnyProcessor(Decompressor())\n}\n```\n\nAdding processors to the request is now easier.\n\n```swift\n// Nuke 4 (NEW)\nrequest.process(with: GaussianBlur())\n```\n\nYou can now customize cache (used for memory caching) and load (used for deduplicating equivalent requests) keys using `Request`.\n\n```swift\n// Nuke 4 (NEW)\npublic struct Request {\n    public var loadKey: AnyHashable?\n    public var cacheKey: AnyHashable?\n}\n```\n\n#### Transformations\n\n`Processing` protocol is now `Equatable`, `func isEquivalent(other: ImageProcessing) -> Bool` was removed. Nuke now uses [type erasure](http://www.russbishop.net/type-erasure) here and in some other places.\n\n```swift\n// Nuke 3\npublic protocol ImageProcessing {\n    func process(image: Image) -> Image?\n    func isEquivalent(other: ImageProcessing) -> Bool\n}\n\n// Nuke 4\npublic protocol Processing: Equatable {\n    func process(_ image: Image) -> Image?\n}\n```\n\n#### Targets\n\nNew `Target` protocol replaced `ImageLoadingView` and `ImageDisplayingView` which had a lot of methods with a default implementation and were very confusing. New protocol on the other hand consists of a single method:\n\n```swift\npublic protocol Target: class {\n    /// Callback that gets called when the request gets completed.\n    func handle(response: Response, isFromMemoryCache: Bool)\n}\n```\n\n#### Preheating\n\nPreheating was moved from `ImageManager` to a separate `Preheater` class. You might create a preheater instance per screen.\n\n```swift\n// Nuke 3\nlet requests = [ImageRequest(URL: imageURL1), ImageRequest(URL: imageURL2)]\nNuke.startPreheatingImages(requests: requests)\nNuke.stopPreheatingImages(requests: requests)\n\n// Nuke 4\nlet preheater = Preheater()\nlet requests = [Request(url: url1), Request(url: url2), ...]\npreheater.startPreheating(for: requests)\npreheater.stopPreheating(for: requests)\n```\n\n#### Accessing Memory Cache\n\nYou used to have to use `ImageManager` to access memory cache. Now it can be used directly (due to simplified keys management, check out private `Request.Key` if you want to know more).\n\n```swift\n// Nuke 3\nlet manager = ImageManager.shared\nlet request = ImageRequest(URL: NSURL(string: \"\")!)\nlet response = ImageCachedResponse(image: UIImage(), userInfo: nil)\nmanager.storeResponse(response, forRequest: request)\nlet cachedResponse = manager.cachedResponseForRequest(request)\n\n// Nuke 4\nlet cache = Cache.shared\nlet request = Request(url: URL(string: \"\")!)\ncache[request] = UIImage()\nlet image = cache[request]\n```\n\n### Advanced\n\n#### Loading Images directly\n\nIf you do have to load images directly (without using `Manager` and `Target`):\n\n```swift\n// Nuke 3\nlet task = Nuke.taskWith(NSURL(URL: \"http://...\")!) {\n    let image: Image? = $0.image\n}\ntask.resume()\ntask.cancel()\n\n// Nuke 4\nlet cts = CancellationTokenSource()\nLoader.shared.loadImage(with: URL(string: \"http://...\")!, token: cts.token)\n    .then { image in print(\"\\(image) loaded\") }\n    .catch { error in print(\"caught \\(error)\") }\ncts.cancel()\n```\n\n#### Redesigned Protocols\n\nProtocols in Nuke 4 are simple and precise, often consisting of a single method.\n\n```swift\n// Nuke 4\n\npublic protocol Loading {\n    func loadImage(with request: Request, token: CancellationToken?) -> Promise<Image>\n}\n\npublic protocol DataLoading {\n    func loadData(with request: URLRequest, token: CancellationToken?) -> Promise<(Data, URLResponse)>\n}\n\npublic protocol DataCaching {\n    func response(for request: URLRequest, token: CancellationToken?) -> Promise<CachedURLResponse>\n    func setResponse(_ response: CachedURLResponse, for request: URLRequest)\n}\n\npublic protocol DataDecoding {\n    func decode(data: Data, response: URLResponse) -> Image?\n}\n\npublic protocol Processing: Equatable {\n    func process(_ image: Image) -> Image?\n}\n\npublic protocol Caching: class {\n    subscript(key: AnyHashable) -> Image? { get set }\n}\n```\n\n#### Adopt AnyHashable\n\nAdopt `AnyHashable` instead of `ImageRequestKey` (which was renamed to `Request.Key` and made private).\n\n## Removed in Nuke 4\n\n### Request Priority\n\nPriority was removed temporarily from `Request` because it wasn't performing as well as expected. There should be a better way to implement it.\n\n### Progress Handler\n\nProgress handler was temporarily removed from `Request`. I'm still on the fence whether this feature should be included in the framework itself. It might be better handled by notification implemented in a specific `DataLoader`. \n\nYou can always just display an activity indicator instead:\n\n```swift\nlet indicator = activityIndicator(for: cell)\n\nindicator.startAnimating()\n    Nuke.loadImage(with: request, into: imageView) { [weak imageView] in\n    imageView?.handle(response: $0, isFromMemoryCache: $1)\n    indicator.stopAnimating()\n}\n```\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 5 Migration Guide.md",
    "content": "# Nuke 5 Migration Guide\n\nThis guide is provided in order to ease the transition of existing applications using Nuke 4.x to the latest APIs, as well as explain the design and structure of new and changed functionality.\n\n## Requirements\n\n- iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0\n- Xcode 8\n- Swift 3\n\n## Overview\n\nNuke 5 is a relatively small release which removes some of the complexity from the framework. Hopefully it will make *contributing* to Nuke easier.\n\nOne of the major changes is the removal of promisified API as well as `Promise` itself. Promises were briefly added in Nuke 4 as an effort to simplify async code. The major downsides of promises are complex memory management, extra complexity for users unfamiliar with promises, complicated debugging, performance penalties. Ultimately I decided that promises were adding more problems than they were solving. \n\nChances are that changes made in Nuke 5 are not going to affect your code.\n\n## Changes\n\n### Removed promisified API and `Promise` itself\n\n> - Remove promisified API, use simple closures instead. For example, `Loading` protocol's method `func loadImage(with request: Request, token: CancellationToken?) -> Promise<Image>` was replaced with a method with a completion closure `func loadImage(with request: Request, token: CancellationToken?, completion: @escaping (Result<Image>) -> Void)`. The same applies to `DataLoading` protocol.\n> - Remove `Promise` class\n> - Remove `PromiseResolution<T>` enum\n> - Remove `Response` typealias\n> - Add `Result<T>` enum which is now used as a replacement for `PromiseResolution<T>` (for instance, in `Target` protocol, etc)\n\n- If you've used promisified APIs you should replace them with a new closure-based APIs. If you still want to use promisified APIs please use [PromiseKit](https://github.com/mxcl/PromiseKit) or some other promise library to wrap Nuke APIs.\n- If you've provided a custom `Loading` or `DataLoading` protocols you should update them to a new closure-based APIs.\n- Replace `PromiseResolution<T>` with `Result<T>` where necessary (custom `Target` conformances, custom `Manager.Handler`).\n\n### Memory cache is now managed exclusively by `Manager`\n\n> - Remove memory cache from `Loader`\n> - `Manager` now not only reads, but also writes to `Cache`\n> - `Manager` now has new methods to load images w/o target (Nuke 5.0.1)\n\n- If you're not constructing a custom `Loader` and you're not using it directly this change doesn't affect you\n- If you're using custom `Loader` directly and rely on its memory caching, please use the new `Manager` APIs that load images w/o target\n- If you're constructing a custom `Loader` but don't use it directly then simply update to a new initializer which no longer requires you to pass memory cache in\n\n### Removed `DataCaching` and `CachingDataLoader`\n\n- Instead of using those types you'll need to wrap `DataLoader` by yourself. For more info see [Third Party Libraries: Using Other Caching Libraries](https://github.com/kean/Nuke/blob/5.0/Documentation/Guides/Third%20Party%20Libraries.md#using-other-caching-libraries). \n\n### Other Changes\n\nMake sure that you take those minor changes into account to:\n\n> - `Loader` constructor now provides a default value for `DataDecoding` object\n> - `DataLoading` protocol now works with a `Nuke.Request` and not `URLRequest` in case some extra info from `URLRequest` is required\n> - Reduce default `URLCache` disk capacity from 200 MB to 150 MB\n> - Reduce default `maxConcurrentOperationCount` of `DataLoader` from 8 to 6.\n> - Shared objects (like `Manager.shared`) are now constants.\n> - `Preheater` is now initialized with `Manager` instead of `Loading` object\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 6 Migration Guide.md",
    "content": "# Nuke 6 Migration Guide\n\nThis guide is provided in order to ease the transition of existing applications using Nuke 5.x to the latest APIs, as well as explain the design and structure of new and changed functionality.\n\n## Requirements\n\n- iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0\n- Xcode 9\n- Swift 4\n\n## Overview\n\nNuke 6 has a relatively small number of changes in the public API, chances are most of them are not going to affect your projects. Most of the deprecated APIs are kept in the project to ease the transition, however, they are going to be removed fairly soon.\n\nThere were a lot of implementation details leaking into the public API in Nuke 5 (e.g. `Deduplicator` class, scheduling infrastructure) which were all made private in Nuke 6. If you were using any of those APIs you can always ping me with your questions on [Twitter](https://twitter.com/a_grebenyuk).\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 7 Migration Guide.md",
    "content": "# Nuke 7 Migration Guide\n\nThis guide is provided in order to ease the transition of existing applications using Nuke 6.x to the latest APIs, as well as explain the design and structure of new and changed functionality.\n\nThis migration guide is still work in progress, the finished version is going to be available when Nuke 7 is finally released.\n\n## Requirements\n\n- iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0\n- Xcode 9.2\n- Swift 4.0\n\n## Overview\n\nNuke 7 is the biggest release yet. It contains more features and refinements than all of the previous releases combined. There are a lot of new APIs in Nuke 7, fortunately, it's almost completely source-compatible with Nuke 6. \n\n> Source-compatibility was removed in [Nuke 7.5](https://github.com/kean/Nuke/releases/tag/7.5). The latest source-compatible release is [Nuke 7.4.2](https://github.com/kean/Nuke/releases/tag/7.4.2). The best way to migrate would be to either upgrade to Nuke 7.4.2 first, or to drop this [Deprecated.swift](https://gist.github.com/kean/a14ca485ce72bef0e50cbb2f36ec7d91) into your project and follow the instructions from the warnings.\n\nMost of the new APIs have `Image*` prefix. Some of the types with `Image*` prefix are new (e.g. `ImagePipeline` which replaced `Manager` and `Loader`), some were just renamed (e.g. `ImageRequest` instead of `Request`), and some are reimagining of old APIs (e.g. `ImageDecoding` instead of `Decoding`).\n\nIf you're using a deprecated API you're going to see a deprecation message with a suggestion which new API you should use instead. All of the deprecated APIs work exactly as they used to in the previous versions. The only exception is `DataLoading` protocol which was replaced with a new version, but most of the apps are not using it directly.\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 8 Migration Guide.md",
    "content": "# Nuke 8 Migration Guide\n\nThis guide is provided in order to ease the transition of existing applications using Nuke 7.x to the latest version, as well as explain the design and structure of new and changed functionality.\n\n> To learn about the new features in Nuke 8 see the [release notes](https://github.com/kean/Nuke/releases/tag/8.0).\n\n## Updated Minimum Requirements\n\n- iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0\n- Xcode 10.2\n- Swift 5.0\n\n## Overview\n\nNuke 8 contains a bunch of new features, refinements, and performance improvements. The default pipeline works exactly the same as in the previous version. The release is mostly source compatible with Nuke 7. The deprecated APIs were added to [Deprecated.swift](https://gist.github.com/kean/05eaa36ac72e4c34dea50911ee68b801) file where every declaration has a comment which guides you through migration. There are still some breaking changes which might affect you which are covered in this guide.\n\n> The deprecated APIs are going to be removed 6 months after the release. If by the time you upgrade to Nuke 8, the deprecated APIs are already removed, you can temporarily drop the [Deprecated.swift](https://gist.github.com/kean/05eaa36ac72e4c34dea50911ee68b801) into your project to ease the migration.\n\n## `Result` Type\n\n`ImageTask.Completion` closure now uses native `Result` type.\n\n**Before:**\n\n```swift\npublic typealias Completion = (Nuke.ImageResponse?, Nuke.ImagePipeline.Error?) -> Void\n```\n\n**After:**\n\n```swift\npublic typealias Completion = (Result<Nuke.ImageResponse, Nuke.ImagePipeline.Error>) -> Void\n```\n\nYou need to update all the place where you were using the completion closures.\n\n```swift\n// Before:\npipeline.loadImage(with: url) { response, error in\n    if let response = response {\n        // handle response\n    } else {\n        // handle error (optional)\n    }\n}\n\n// After:\npipeline.loadImage(with: url) { result in\n    switch result {\n    case let .success(response):\n        // handle response\n    case let .failure(error):\n        // handle error (non optional)\n    }\n}\n```\n\n```swift\n// Before:\npipeline.loadImage(with: url) { _, _ in }\n\n// After:\npipeline.loadImage(with: url) { _ in }\n```\n\n## `ImageProcessing` Protocol\n\n> **Affects you if you have any custom image processors.**\n\nThe `ImageProcessing` protocol was changed to support the new feature in Nuke 8 - [Caching Processed Images](https://github.com/kean/Nuke/pull/227). In order to generate cache keys, each processor now must return a unique string identifier. Instead of conforming to `Equatable` protocol, each processor now must also return a `hashableIdentifier` (`AnyHashable`) to be used by the memory cache for which string manipulations would be unacceptably slow.\n\n**Before:**\n\n```swift\npublic protocol ImageProcessing: Equatable {\n    func process(image: Image, context: ImageProcessingContext) -> Image?\n}\n```\n\n**After:**\n\n```swift\npublic protocol ImageProcessing {\n    func process(image: Image, context: ImageProcessingContext?) -> Image?\n    var identifier: String { get }\n    var hashableIdentifier: AnyHashable { get }\n}\n```\n\nAn example of migrating a custom processor.\n\n**Before:**\n\n```swift\nstruct GaussianBlur: ImageProcessing {\n    let radius: Int\n\n    func process(image: Image, context: ImageProcessingContext) -> Image? {\n    \treturn /* create blurred image */\n    }\n}\n```\n\n**After:**\n\n```swift\nstruct GaussianBlur: ImageProcessing, Hashable {\n    let radius: Int\n\n    func process(image: Image, context: ImageProcessingContext?) -> Image? {\n    \treturn /* create blurred image */\n    }\n\n    // Prefer to use reverse DNS notation.\n    var identifier: String { return \"com.youdomain.processor.gaussianblur-\\(radius)\" }\n    var hashableIdentifier: AnyHashable { return self }\n}\n```\n\n## `AnyImageProcessor`\n\n> **Affects you if you are explicitly using `AnyImageProcessor` struct.**\n\n`AnyImageProcessor` was removed because it was no longer needed anymore. Anywhere where you used `AnyImageProcessor` before, you should now be able to pass the processor directly.\n\n\n## `ImageDisplaying` Protocol\n\n> **Affects you if you are using `ImageDisplaying` protocol directly.**\n\n`ImageDisplaying` protocol was a pure @objc protocol which didn't have any prefixes which meant that it could result in collisions with other methods/protocols in ObjC runtime. In order to reduce the chance of collision, the `Nuke_` prefixes were added in Nuke 8.\n\n**Before:**\n\n```swift\n@objc public protocol ImageDisplaying {\n    @objc func display(image: Nuke.Image?)\n}\n```\n\n**After:**\n\n```swift\n@objc public protocol Nuke_ImageDisplaying {\n    @objc func nuke_display(image: Image?)\n}\n```\n\nYou need to update these protocols and methods to the new ones.\n\n\n"
  },
  {
    "path": "Documentation/Migrations/Nuke 9 Migration Guide.md",
    "content": "# Nuke 9 Migration Guide\n\nThis guide is provided in order to ease the transition of existing applications using Nuke 8.x to the latest version, as well as explain the design and structure of new and changed functionality.\n\n> To learn about the new features in Nuke 9 see the [release notes](https://github.com/kean/Nuke/releases/tag/9.0.0).\n\n## Minimum Requirements\n\n- iOS 11.0, tvOS 11.0, macOS 10.13, watchOS 4.0\n- Xcode 11.0\n- Swift 5.1\n\n## Overview\n\nNuke 9 contains a ton of new features, refinements, and performance improvements. There are some breaking changes and deprecations which the compiler is going to guide you through as you update.\n\n## ImageProcessing\n\nIf you have custom image processors (`ImageProcessing` protocol), update `process(image:)` method to use the new signature. There are now two levels of image processing APIs: the basic and the advanced one. Please implement the one that best fits your needs.\n\n```swift\n// Nuke 8\nfunc process(_ image: PlatformImage, context: ImageProcessingContext?) -> PlatformImage?\n\n// Nuke 9\nfunc process(_ image: UIImage) -> UIImage? // NSImage on macOS\n// Optional\nfunc process(_ image container: ImageContainer, context: ImageProcessingContext) -> ImageContainer?\n```\n\n## ImageDecoding\n\n```swift\n// Nuke 8\npublic protocol ImageDecoding {\n    func decode(data: Data, isFinal: Bool) -> PlatformImage?\n}\n\n// Nuke 9\npublic protocol ImageDecoding {\n    func decode(_ data: Data) -> ImageContainer?\n    // Optional\n    func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer?\n}\n\npublic protocol ImageDecoderRegistering: ImageDecoding {\n    init?(data: Data, context: ImageDecodingContext)\n    // Optional\n    init?(partiallyDownloadedData data: Data, context: ImageDecodingContext)\n}\n```\n\n## ImageEncoding\n\nIf you have custom encoders (`ImageEncoding` protocol), update `encode(image:)` method to use the new signature. There are now two levels of image encoding APIs: the basic and the advanced one. Please implement the one that best fits your needs.\n\n```swift\n// Nuke 8\npublic protocol ImageEncoding {\n    func encode(image: PlatformImage) -> Data?\n}\n\n// Nuke 9\npublic protocol ImageEncoding {\n    func encode(_ image: PlatformImage) -> Data?\n    // Optional\n    func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data?\n}\n```\n\n## ImageCaching\n\n`ImageCaching` was updated to use `ImageContainer` type. Individual methods were replaced with a subscript.\n\n```swift\n// Nuke 8\npublic protocol ImageCaching: AnyObject {\n    /// Returns the `ImageResponse` stored in the cache with the given request.\n    func cachedResponse(for request: ImageRequest) -> ImageResponse?\n\n    /// Stores the given `ImageResponse` in the cache using the given request.\n    func storeResponse(_ response: ImageResponse, for request: ImageRequest)\n\n    /// Remove the response for the given request.\n    func removeResponse(for request: ImageRequest)\n}\n\n// Nuke 9\npublic protocol ImageCaching: AnyObject {\n    subscript(request: ImageRequest) -> ImageContainer?\n}\n"
  },
  {
    "path": "Documentation/Nuke.docc/Customization/ImageFormats/image-decoding.md",
    "content": "# Image Decoding\n\n## ImageDecoding Protocol\n\nAt the core of the decoding infrastructure is the ``ImageDecoding`` protocol.\n\n```swift\npublic protocol ImageDecoding: Sendable {\n    /// Returns `true` if you want the decoding to be performed on the\n    /// decoding queue. If `false`, decoding is performed synchronously\n    /// on the pipeline operation queue. By default, `true`.\n    var isAsynchronous: Bool { get }\n\n    /// Produces an image from the given image data.\n    func decode(_ data: Data) throws -> ImageContainer\n\n    /// Produces an image from the given partially downloaded image data.\n    /// This method might be called multiple times during a single decoding\n    /// session. When the image download is complete, ``decode(_:)`` is called.\n    ///\n    /// - returns: nil by default.\n    func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer?\n}\n```\n\n``ImageContainer`` is a struct that wraps the decoded image and (optionally) the original data and some additional information. The decoder decides what to attach to the container.\n\n```swift\npublic struct ImageContainer {\n    /// Either `UIImage` or `NSImage`, depending on the platform.\n    public var image: PlatformImage\n    /// An image type.\n    public var type: AssetType?\n    /// Returns `true` if the image is a preview (progressive scan, thumbnail).\n    public var isPreview: Bool\n    /// Contains the original image data, but only if the decoder attaches it.\n    public var data: Data?\n    /// Metadata provided by the user.\n    public var userInfo: [UserInfoKey: Any]\n}\n```\n\nWhen the first chunk of the image data is loaded, ``ImagePipeline`` creates a decoder for the given image format.\n\nThe pipeline uses ``ImageDecoderRegistry`` to find the decoder.  The decoder is created once and is reused across a single image decoding session until the final chunk of data is downloaded. If the decoder supports progressive decoding, make it a `class` to retain state within a single decoding session.\n\n> ``ImageDecoding/decode(_:)`` method only passes `data` to the decoder. If the decoder needs additional information, pass it when instantiating it. ``ImageDecodingContext`` provides everything that you might need, including the full ``ImageRequest``.\n>\n> You can also take advantage of ``ImageRequest/userInfo``. For example, you may pass the target image view size to the SVG decoder to let it know the size of the image to create.\n\nThe decoding is performed in the background on the operation queue provided in ``ImagePipeline/Configuration-swift.struct``. There is always only one decoding request at a time. The pipeline doesn't call ``ImageDecoding/decodePartiallyDownloadedData(_:)-9budu`` again until you are finished with the previous chunk.\n\n## Registering Decoders\n\nTo register the decoders, use ``ImageDecoderRegistry``.\n\n```swift\nfunc register() {\n    ImageDecoderRegistry.shared.register(ImageDecoders.SVG.init)\n}\n\nextension ImageDecoders {\n    final class SVG: ImageDecoding {\n        init?(context: ImageDecodingContext) {\n            guard context.isCompleted else {\n                return nil // No progressive decoding\n            }\n\n            let isSVG = context.urlResponse?.url?.absoluteString.hasSuffix(\".svg\") ?? false\n            guard isSVG else {\n                return nil // Image format isn't supported.\n            }   \n        }\n    }\n}\n```\n\n> Tip: To determine image type, use an ``AssetType`` initializer that takes data as input. ``AssetType`` represents uniform type identifiers or UTI.\n\nWhen you register a decoder, you have access to ``ImageDecodingContext`` for the given decoding session.\n\n## Rendering Engines\n\nThe decoders in Nuke work at download time - regular decoders produce images as data arrives, while progressive decoders can produce multiple previews before delivering the final images. But there are scenarios when decoding at download time doesn't work: for example, for animated images.\n\nFor animated images, it is not feasible to decode all of the frames and put them in memory as bitmaps at download time – it will consume too much memory. You have to postpone decoding to rendering time. When the image is displayed, a rendering engine, like [Gifu](https://github.com/kaishin/Gifu) or others, will decode and cache image frames on demand.\n\n> GIF is not an efficient format. It is recommended to use short MP4 clips instead. See [Nuke Demo](https://github.com/kean/NukeDemo) for an example.\n\n## Built-In Image Decoders\n\nYou can find all of the built-in decoders in the ``ImageDecoders`` namespace.\n\n### ImageDecoders.Default\n\n``ImageDecoders/Default`` is used by default if no custom decoders are registered. It uses native `UIImage(data:)` (and `NSImage(data:)`) initializers to create images from data.\n\nThe default ``ImageDecoders/Default`` also supports progressive decoding via `CGImageSourceCreateIncremental`. It produces previews as data arrives, gated by ``ImagePipeline/PreviewPolicy`` (`.incremental` for progressive JPEGs and GIFs by default, `.disabled` for other formats).\n\n### ImageDecoders.Video \n\nGenerates a video preview and attaches downloaded data to the image container.\n\n### ImageDecoders.Empty\n\n``ImageDecoders/Empty`` returns an empty placeholder image and attaches image data to the image container. It could also be configured to return partially downloaded data. ``ImageDecoders/Empty`` can be used when the rendering engine works directly with image data.\n"
  },
  {
    "path": "Documentation/Nuke.docc/Customization/ImageFormats/image-encoding.md",
    "content": "# Image Encoding\n\nTo encode images, use types conforming to the ``ImageEncoding`` protocol:\n\n```swift\npublic protocol ImageEncoding: Sendable {\n    /// Encodes the given image.\n    func encode(_ image: PlatformImage) -> Data?\n\n    /// An optional method which encodes the given image container.\n    func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data?\n}\n```\n\nThere is currently no dedicated image encoder registry. Use the pipeline configuration to register custom encoders using ``ImagePipeline/Configuration-swift.struct/makeImageEncoder``.\n\n## Built-In Image Encoders\n\nYou can find all of the built-in encoders in the ``ImageEncoders`` namespace.\n\n### ImageEncoders.Default\n\n``ImageEncoders/Default`` encodes opaque images as `jpeg` and images with opacity as `png`. It can also be configured to use `heif` instead of `jpeg` using ``ImageEncoders/Default/isHEIFPreferred`` option.\n\n### ImageEncoders.ImageIO\n\n``ImageEncoders/ImageIO`` is an [Image I/O](https://developer.apple.com/documentation/imageio) based encoder.\n \nImage I/O is a system framework that allows applications to read and write most image file formats. This framework offers high efficiency, color management, and access to image metadata.\n\n```swift\nlet image: UIImage\nlet encoder = ImageEncoders.ImageIO(type: .heif, compressionRatio: 0.8)\nlet data = encoder.encode(image: image)\n```\n"
  },
  {
    "path": "Documentation/Nuke.docc/Customization/ImageFormats/image-formats.md",
    "content": "# Image Formats\n\nLearn about image formats supported in Nuke and how to extend them.\n\n## Overview\n\nNuke has built-in support for basic image formats like `jpeg`, `png`, and `heif`. It also has the infrastructure for supporting a variety of custom image formats.\n\nNuke can drive progressive decoding, animated image rendering, progressive animated image rendering, drawing vector images directly or converting them to bitmaps, parsing thumbnails included in the image containers, and more.\n\n## Topics\n\n### Supported Images\n\n- <doc:supported-image-formats>\n- ``PlatformImage``\n- ``AssetType``\n\n### Decoding\n\n- <doc:image-decoding>\n- ``ImageDecoding``\n- ``ImageDecoders``\n- ``ImageDecodingError``\n- ``ImageDecodingContext``\n- ``ImageDecoderRegistry``\n\n### Encoding\n\n- <doc:image-encoding>\n- ``ImageEncoding``\n- ``ImageEncoders``\n- ``ImageEncodingContext``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Customization/ImageFormats/supported-image-formats.md",
    "content": "# Supported Formats\n\nNuke has built-in support for basic image formats like `jpeg`, `png`, and `heif`. It also has the infrastructure for supporting a variety of custom image formats.\n\nNuke is capable of driving progressive decoding, animated image rendering, progressive animated image rendering, drawing vector images directly or converting them to bitmaps, parsing thumbnails included in the image containers, and more.\n\n### Common Image Formats\n\nAll image formats natively supported by the platform are also supported by Nuke, including `PNG`, `TIFF`, `JPEG`, `GIF`, `BMP`, `ICO`, `CUR`, `XBM`, `HEIF`, and `WebP` (iOS 14+).\n\nYou can use the basic `UIImageView`/`NSImageView`/`WKInterfaceImage` to render the images of any of the natively supported formats.\n\n### Progressive JPEG\n\n**Decoding**\n\n``ImageDecoders/Default`` supports progressive JPEG via `CGImageSourceCreateIncremental`. When ``ImagePipeline/Configuration-swift.struct/isProgressiveDecodingEnabled`` is `true`, the pipeline produces previews as data arrives.\n\nBy default, progressive previews are only enabled for progressive JPEGs and GIFs (``ImagePipeline/PreviewPolicy``). Baseline JPEGs, PNGs, and other formats produce no previews unless explicitly configured via ``ImagePipeline/Delegate/previewPolicy(for:pipeline:)``.\n\nFor progressive JPEGs with large EXIF headers where `CGImageSourceCreateIncremental` fails to produce incremental previews, the decoder automatically falls back to generating a thumbnail from the available data.\n\n**Encoding**\n\nNone.\n\n**Rendering**\n\nTo render progressive JPEG, you can use the basic `UIImageView`/`NSImageView`/`WKInterfaceImage`. The default image view loading extensions also support displaying progressive previews.\n\n### HEIF\n\n**Decoding**\n\n``ImageDecoders/Default`` supports [HEIF](https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format).\n\n**Encoding**\n\n``ImageEncoders/Default`` supports [HEIF](https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format) but doesn't use it by default. To enable it, use ``ImageEncoders/Default/isHEIFPreferred``.\n\nYou can use ``ImageEncoders/ImageIO`` directly:\n\n```swift\nlet image: UIImage\nlet encoder = ImageEncoders.ImageIO(type: .heif, compressionRatio: 0.8)\nlet data = encoder.encode(image: image)\n```\n\n**Rendering**\n\nTo render HEIF images, you can use `UIImageView`/`NSImageView`/`WKInterfaceImage`.\n\n### GIF\n\n**Decoding**\n\n``ImageDecoders/Default`` automatically recognizes GIFs. It creates an image container (``ImageContainer``) with the first frame of the GIF as a placeholder and attaches the original image data to the container so that you can perform just-in-time decoding at rendering time.\n\n**Encoding**\n\nNone.\n\n**Rendering**\n\nTo render animated GIFs, please consider using one of the open-source GIF rendering engines, like [Gifu](https://github.com/kaishin/Gifu), [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage), or other.\n\n**Gifu Example**\n\n```swift\n/// A custom image view that supports downloading and displaying animated images.\nfinal class ImageView: UIView {\n    private let imageView: GIFImageView\n    private let spinner: UIActivityIndicatorView\n    private var task: ImageTask?\n\n    /* Initializers skipped */\n\n    func setImage(with url: URL) {\n        prepareForReuse()\n\n        if let response = ImagePipeline.shared.cache[url] {\n            imageView.display(response: response)\n            if !response.isPreview {\n                return \n            }\n        }\n\n        spinner.startAnimating()\n        task = ImagePipeline.shared.loadImage(with: url) { [weak self] result in\n            self?.spinner.stopAnimating()\n            if case let .success(response) = result {\n                self?.imageView.display(response: response)\n            }\n        }\n    }\n    \n    private func display(response: ImageResponse) {\n        if let data = response.container.data {\n            animate(withGIFData: data)\n        } else {\n            image = response.image\n        }\n    }\n    \n    private func prepareForReuse() {\n        task?.cancel()\n        spinner.stopAnimating()\n        imageView.prepareForReuse()\n    }\n}\n```\n\nTo see this code in action, check out the [demo project](https://github.com/kean/NukeDemo).\n\n> `GIF` is not the most efficient format for transferring and displaying animated images. Consider using [short videos instead](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/replace-animated-gifs-with-video/). You can find a PoC available in the [demo project](https://github.com/kean/NukeDemo) that uses Nuke to load, cache and display an `MP4` video.\n\n\n### SVG\n\n**Decoding**\n\nThere is currently no built-in support for SVG. Use ``ImageDecoders/Empty`` to pass the original image data to an SVG-enabled view and render it using an external mechanism.\n\n**Encoding**\n\nNone.\n\n**Rendering**\n\nTo render SVG, consider using [SwiftSVG](https://github.com/mchoe/SwiftSVG), [SVG](https://github.com/SVGKit/SVGKit), or other frameworks. Here is an example of `SwiftSVG` rendering vector images.\n\n```swift\nImageDecoderRegistry.shared.register { context in\n    // Replace this with whatever works for you. There are no magic numbers\n    // for SVG like are used for other binary formats, it's just XML.\n    let isSVG = context.urlResponse?.url?.absoluteString.hasSuffix(\".svg\") ?? false\n    return isSVG ? ImageDecoders.Empty() : nil\n}\n\nlet url = URL(string: \"https://upload.wikimedia.org/wikipedia/commons/9/9d/Swift_logo.svg\")\nImagePipeline.shared.loadImage(with: url) { [weak self] result in\n    guard let self, let data = try? result.get().container.data else {\n        return\n    }\n    // You can render an image using whatever size you want, vector!\n    let targetBounds = CGRect(origin: .zero, size: CGSize(width: 300, height: 300))\n    let svgView = UIView(SVGData: data) { layer in\n        layer.fillColor = UIColor.orange.cgColor\n        layer.resizeToFit(targetBounds)\n    }\n    self.view.addSubview(svgView)\n    svgView.bounds = targetBounds\n    svgView.center = self.view.center\n}\n```\n\n> Important: Both [SwiftSVG](https://github.com/mchoe/SwiftSVG) and [SVG](https://github.com/SVGKit/SVGKit) only support a subset of SVG features.\n\n### WebP\n\n[WebP](https://developers.google.com/speed/webp) is supported natively on macOS 11+, iOS 14+, and watchOS 7+ via Image I/O. No additional plugins are required.\n"
  },
  {
    "path": "Documentation/Nuke.docc/Customization/ImageProcessing/image-processing.md",
    "content": "# Image Processing\n\nLearn how to use existing image filters and create custom ones.\n\n## Overview\n\nNuke features a powerful and efficient image processing infrastructure with multiple built-in processors and an API for creating custom ones.\n\n```swift\nImageRequest(url: url, processors: [\n    .resize(size: imageView.bounds.size)\n])\n```\n\nThe built-in processors can all be found in the ``ImageProcessors`` namespace, but the preferred way to create them is by using static factory methods on ``ImageProcessing`` protocol.\n\n## Custom Processors\n\nCustom processors need to implement ``ImageProcessing`` protocol. For the basic image processing needs, implement ``ImageProcessing/process(_:)`` method and create an identifier that uniquely identifies the processor. For processors with no input parameters, return a static string.\n\n```swift\npublic protocol ImageProcessing {\n    func process(image: UIImage) -> UIImage? // NSImage on macOS\n    var identifier: String { get }\n}\n```\n\n> All processing tasks are executed on a dedicated queue (``ImagePipeline/Configuration-swift.struct/imageProcessingQueue``).\n\nIf your processor needs to manipulate image metadata (``ImageContainer``) or get access to more information via ``ImageProcessingContext``, there is an additional method that you can implement in addition to ``ImageProcessing/process(_:context:)-26ffb``.\n\n```swift\npublic protocol ImageProcessing {\n    func process(_ image container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer\n}\n```\n\nIn addition to ``ImageProcessing/identifier`` (a `String`), you can implement ``ImageProcessing/hashableIdentifier-2i3a7`` to be used by the memory cache where string manipulations would be too slow. By default, this method returns the `identifier` string. If your processor conforms to `Hashable` protocol, it gets a default ``ImageProcessing/hashableIdentifier-2i3a7`` implementation that returns `self`.\n\n## Topics\n\n### Image Processing\n\n- ``ImageProcessing``\n- ``ImageProcessingOptions``\n- ``ImageProcessingContext``\n- ``ImageProcessingError``\n\n### Built-In Processors\n\n- ``ImageProcessing/resize(size:unit:contentMode:crop:upscale:)``\n- ``ImageProcessing/resize(width:unit:upscale:)``\n- ``ImageProcessing/resize(height:unit:upscale:)``\n- ``ImageProcessing/circle(border:)``\n- ``ImageProcessing/roundedCorners(radius:unit:border:)``\n- ``ImageProcessing/gaussianBlur(radius:)``\n- ``ImageProcessing/coreImageFilter(name:)``\n- ``ImageProcessing/coreImageFilter(name:parameters:identifier:)``\n- ``ImageProcessing/process(id:_:)``\n- ``ImageProcessors``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Customization/LoadingData/loading-data.md",
    "content": "# Loading Data\n\nLearn how pipeline loads data.\n\n## Overview\n\n``DataLoader`` uses [`URLSession`](https://developer.apple.com/reference/foundation/nsurlsession) to load image data. The data is cached on disk using [`URLCache`](https://developer.apple.com/reference/foundation/urlcache), which by default is initialized with a memory capacity of 0 MB (Nuke only stores processed images in memory) and a disk capacity of 150 MB.\n\n> Tip: See [Image Caching](https://kean.blog/post/image-caching) to learn more about HTTP cache. To learn more about caching in Nuke and how to configure it, see <doc:caching>.\n\nThe `URLSession` class natively supports the following URL schemes: `data`, `file`, `ftp`, `http`, and `https`.\n\nThe default ``DataLoader`` works great for most situations, but if you need to provide a custom networking layer, you can use a ``DataLoading`` protocol. See also, [Alamofire Plugin](https://github.com/kean/Nuke-Alamofire-Plugin).\n\n## Intercepting Requests\n\nTo modify a URL request just before it is sent — for example, to inject authentication tokens or sign requests — implement ``ImagePipeline/Delegate-swift.protocol/willLoadData(for:urlRequest:pipeline:)`` in your pipeline delegate:\n\n```swift\nfinal class AuthenticatedPipelineDelegate: ImagePipeline.Delegate {\n    func willLoadData(\n        for request: ImageRequest,\n        urlRequest: URLRequest,\n        pipeline: ImagePipeline\n    ) async throws -> URLRequest {\n        var urlRequest = urlRequest\n        let token = try await TokenStore.shared.validToken() // async, throws on failure\n        urlRequest.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        return urlRequest\n    }\n}\n\nlet pipeline = ImagePipeline(delegate: AuthenticatedPipelineDelegate()) {\n    // ...\n}\n```\n\nThe method is called on every URL-based request, after resumable data headers are applied but before the request is passed to ``DataLoading``. Throwing an error cancels the request and surfaces the error as ``ImagePipeline/Error/dataLoadingFailed(error:)``. The hook is not called for requests that use a custom data fetch closure or that target local file resources.\n\n## Monitoring Network Requests\n\nNuke can be used with [Pulse](https://github.com/kean/Pulse) for monitoring network traffic.\n\n```swift\n(ImagePipeline.shared.configuration.dataLoader as? DataLoader)?.delegate = URLSessionProxyDelegate()\n```\n\nThe same ``DataLoader/delegate`` can be used for modifying data loader behavior, e.g. for handling authentication requests and other aspects of data loading. \n\n```swift\n// The delegate is retained by the `DataLoader`.\n(ImagePipeline.shared.configuration.dataLoader as? DataLoader)?.delegate = YourDelegate()\n\nfinal class YourDelegate: URLSessionTaskDelegate {\n    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {\n        // Handle authentication challenge...\n    }\n}\n```\n\n## Resumable Downloads\n\nIf the data task is terminated when the image is partially loaded (either because of a failure or a cancellation), the next load will resume where the previous one left off. Resumable downloads require the server to support [HTTP Range Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests). Nuke supports both validators: `ETag` and `Last-Modified`. Resumable downloads are enabled by default. You can learn more in [\"Resumable Downloads\"](https://kean.blog/post/resumable-downloads).\n\n## Custom Networking Layer\n\nIf you'd like to use Alamofire for networking, it's easy to do thanks to an [Alamofire plugin](https://github.com/kean/Nuke-Alamofire-Plugin) that allows you to load image data using [Alamofire.SessionManager](https://github.com/Alamofire/Alamofire).\n\nIf you'd like to use some other networking library or use your custom code, all you need to do is implement the ``DataLoading`` protocol consisting of a single method.\n\n```swift\n/// Implements data loading using Alamofire framework.\npublic class AlamofireDataLoader: Nuke.DataLoading {\n    public let session: Alamofire.Session\n\n    /// Initializes the receiver with a given Alamofire.SessionManager.\n    /// - parameter session: Alamofire.Session.default by default.\n    public init(session: Alamofire.Session = Alamofire.Session.default) {\n        self.session = session\n    }\n\n    // MARK: DataLoading\n\n    /// Loads data using Alamofire.SessionManager.\n    public func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse) {\n        let task = self.session.streamRequest(request)\n\n        let response = try await withCheckedThrowingContinuation { continuation in\n            task.onHTTPResponse { response in\n                continuation.resume(returning: response)\n            }\n        }\n\n        let stream = AsyncThrowingStream<Data, Error> { streamContinuation in\n            streamContinuation.onTermination = { @Sendable _ in\n                task.cancel()\n            }\n            task.responseStream { stream in\n                switch stream.event {\n                case let .stream(result):\n                    switch result {\n                    case let .success(data):\n                        streamContinuation.yield(data)\n                    }\n                case let .complete(completion):\n                    if let error = completion.error {\n                        streamContinuation.finish(throwing: error)\n                    } else {\n                        streamContinuation.finish()\n                    }\n                }\n            }\n        }\n        return (stream, response)\n    }\n}\n```\n\n## Topics\n\n### Data Loader\n\n- ``DataLoading``\n- ``DataLoader``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Essentials/getting-started.md",
    "content": "# Getting Started\n\nLearn about main Nuke features and APIs.\n\n## Image Pipeline\n\n``ImagePipeline`` downloads images, caches, and prepares them for display. To load an image, use an async method ``ImagePipeline/image(for:)-4akzh`` returning an image.\n\n```swift\nlet image = try await ImagePipeline.shared.image(for: url)\n```\n\nTo get more control over the download, use ``ImagePipeline/imageTask(with:)-7s0fc`` to create an ``AsyncImageTask`` and then access its ``AsyncImageTask/image`` or ``AsyncImageTask/response`` to receive the image.\n\n```swift\nfunc loadImage() async throws {\n    let imageTask = ImagePipeline.shared.imageTask(with: url)\n    for await progress in imageTask.progress {\n        // Update progress\n    }\n    imageView.image = try await imageTask.image\n}\n```\n\n> Tip: You can start by using a ``ImagePipeline/shared`` pipeline and create a custom one later. To create a custom pipeline, use a convenience ``ImagePipeline/init(delegate:_:)`` initializer or one of the pre-defined configurations, such as ``ImagePipeline/Configuration-swift.struct/withDataCache``.\n\n> The documentation uses Async/Await APIs in the examples, but ``ImagePipeline`` also has equivalent completion-based and Combine APIs.\n\n## Image Request\n\n``ImageRequest`` allows you to set image processors, downsample images, change the request priority, and provide other options. See ``ImageRequest`` reference to learn more.\n\n```swift\nlet request = ImageRequest(\n    url: URL(string: \"http://example.com/image.jpeg\"),\n    processors: [.resize(width: 320)],\n    priority: .high,\n    options: [.reloadIgnoringCachedData]\n)\nlet image = try await pipeline.image(for: request)\n```\n\n> Tip: You can use built-in processors or create custom ones. Learn more in <doc:image-processing>.\n\n## Caching\n\nNuke has two cache layers: memory cache and disk cache.\n\n``ImageCache`` stores images prepared for display in memory. It uses a fraction of available RAM and automatically removes most cached images when the app goes to the background or receives a memory pressure warning.\n\nFor caching data persistently, by default, Nuke uses [`URLCache`](https://developer.apple.com/documentation/foundation/urlcache) with an increased capacity. One of its advantages is HTTP [`cache-control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) support.\n\nYou can also replace `URLCache` with a custom ``DataCache`` that ignores HTTP `cache-control` headers using ``ImagePipeline/Configuration-swift.struct/withDataCache(name:sizeLimit:)``.\n\n```swift\nImagePipeline.shared = ImagePipeline(configuration: .withDataCache)\n```\n\n``DataCache`` is faster than `URLCache` and provides more control. For example, it can be configured to store processed images using ``ImagePipeline/Configuration-swift.struct/dataCachePolicy``. The downside is that without HTTP `cache-control`, the images never get validated, and if the URL content changes, the app will continue showing stale data.  \n\n> Tip: To learn more about caching, see <doc:caching> section.\n\n## Performance\n\nOne of the key Nuke features is performance. It does a lot by default: custom cache layers, coalescing of equivalent requests, resumable HTTP downloads, and more. But there are certain things that the user of the framework can also do to use it more effectively, for example, <doc:prefetching>. To learn more about what you can do to improve image loading performance in your apps, see <doc:performance-guide>.\n\nTo optimize performance, you need to be able to monitor it. And that's where [Pulse](https://github.com/kean/Pulse) network logging framework comes in handy. It is optimized for working with images and is easy to integrate:\n\n```swift\n(ImagePipeline.shared.configuration.dataLoader as? DataLoader)?.delegate = URLSessionProxyDelegate()\n```\n\n## NukeUI\n\n**NukeUI** is a module that provides async image views for SwiftUI, UIKit, and AppKit.\n\n```swift\nstruct ContainerView: View {\n    var body: some View {\n        LazyImage(url: URL(string: \"https://example.com/image.jpeg\"))\n    }\n}\n```\n\nLearn more in NukeUI [documentation](https://kean-docs.github.io/nukeui/documentation/nukeui/).\n"
  },
  {
    "path": "Documentation/Nuke.docc/Extensions/DataLoader-Extension.md",
    "content": "# ``Nuke/DataLoader``\n\n\n## Topics\n\n### Initializers\n\n- ``init(configuration:validate:)``\n\n### Loading Data\n\n- ``loadData(with:)``\n\n### Observing Events\n\n- ``delegate``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Extensions/ImagePipeline-Extension.md",
    "content": "# ``Nuke/ImagePipeline``\n\n## Creating a Pipeline\n\nYou can start using a ``ImagePipeline/shared`` pipeline and create a custom one later if needed. To create a custom pipeline, you can use a convenience ``ImagePipeline/init(delegate:_:)`` initializer:\n\n```swift\nImagePipeline {\n    $0.dataCache = try? DataCache(name: \"com.myapp.datacache\")\n    $0.dataCachePolicy = .automatic\n}\n```\n\nYou can customize ``ImagePipeline`` by initializing it with ``ImagePipeline/Configuration-swift.struct`` and ``ImagePipeline/Delegate-swift.protocol``. You can provide custom caches, data loaders, add support for new image formats, and more.\n\n> Tip: The pipeline has two cache layers: memory cache and disk cache. By default, only the memory cache is enabled. For caching data persistently, it relies on system [`URLCache`](https://developer.apple.com/documentation/foundation/urlcache). There are advantages to enabling a custom disk cache. You can learn more in <doc:caching>.\n\n## Loading Images\n\nUse ``ImagePipeline/image(for:)-4akzh`` that works with both `URL` and ``ImageRequest`` and returns an image.\n\n```swift\nlet image = try await ImagePipeline.shared.image(for: url)\n```\n\nAlternatively, you can create an ``AsyncImageTask`` and access its ``AsyncImageTask/image`` or ``AsyncImageTask/response`` to fetch the image. You can use ``AsyncImageTask`` to cancel the request, change the priority of the running task, and observe its progress.\n\n```swift\nfinal class AsyncImageView: UIImageView {\n    func loadImage() async throws {\n        let imageTask = ImagePipeline.shared.imageTask(with: url)\n        for await progress in imageTask.progress {\n            // Update progress\n        }\n        imageView.image = try await imageTask.image\n    }\n}\n```\n\n> Tip: The recommended way to load images with ``ImagePipeline`` is by using Async/Await API. But the pipeline also has API that works with closures and Combine publishers.\n\n## Caching\n\nThe pipeline has two cache layers: memory cache and disk cache.\n\n``ImageCache`` stores images prepared for display in memory. It uses a fraction of available RAM and automatically removes most of the cached images when the app goes to the background or receives a memory pressure warning.\n\nFor caching data persistently, by default, Nuke uses [`URLCache`](https://developer.apple.com/documentation/foundation/urlcache) with an increased capacity. One of its advantages is HTTP [`cache-control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) support.\n\nYou can also replace `URLCache` with a custom ``DataCache`` that ignores HTTP `cache-control` headers using ``ImagePipeline/Configuration-swift.struct/withDataCache(name:sizeLimit:)``.\n\n```swift\nImagePipeline.shared = ImagePipeline(configuration: .withDataCache)\n```\n\n``DataCache`` is a bit faster than `URLCache` and provides more control. For example, it can be configured to store processed images using ``ImagePipeline/Configuration-swift.struct/dataCachePolicy``. The downside is that without HTTP `cache-control`, the images never get validated and if the URL content changes, the app will continue showing stale data.  \n\n> Tip: To learn more about caching, see <doc:caching> section.\n\n## Coalescing\n\nThe pipeline avoids doing any duplicated work when loading images. Let's take two requests with the same URL but different processors as an example:\n\n```swift\nlet url = URL(string: \"http://example.com/image\")\nasync let first = pipeline.image(for: ImageRequest(url: url, processors: [\n    .resize(size: CGSize(width: 44, height: 44)),\n    .gaussianBlur(radius: 8)\n]))\nasync let second = pipeline.image(for: ImageRequest(url: url, processors: [\n    .resize(size: CGSize(width: 44, height: 44))\n]))\nlet images = try await (first, second)\n```\n\nThe pipeline will load the data only once, resize the image once and blur it only once. There is no duplicated work done. The work only gets canceled when all the registered requests are, and the priority is based on the highest priority of the registered requests.\n\nCoalescing can be disabled using ``ImagePipeline/Configuration-swift.struct/isTaskCoalescingEnabled`` configuration option.\n\n## Progressive Decoding\n\nIf progressive decoding is enabled, the pipeline attempts to produce previews as data arrives. The behavior is controlled by ``ImagePipeline/PreviewPolicy``, which the pipeline resolves via ``ImagePipeline/Delegate/previewPolicy(for:pipeline:)``.\n\n**Default policy:** `.incremental` for progressive JPEGs and GIFs, `.disabled` for all other formats (baseline JPEGs, PNGs, etc.). This means only formats that benefit from incremental rendering produce previews by default.\n\n**Available policies:**\n- `.incremental` — Uses `CGImageSourceCreateIncremental` to produce a new preview as more data arrives. For JPEGs with large EXIF headers where incremental decoding fails, the decoder automatically falls back to generating a thumbnail.\n- `.thumbnail` — Extracts the embedded EXIF thumbnail (if any), then stops.\n- `.disabled` — No previews.\n\n**Throttling:** The pipeline throttles progressive decoding attempts using ``ImagePipeline/Configuration-swift.struct/progressiveDecodingInterval`` (default: 0.5s). When data arrives faster than this interval, intermediate chunks are skipped. This prevents excessive decoding work on fast connections.\n\n**Backpressure:** Every preview goes through the same processing and decompression phases as the final image. If a stage can't keep up, the pipeline waits for the current operation to finish before starting the next one. All outstanding progressive operations are canceled when the data is fully downloaded.\n\n## Topics\n\n### Getting a Pipeline\n\n- ``shared``\n\n### Initializers\n\n- ``init(configuration:delegate:)``\n- ``init(delegate:_:)``\n\n### Configuration\n\n- ``configuration-swift.property``\n- ``Configuration-swift.struct``\n\n### Loading Images (Async/Await)\n\n- ``image(for:)-4akzh``\n- ``image(for:)-9egg6``\n- ``imageTask(with:)-7s0fc``\n- ``imageTask(with:)-6aagk``\n\n### Loading Images (Combine)\n\n- ``imagePublisher(with:)-8j2bd``\n- ``imagePublisher(with:)-3pzm6``\n\n### Loading Images (Closures)\n\n- ``loadImage(with:completion:)-6q74f``\n- ``loadImage(with:completion:)-43osv``\n- ``loadImage(with:queue:progress:completion:)``\n\n### Loading Data\n\n- ``data(for:)-86rhw``\n- ``data(for:)-54h5g``\n- ``loadData(with:completion:)-815rt``\n- ``loadData(with:completion:)-6cwk3``\n- ``loadData(with:queue:progress:completion:)``\n\n### Accessing Cached Images\n\n- ``cache-swift.property``\n- ``Cache-swift.struct``\n\n### Invalidation\n\n- ``invalidate()``\n\n### Error Handling\n\n- ``Error``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Extensions/ImagePipelineConfiguration-Extension.md",
    "content": "# ``Nuke/ImagePipeline/Configuration-swift.struct``\n\n## Topics\n\n### Initializers\n\n- ``init(dataLoader:)``\n\n### Predefined Configurations\n\nTo learn more about caching, see <doc:caching>.\n\n- ``withDataCache``\n- ``withDataCache(name:sizeLimit:)``\n- ``withURLCache``\n\n### Dependencies\n\n- ``dataLoader``\n- ``dataCache``\n- ``imageCache``\n- ``makeImageDecoder``\n- ``makeImageEncoder``\n\n### Caching Options\n\n- ``dataCachePolicy``\n- ``ImagePipeline/DataCachePolicy``\n- ``isStoringPreviewsInMemoryCache``\n\n### Other Options\n\n- ``isDecompressionEnabled``\n- ``isTaskCoalescingEnabled``\n- ``isRateLimiterEnabled``\n- ``isProgressiveDecodingEnabled``\n- ``progressiveDecodingInterval``\n- ``isResumableDataEnabled``\n\n### Global Options\n\n- ``isSignpostLoggingEnabled``\n\n### Operation Queues\n\n- ``dataLoadingQueue``\n- ``imageProcessingQueue``\n- ``imageDecompressingQueue``\n- ``imageDecodingQueue``\n- ``imageEncodingQueue``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Extensions/ImagePipelineDelegate-Extension.md",
    "content": "# ``Nuke/ImagePipeline/Delegate-swift.protocol``\n\n## Topics\n\n### Data Loading\n\n- ``dataLoader(for:pipeline:)-7xolj``\n- ``willLoadData(for:urlRequest:pipeline:)``\n\n### Decoding and Encoding\n\n- ``imageDecoder(for:pipeline:)-2rbkl``\n- ``imageEncoder(for:pipeline:)-6uxsr``\n- ``previewPolicy(for:pipeline:)``\n\n### Caching\n\n- ``imageCache(for:pipeline:)-1i8cv``\n- ``dataCache(for:pipeline:)-2lnae``\n- ``cacheKey(for:pipeline:)-8k9a4``\n- ``willCache(data:image:for:pipeline:completion:)-7eg0n``\n\n### Decompression\n\n- ``shouldDecompress(response:for:pipeline:)-3cw2f``\n- ``decompress(response:request:pipeline:)-lbbz``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Extensions/ImagePiplelineCache-Extension.md",
    "content": "# ``Nuke/ImagePipeline/Cache-swift.struct``\n\n## Topics\n\n### Accessing Cached Images\n\n- ``cachedImage(for:caches:)``\n- ``storeCachedImage(_:for:caches:)``\n- ``removeCachedImage(for:caches:)``\n- ``containsCachedImage(for:caches:)``\n\n### Accessing Cached Data\n\n- ``cachedData(for:)``\n- ``storeCachedData(_:for:)``\n- ``removeCachedData(for:)``\n- ``containsData(for:)``\n\n### Removing All\n\n- ``removeAll(caches:)``\n\n### Cache Keys\n\n- ``makeImageCacheKey(for:)``\n- ``makeDataCacheKey(for:)``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Extensions/ImageRequest-Extension.md",
    "content": "# ``Nuke/ImageRequest``\n\n## Image Processing\n\nSet ``ImageRequest/processors`` to apply one of the built-in processors that can be found in ``ImageProcessors`` namespace or a custom one.\n\n```swift\nrequest.processors = [.resize(width: 320)]\n```\n\n> Tip: See <doc:image-processing> for more information on image processing.\n\n## Topics\n\n### Initializers\n\n- ``init(url:processors:priority:options:userInfo:)``\n- ``init(urlRequest:processors:priority:options:userInfo:)``\n- ``init(id:data:processors:priority:options:userInfo:)``\n- ``init(stringLiteral:)``\n\n### Options\n\n- ``processors``\n- ``priority-swift.property``\n- ``options-swift.property``\n- ``imageID``\n- ``scale``\n- ``thumbnail``\n- ``userInfo``\n\n### Nested Types\n\n- ``Priority-swift.enum``\n- ``Options-swift.struct``\n- ``ThumbnailOptions``\n- ``UserInfoKey``\n\n### Instance Properties\n\n- ``urlRequest``\n- ``url``\n- ``description``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Extensions/ImageResponse-Extension.md",
    "content": "# ``Nuke/ImageResponse``\n\n## Topics\n\n### Related Types\n\n- ``ImageContainer``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Extensions/ImageTask-Extension.md",
    "content": "# ``Nuke/ImageTask``\n\n## Topics\n\n### Controlling the Task State\n\n- ``cancel()``\n- ``state-swift.property``\n- ``State-swift.enum``\n- ``priority``\n- ``ImageRequest/Priority-swift.enum``\n\n### Task Progress\n\n- ``progress-swift.property``\n- ``Progress-swift.struct``\n\n### General Task Information\n\n- ``request``\n- ``taskId``\n- ``description``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Nuke.md",
    "content": "# ``Nuke``\n\nA powerful image loading system for Apple platforms.\n\n## Overview\n\nNuke provides an efficient way to download and display images in your app. It's easy to learn and use. Its architecture enables many powerful features while offering virtually unlimited possibilities for customization.\n\nThe framework is lean and compiles in under 2 seconds. Nuke has an automated test suite 2x the codebase size, ensuring excellent reliability. Every feature is carefully designed and optimized for performance.\n\n## Sponsors 💖\n\n[Support](https://github.com/sponsors/kean) Nuke on GitHub Sponsors.\n\n## Getting Started\n\nStart learning with <doc:getting-started> and review the rest of the articles in the documentation as needed. Check out the [demo project](https://github.com/kean/NukeDemo) to see Nuke in action.\n\nUpgrading from the previous version? Use a [Migration Guide](https://github.com/kean/Nuke/tree/master/Documentation/Migrations).\n\nLooking for UI components? See [NukeUI](https://kean-docs.github.io/nukeui/documentation/nukeui/) and [NukeExtensions](https://kean-docs.github.io/nukeextensions/documentation/nukeextensions/) documentation.\n\nTo install Nuke, use Swift Package Manager.\n\n## Extensions\n\nThe image pipeline is easy to customize and extend. Check out the following first-class extensions and packages built by the community.\n\n|Name|Description|\n|--|--|\n|[**Pulse**](https://github.com/kean/Pulse)|A network logging framework with easy [integration](https://github.com/kean/Nuke/pull/583)|\n|[**Alamofire Plugin**](https://github.com/kean/Nuke-Alamofire-Plugin)|Replace networking layer with [Alamofire](https://github.com/Alamofire/Alamofire)|\n|[**NukeWebP**](https://github.com/makleso6/NukeWebP)| **Community**. [WebP](https://developers.google.com/speed/webp/) support, built by [Maxim Kolesnik](https://github.com/makleso6)|\n|[**WebP Plugin**](https://github.com/ryokosuge/Nuke-WebP-Plugin)| **Community**. [WebP](https://developers.google.com/speed/webp/) support, built by [Ryo Kosuge](https://github.com/ryokosuge)|\n|[**AVIF Plugin**](https://github.com/delneg/Nuke-AVIF-Plugin)| **Community**. [AVIF](https://caniuse.com/avif) support, built by [Denis](https://github.com/delneg)|\n|[**RxNuke**](https://github.com/kean/RxNuke)|[RxSwift](https://github.com/ReactiveX/RxSwift) extensions for Nuke with examples|\n\n## Minimum Requirements\n\n| Nuke | Date         | Swift | Xcode | Platforms                                     |\n|------|--------------|-------|-------|-----------------------------------------------|\n| 12.0 | Mar 4, 2023  | 5.7   | 14.1  | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0 |\n| 11.0 | Jul 20, 2022 | 5.6   | 13.3  | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0 |\n| 10.0 | June 1, 2021 | 5.3   | 12.0  | iOS 11.0, watchOS 4.0, macOS 10.13, tvOS 11.0 |\n| 9.0  | May 20, 2020 | 5.1   | 11.0  | iOS 11.0, watchOS 4.0, macOS 10.13, tvOS 11.0 |\n| 8.0  | July 8, 2019 | 5.0   | 10.2  | iOS 10.0, watchOS 3.0, macOS 10.12, tvOS 10.0 |\n| 7.6  | Apr 7, 2019  | 4.2   | 10.1  | iOS 10.0, watchOS 3.0, macOS 10.12, tvOS 10.0 |\n| 6.0  | Dec 23, 2017 | 4.0   | 9.2   | iOS 9.0, watchOS 2.0, macOS 10.11, tvOS 9.0   |\n| 5.0  | Feb 1, 2017  | 3.0   | 8.0   | iOS 9.0, watchOS 2.0, macOS 10.11, tvOS 9.0   |\n| 4.0  | Sep 19, 2016 | 3.0   | 8.0   | iOS 9.0, watchOS 2.0, macOS 10.11, tvOS 9.0   |\n| 3.0  | Mar 26, 2016 | 2.2   | 7.3   | iOS 8.0, watchOS 2.0, macOS 10.9, tvOS 9.0    |\n| 2.0  | Feb 6, 2016  | 2.0   | 7.1   | iOS 8.0, watchOS 2.0, macOS 10.9, tvOS 9.0    |\n| 1.0  | Oct 18, 2015 | 2.0   | 7.0   | iOS 8.0, watchOS 2.0, macOS 10.9              |\n| 0.2  | Sep 18, 2015 | 2.0   | 7.0   | iOS 8.0, watchOS 2.0                          |\n\n## Topics\n\n### Essentials\n\n- <doc:getting-started>\n- ``ImagePipeline``\n- ``ImageRequest``\n- ``ImageResponse``\n- ``ImageTask``\n\n### Customization\n\n- <doc:image-processing>\n- <doc:loading-data>\n- <doc:image-formats>\n- ``ImagePipeline/Delegate-swift.protocol``\n\n### Performance\n\n- <doc:performance-guide>\n- <doc:prefetching>\n- <doc:combine>\n- <doc:caching>\n- ``ImagePrefetcher``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Performance/Caching/accessing-caches.md",
    "content": "# Access Cached Images\n\nLearn how to access cached images and data.\n\n## Overview\n\nThe pipeline performs cache lookup automatically when you request an image, but you also have a fair deal of control over the caching behavior with ``ImageRequest/Options-swift.struct``.\n\n### Fetching From Cache\n\nIf you want to perform cache lookup without downloading the image from the network, use ``ImageRequest/Options-swift.struct/returnCacheDataDontLoad`` option.\n\n```swift\nlet request = ImageRequest(url: url, options: [.returnCacheDataDontLoad])\nlet response = try await pipeline.imageTask(with: request).response\nlet cacheType = response.cacheType // .memory, .disk, or nil\n```\n\n> Important: This option only affects custom cache layers, but not `URLCache`, which is controlled by the [URL loading system](https://developer.apple.com/documentation/foundation/url_loading_system).\n\n### Reloading Images\n\nIf you need to reload an image, you can use ``ImagePipeline/Cache-swift.struct`` to remove the image from all cache layers (excluding `URLCache` that reloads automatically based on HTTP cache-control headers) before downloading it.\n\n```swift\nlet request = ImageRequest(url: url)\npipeline.cache.removeCachedImage(for: request)\n```\n\nIf you want to keep the image in caches but reload it, you can instruct the pipeline to ignore the cached data.\n\n```swift\nlet request = ImageRequest(url: url, options: [ .reloadIgnoringCachedData])\nlet response = try await pipeline.imageTask(with: request).response\n```\n\n``ImageRequest/Options-swift.struct`` provides even more granular control if needed, e.g. ``ImageRequest/Options-swift.struct/disableMemoryCacheReads`` and other similar options.\n\n## Direct Access\n\nYou can access any caching layer directly, but the pipeline also offers a convenience API: ``ImagePipeline/Cache-swift.struct``. By using it, you can update multiple cache layers at once, and you don't need to worry about managing the cache keys. It works with custom caches (``ImageCaching`` and ``DataCaching``) but not with `URLCache`, which is controlled by the [URL loading system](https://developer.apple.com/documentation/foundation/url_loading_system).\n\n### Subscript\n\nYou can access the memory cache with a subscript.\n\n```swift\nlet image = pipeline.cache[URL(string: \"https://example.com/image.jpeg\")!]\npipeline.cache[ImageRequest(url: url)] = nil\n```\n\n> ``ImageContainer`` contains some metadata about the image, and in the case of animated images or other images that require non-trivial rendering, also contains `data`. It also allows you to distinguish between progressive previews in case ``ImagePipeline/Configuration-swift.struct/isStoringPreviewsInMemoryCache`` option is enabled.\n\nAll ``ImagePipeline/Cache-swift.struct`` respect request cache control options.\n\n```swift\nlet url = URL(string: \"https://example.com/image.jpeg\")!\npipeline.cache[url] = ImageContainer(image: image)\n\n// Returns `nil` because memory cache reads are disabled\nlet request = ImageRequest(url: url, options: [.disableMemoryCacheReads])\nlet image = pipeline.cache[request]\n```\n\n### Accessing Images\n\nApart from the subscript, ``ImagePipeline/Cache-swift.struct`` also has methods for reading and writing images in either memory or disk cache or both.\n\n```swift\nlet cache = pipeline.cache\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\")!)\n\ncache.cachedImage(for: request) // From any cache layer\ncache.cachedImage(for: request, caches: [.memory]) // Only memory\ncache.cachedImage(for: request, caches: [.disk]) // Only disk (decodes data)\n\nlet data = cache.cachedData(for: request)\ncache.containsData(for: request) // Fast contains check \n\n// Stores image in the memory cache and stores an encoded\n// image in the disk cache\ncache.storeCachedImage(ImageContainer(image: image), for: request)\n\ncache.removeCachedImage(for: request)\ncache.removeAll()\n```\n\n### Managing Cache Keys\n\nYou don't need to worry about cache keys when working with ``ImagePipeline/Cache-swift.struct``, but it also gives you access to them in case you need it.\n\n```swift\nlet request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\"))\npipeline.cache.makeImageCacheKey(for: request)\npipeline.cache.makeDataCacheKey(for: request)\n```\n\nThere is also a hook in ``ImagePipeline/Delegate-swift.protocol`` that allows you to customize how the keys are generated:\n\n```swift\nfunc cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? {\n    request.imageID\n}\n```\n"
  },
  {
    "path": "Documentation/Nuke.docc/Performance/Caching/cache-layers.md",
    "content": "# Cache Layers\n\nLearn about memory and disk cache layers in Nuke.\n\n## Overview\n\nNuke has three cache layers:\n\n- ``ImageCache`` – LRU **memory** cache for processed images\n- ``DataCache`` – aggressive LRU **disk** cache\n- [`URLCache`](https://developer.apple.com/documentation/foundation/urlcache) – HTTP **disk** cache, which is part of the native [URL loading system](https://developer.apple.com/documentation/foundation/url_loading_system)\n\nThe default pipeline uses a combination of ``ImageCache`` and [`URLCache`](https://developer.apple.com/documentation/foundation/urlcache) with an increased disk size. This configuration supports HTTP [`cache-control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). \n\n### Memory Cache\n\n``ImageCache`` is a **memory** cache with an [LRU cleanup](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)) policy (least recently used are removed first). The pipeline uses it to store processed images that are decompressed and ready to be displayed.\n\n``ImageCache`` discards the least recently cached images if either *cost* or *count* limit is reached. The default cost limit represents a number of bytes and is calculated based on the amount of physical memory available on the device. The default count limit is `Int.max`.\n\n```swift\n// Configure cache\nImageCache.shared.costLimit = 1024 * 1024 * 100 // 100 MB\nImageCache.shared.countLimit = 100\nImageCache.shared.ttl = 120 // Invalidate image after 120 sec\n\n// Read and write images\nlet request = ImageRequest(url: url)\nImageCache.shared[request] = ImageContainer(image: image)\nlet image = ImageCache.shared[request]\n\n// Clear cache\nImageCache.shared.removeAll()\n```\n\n`ImageCache` automatically removes all stored elements when it receives a memory warning. It also automatically removes *most* stored elements when the app enters the background.\n\n> You can implement a custom cache by conforming to the ``ImageCaching`` protocol.\n\n### HTTP Disk Cache\n\n[`URLCache`](https://developer.apple.com/documentation/foundation/urlcache) is an HTTP **disk** cache that is part of the native [URL loading system](https://developer.apple.com/documentation/foundation/url_loading_system). It is used by the default image pipeline, which is instantiated with a ``ImagePipeline/Configuration-swift.struct/withURLCache`` configuration.\n\n```swift\n// Configure cache\nDataLoader.sharedUrlCache.diskCapacity = 100\nDataLoader.sharedUrlCache.memoryCapacity = 0\n\n// Read and write responses\nlet urlRequest = URLRequest(url: url)\n_ = DataLoader.sharedUrlCache.cachedResponse(for: urlRequest)\nDataLoader.sharedUrlCache.removeCachedResponse(for: urlRequest)\n\n// Clear cache\nDataLoader.sharedUrlCache.removeAllCachedResponses()\n```\n\nAn HTTP disk cache (``ImagePipeline/Configuration-swift.struct/withURLCache`` option) gives the server precise control over caching via [`cache-control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) HTTP headers. You can specify what images to cache and for how long. The client can also periodically check the cached response for [freshness](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#freshness) and refresh if needed – useful for refreshing profile pictures or logos.\n\n> Tip: Learn more about HTTP cache in [\"Image Caching.\"](https://kean.blog/post/image-caching#http-caching)\n\n#### Serving Stale Images\n\nIf the resource expires, `URLSession` isn’t going to serve it until it goes to the server and validates whether the contents stored in the cache are still fresh.\n\n**Solutions**\n\n- Increase the expiration age in HTTP `cache-control` headers\n- Use a custom disk cache that ignores HTTP `cache-control` headers\n- Ask `URLSession` to return an expired image using [URLRequest.CachePolicy.returnCacheDataDontLoad](https://developer.apple.com/documentation/foundation/nsurlrequest/cachepolicy/returncachedatadontload) and then validate it later in the background\n- Dynamically switch between `.useProtocolCachePolicy` to `.returnCacheDataDontLoad` when network appears to be offline\n\n### Aggressive Disk Cache\n\nIf HTTP caching is not your cup of tea, try a custom LRU disk cache for fast and reliable *aggressive* data caching (ignores [HTTP cache control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)). You can enable it using the respective pipeline configuration.\n\n```swift\nImagePipeline.shared = ImagePipeline(configuration: .withDataCache)\n```\n\nIf you want to change the disk cache configuration, you can also instantiate ``DataCache`` manually or even provide your own implementation by conforming to ``DataCaching`` protocol:\n\n```swift\nImagePipeline {\n    $0.dataCache = try? DataCache(name: \"com.myapp.datacache\")\n}\n```\n\n> Important: If you enable it manually, make sure to disable the native URL cache. To do it, pass a ``DataLoader`` with a custom `URLSessionConfiguration` when creating a pipeline. Built-in ``ImagePipeline/Configuration-swift.struct/withDataCache`` configuration takes care of it automatically for you.\n\nBy default, the pipeline stores only the original image data. You can change this behavior by specifying a different cache policy that was [described earlier](#cache-policy).\n\n```swift\nlet dataCache = try DataCache(name: \"my-cache\")\n\ndataCache.sizeLimit = 1024 * 1024 * 100 // 100 MB\n\ndataCache.storeData(data, for: \"key\")\nif dataCache.containsData(for: \"key\") {\n    print(\"Data is cached\")\n}\nlet data = dataCache.cachedData(for: \"key\")\n// or let data = dataCache[\"key\"]\ndataCache.removeData(for: \"key\")\ndataCache.removeAll()\n```\n\n``DataCache`` is asynchronous which means ``DataCache/storeData(_:for:)`` method returns immediately and the disk I/O happens later. For a synchronous write, use ``DataCache/flush()``.\n\n> Tip: To share a disk cache between your app and an extension (e.g. a Notification Service Extension), point ``DataCache`` at a directory inside a shared app group container. Set ``DataCache/isSweepEnabled`` to `false` in the extension so that only the main app enforces size limits.\n>\n> ```swift\n> let sharedCacheURL = FileManager.default\n>     .containerURL(forSecurityApplicationGroupIdentifier: \"group.com.myapp\")\n>\n> // Main app\n> ImagePipeline.shared = ImagePipeline {\n>     $0.dataCache = try? DataCache(path: sharedCacheURL)\n> }\n>\n> // Extension — reads/writes the same cache but skips LRU sweeps\n> ImagePipeline.shared = ImagePipeline {\n>     $0.dataCache = {\n>         let cache = try? DataCache(path: sharedCacheURL)\n>         cache?.isSweepEnabled = false\n>         return cache\n>     }()\n> }\n> ```\n\n```swift\ndataCache.storeData(data, for: \"key\")\ndataCache.flush()\n// or dataCache.flush(for: \"key\")\n\nlet url = dataCache.url(for: \"key\")\n// Access file directly\n```\n"
  },
  {
    "path": "Documentation/Nuke.docc/Performance/Caching/caching.md",
    "content": "# Caching\n\nLearn about cache layers in Nuke and how to configure them.\n\n## Overview\n\nNuke has three cache layers that you can configure to precisely match your app needs. The pipeline uses these caches when you request an image. Your app has advanced control over how images are stored and retrieved and direct access to all cache layers.\n\n## Topics\n\n### Overview\n\n- <doc:cache-layers>\n- <doc:accessing-caches>\n\n### Memory Cache\n\n- ``ImageCaching``\n- ``ImageCache``\n- ``ImageCacheKey``\n\n### Disk Cache\n\n- ``DataCaching``\n- ``DataCache``\n\n### Composite Cache\n\n- ``ImagePipeline/Cache-swift.struct``\n"
  },
  {
    "path": "Documentation/Nuke.docc/Performance/combine.md",
    "content": "# Combine\n\nLearn how to use Combine publishers to improve image loading performance in your apps.  \n\nThe publisher created using ``ImagePipeline/imagePublisher(with:)-3pzm6`` method starts a new ``ImageTask`` when a subscriber is added and delivers the results to the subscriber. If the requested image is available in the memory cache, the value is delivered immediately. When the subscription is canceled, the task also gets canceled.\n\n > If you need to support earlier iOS versions, check out [RxNuke](https://github.com/kean/RxNuke).\n\n ## Image Publisher\n\n To create a publisher, use ``ImagePipeline/imagePublisher(with:)-3pzm6`` method from ``ImagePipeline``.\n\nA basic example where we load an image and display the result on success:\n\n```swift\ncancellable = pipeline.imagePublisher(with: url)\n    .sink(receiveCompletion: { _ in /* Ignore errors */ },\n          receiveValue: { imageView.image = $0.image })\n```\n\n ## Displaying Images\n\n So you created a custom publisher by combining a couple of operators, how do you use it to display the image? NukeUI module provides a simple way to display the resulting image.\n\n```swift\nlet image = FetchImage()\nlet publisher = pipeline.imagePublisher(with: \"https://example.com/image.jpeg\")\nimage.load(publisher)\n```\n\n ## Use Cases\n\n There are many scenarios in which you can find Combine useful. Here are some of them.\n\n ### Low Resolution to High\n\n Let's say you want to show a user a high-resolution image that takes a while to load. You can show a spinner while the high-resolution image is downloaded, but you can improve the user experience by quickly downloading and displaying a thumbnail.\n\n > As an alternative, Nuke also supports progressive JPEG. It is enabled by default.\n\n You can implement it using `append` operator. This operator results in a serial execution. It starts a thumbnail request, waits until it finishes, and only then starts a request for a high-resolution image.\n\n```swift\nlet lowResImage = pipeline.imagePublisher(with: lowResUrl).orEmpty\nlet highResImage = pipeline.imagePublisher(with: highResUrl).orEmpty\n\ncancellable = lowResImage.append(highResImage)\n    .sink(receiveCompletion: { _ in /* Ignore errors */ },\n          receiveValue: { imageView.image = $0.image })\n```\n\n > `orEmpty` is a custom property which catches the errors and immediately completes the publisher instead.\n\n```swift\npublic extension Publisher {\n    var orEmpty: AnyPublisher<Output, Never> {\n        catch { _ in Empty<Output, Never>() }.eraseToAnyPublisher()\n    }\n}\n```\n\n### Load the First Available\n\nLet's say you have multiple URLs for the same image. For example, you uploaded the image from the camera to the server; you have the image stored locally. When you display this image, it would be beneficial to first load the local URL, and if that fails, try to download from the network.\n\nThis use case is very similar to going from low to high resolution, except for the addition of the `first()` operator that stops the execution when the first value is received.\n\n```swift\nlet localImage = pipeline.imagePublisher(with: localUrl).orEmpty\nlet networkImage = pipeline.imagePublisher(with: networkUrl).orEmpty\n\ncancellable = localImage.append(networkImage)\n    .first()\n    .sink(receiveCompletion: { _ in /* Ignore errors */ },\n          receiveValue: { imageView.image = $0.image })\n```\n\n### Load Multiple Images\n\nLet's say you want to load two icons for a button, one icon for a `.normal` state, and one for a `.selected` state. You want to update the button, only when both icons are fully loaded. This can be achieved using a `combine` operator.\n\n```swift\nlet iconImage = pipeline.imagePublisher(with: iconUrl)\nlet iconSelectedImage = pipeline.imagePublisher(with: iconSelectedUrl)\n\ncancellable = iconImage.combineLatest(iconSelectedImage)\n    .sink(receiveCompletion: { _ in /* Ignore errors */ },\n          receiveValue: { icon, iconSelected in\n            button.isHidden = false\n            button.setImage(icon.image, for: .normal)\n            button.setImage(iconSelected.image, for: .selected)\n         })\n```\n\nNotice there is no `orEmpty` in this example since we want both requests to succeed.\n\n### Validate Stale Image\n\nLet's say you want to show the user a stale image stored in disk cache (`Foundation.URLCache`) while you go to the server to validate if the image is still fresh. It can be implemented using the same `append` operator that we covered previously.\n\n```swift\nlet cacheRequest = URLRequest(url: url, cachePolicy: .returnCacheDataDontLoad)\nlet networkRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy)\n\nlet cachedImage = pipeline.imagePublisher(with: ImageRequest(urlRequest: cacheRequest)).orEmpty\nlet networkImage = pipeline.imagePublisher(with: ImageRequest(urlRequest: networkRequest)).orEmpty\n\ncancellable = cachedImage.append(networkImage)\n    .sink(receiveCompletion: { _ in /* Ignore errors */ },\n          receiveValue: { imageView.image = $0.image })\n```\n\n ### Low Data Mode\n\n Starting with iOS 13, the iOS users can enable \"Low Data Mode\" in system settings. One of the ways the apps can handle it is to use resources that take less network bandwidth. Combine makes it easy to implement.\n\n```swift\n// Create the original image request and prevent it from going through\n// when \"Low Data Mode\" is enabled in the iOS settings.\nvar urlRequest = URLRequest(url: URL(string: \"https://example.com/high-quality.jpeg\")!)\nurlRequest.allowsConstrainedNetworkAccess = false\nlet request = ImageRequest(urlRequest: urlRequest)\n\n// Catch the \"constrained\" network error and provide a fallback resource\n// that uses less network bandwidth.\nlet image = pipeline.imagePublisher(with: request).tryCatch { error -> ImagePublisher in\n    guard (error.dataLoadingError as? URLError)?.networkUnavailableReason == .constrained else {\n        throw error\n    }\n    return pipeline.imagePublisher(with: URL(string: \"https://example.com/low-quality.jpeg\"))\n}\n\ncancellable = image.sink(receiveCompletion: { result in\n    // Handle error\n}, receiveValue: {\n    imageView.image = $0.image\n})\n```\n\n > tip: Learn more about Low Data Mode in [WWDC2019: Advances in Networking, Part 1](https://developer.apple.com/videos/play/wwdc2019/712/).\n"
  },
  {
    "path": "Documentation/Nuke.docc/Performance/performance-guide.md",
    "content": "# Performance Guide\n\nLearn about the performance features in Nuke and how to make the most of them.\n\n## Caching\n\nImages can take a lot of space. By using Nuke, you can ensure that when you download an image, it will be cached so that you don't have to download it again. Nuke provides three different caching layers.\n\n### L1. Memory Cache (Default)\n\nThe images are stored in a fast in-memory cache: ``ImageCache``. It uses [LRU (least recently used)](https://en.wikipedia.org/wiki/Cache_algorithms#Examples) replacement algorithm and has a strict size limit. It also automatically evicts images on memory warnings and removes a portion of its contents when the application enters background mode.\n\n> Important: Nuke stores decompressed (bitmapped) images in the memory cache. If your app is loading and displaying high-resolution images, consider downsampling them and/or increasing cache limits. For context, a bitmap for a 6000x4000px image takes 92 MB (assuming it needs 4 bytes per pixel).\n\n### L2. HTTP Disk Cache (Default)\n\nBy default, unprocessed image data is stored in native [`URLCache`](https://developer.apple.com/documentation/foundation/urlcache), which is part of the [Foundation URL Loading System](https://developer.apple.com/documentation/foundation/url_loading_system). The main feature of `URLCache` is its support of [Cache Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). Here is an example of an HTTP header with cache control.\n\n```\nHTTP/1.1 200 OK\nCache-Control: public, max-age=3600\nExpires: Mon, 26 Jan 2016 17:45:57 GMT\nLast-Modified: Mon, 12 Jan 2016 17:45:57 GMT\nETag: \"686897696a7c876b7e\"\n```\n\nThis response is cacheable, and will be *fresh* for 1 hour. When the response becomes *stale*, the client *validates* it by making a *conditional* request using the `If-Modified-Since` and/or `If-None-Match` headers. If the response is still fresh, the server returns status code `304 Not Modified` to instruct the client to use cached data, or it would return `200 OK` with new data otherwise.\n\n> Tip: Make sure that the images served by the server have [Cache Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) set correctly.\n\n> Important: By default, `URLCache` doesn't serve stale images offline. To show a stale image, pass the `URLRequest` with cache policy set to [.returnCacheDataDontLoad](https://developer.apple.com/documentation/foundation/nsurlrequest/cachepolicy/returncachedatadontload) and then perform a second request to refresh the image.\n\n### L3. Aggressive Disk Cache (Optional)\n\nIf your server uses unique URLs for images for which the contents never change, consider enabling ``DataCache`` (see ``ImagePipeline/Configuration-swift.struct/withDataCache`` that also takes care of disabling the default `URLCache`). It's a fast persistent cache with non-blocking writes that allows reads to be parallel to writes and each other. It also works offline and reduces pressure on `URLSession`.\n\n> Tip: By default, ``DataCache`` stores only the original image data. To also cache processed images, set a data cache policy that enables it. ``ImagePipeline/DataCachePolicy/automatic`` is a good default: it stores original data for unprocessed requests and processed images for requests with processors.\n\n```swift\nImagePipeline.shared = ImagePipeline(configuration: .withDataCache(dataCachePolicy: .automatic))\n```\n\n> Tip: To save disk space, see `ImageEncoders.ImageIO` and `ImageEncoder.isHEIFPreferred` option for HEIF support.\n\n## Prefetching\n\nPrefetching means downloading data ahead of time in anticipation of its use. It creates an illusion that the images are simply available the moment you want to see them – no networking involved. It's very effective. See <doc:prefetching> to learn more about how to enable it.\n\n> Important: If you apply processors when displaying final images, make sure to use the same processors for prefetching. Otherwise, Nuke will end up populating the memory cache with the versions of the images you are never going to need for display.\n\n## Decompression\n\nImage formats often use compression to reduce the overall data size, but it comes at a cost. An image needs to be decompressed, or _bitmapped_, before it can be displayed. `UIImage` does _not_ eagerly decompress this data until you display it. It leads to performance issues like scroll view stuttering. To avoid it, Nuke automatically decompresses the images in the background. Decompression only runs if needed; it won't run for already processed images.\n\n> Note: See [Image and Graphics Best Practices](https://developer.apple.com/videos/play/wwdc2018/219) to learn more about image decoding and downsampling.\n\n## Downsample Images\n\nIdeally, the app should download the images optimized for the target device screen size, but it's not always possible. To reduce memory usage, downsample the images.\n\n```swift\n// Target size is in points\nlet request = ImageRequest(url: url, processors: [.resize(width: 320)])\n```\n\n> Tip: Some image formats, such as jpeg, can have thumbnails embedded in the original image data. If you are working with a large image and want to show only a thumbnail, consider using ``ImageRequest/ThumbnailOptions``. If the thumbnails aren't available, they are generated. It can be up to 4x faster than using ``ImageProcessors/Resize`` for high-resolution images. \n\n## Main Thread Performance\n\nNuke has a range of optimizations across the board to ensure it does as little work on the main thread as possible.\n\n- **CoW**. The primary type in Nuke is ``ImageRequest``. It has multiple options, so the struct is quite large. To make sure that passing it around is as efficient as possible, ``ImageRequest``  uses a Copy-on-Write technique.\n- **OptionSet**. In one of the recent versions of Nuke, ``ImageRequest`` was optimized even further by using option sets and reordering properties to take advantage of gaps in memory stride to reduce its memory layout.\n- **ImageRequest.CacheKey**. Most frameworks use strings to uniquely identify requests. But string manipulations are expensive, and this is why in Nuke, there is a special internal type, `ImageRequest.CacheKey`, which allows for efficient equality checks with no strings manipulation.\n\nThese are just some examples of the optimization techniques used in Nuke. There are many more. Every new feature in Nuke is designed with performance in mind to make sure there are no performance regressions ever.\n\n> Tip: One thing you can do to optimize the main thread's performance is create URLs in the background, as their initialization can be relatively expensive. It's best to do it during decoding.  \n\n## Resumable Downloads\n\nMake sure your server supports resumable downloads. If the data task is terminated when the image is partially loaded (either because of a failure or a cancellation), the next load will resume where the previous one left off. Resumable downloads require the server to support [HTTP Range Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests). Nuke supports both validators: `ETag` and `Last-Modified`. Resumable downloads are enabled by default. You can learn more in [\"Resumable Downloads\"](https://kean.blog/post/resumable-downloads).\n\n## Coalescing\n\nThanks to coalescing (enabled by default), the pipeline avoids doing any duplicated work when loading images. Let's take the following two requests as an example.\n\n```swift\nlet url = URL(string: \"https://example.com/image\")\n\n// Only one network request is made for both of these\npipeline.loadImage(with: ImageRequest(url: url, processors: [\n    .resize(size: CGSize(width: 44, height: 44)),\n    .gaussianBlur(radius: 8)\n]))\npipeline.loadImage(with: ImageRequest(url: url, processors: [\n    .resize(size: CGSize(width: 44, height: 44))\n]))\n```\n\nNuke will load the data only once, resize the image once and blur it also only once. There is no duplicated work done. When you request an image, the pipeline creates a dependency graph of tasks needed to deliver the final images and reuses the ones that it can.\n\n> Note: Coalescing is controlled by ``ImagePipeline/Configuration-swift.struct/isTaskCoalescingEnabled``. It can be disabled if you need requests with the same URL to be treated as independent tasks.\n\n## Progressive Decoding\n\nNuke supports progressive JPEG, but it must be enabled in the pipeline configuration.\n\n```swift\nImagePipeline.shared = ImagePipeline {\n    $0.isProgressiveDecodingEnabled = true\n}\n```\n\nOnce enabled, you’ll first see a blurry low-quality version of the full image, which gets sharper as more data arrives. Progressive previews are delivered through the same ``ImageTask`` progress handler or `AsyncStream` used for the final image.\n\n## Request Priorities\n\nNuke is fully asynchronous and performs well under stress. ``ImagePipeline`` distributes its work on [operation queues](https://developer.apple.com/documentation/foundation/operationqueue) dedicated to a specific type of work, such as processing and decoding. Each queue limits the number of concurrent tasks, respects the request priorities, and cancels the work as soon as possible.\n\nCancelling an ``ImageTask`` frees its associated network and CPU resources immediately. Thanks to coalescing, the underlying work is only cancelled when all requests sharing it have been cancelled — so cancelling one request doesn't affect others loading the same image.\n\nNuke allows you to set the request priority and update it for outstanding tasks. It uses priorities for prefetching: the requests created by the prefetcher all have `.low` priority to make sure they don't interfere with the \"regular\" requests. See <doc:prefetching> to learn more.\n\nThere are many other creative ways to use priorities. For example, when the user taps an image in a grid to open it full screen, you can lower the priority of the requests for the images that are not visible on the screen.\n\n```swift\nfinal class ImageView: UIView {\n    private var task: ImageTask?\n\n    override func willMove(toWindow newWindow: UIWindow?) {\n        super.willMove(toWindow: newWindow)\n\n        task?.priority = newWindow == nil ? .low : .high\n    }\n}\n```\n\n## Rate Limiting\n\nIf the app starts and cancels requests at a fast rate, Nuke will rate limit the requests, protecting `URLSession`. `RateLimiter` uses a classic [token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm. The implementation supports quick bursts of requests which can be executed without any delays when \"the bucket is full\". It is important to make sure `RateLimiter` only kicks in when needed, but when the user opens the screen, all the requests are fired immediately.\n\n## Auto Retry\n\nEnable [`waitsForConnectivity`](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/2908812-waitsforconnectivity) on `URLSession` to indicate that the session should wait for connectivity to become available instead of failing the request immediately in case of a network failure.\n\n## Measure\n\nIf you want to see how the system behaves, how long each operation takes, and how many are performed in parallel, enable the ``ImagePipeline/Configuration-swift.struct/isSignpostLoggingEnabled`` option and use the `os_signpost` Instrument. For more information, see [Apple Documentation: Logging](https://developer.apple.com/documentation/os/logging) and [WWDC 2018: Measuring Performance Using Logging](https://developer.apple.com/videos/play/wwdc2018/405/).\n\n## Selecting a System\n\nMake sure you select one image loading framework and stick to it. If you use more than one framework, it will prevent them from managing the system resources efficiently, such as caches. If, for any reason, you must use more than one framework, ensure that they at least share the same memory and disk caches.\n"
  },
  {
    "path": "Documentation/Nuke.docc/Performance/prefetching.md",
    "content": "# Prefetching\n\nLearn how to prefetch images to improve user experience.\n\n## Overview\n\nLoading data ahead of time in anticipation of its use ([prefetching](https://en.wikipedia.org/wiki/Prefetching)) is a great way to improve user experience. It's especially effective for images; it can give users an impression that there is no networking and the images are just magically always there.\n\n## UICollectionView\n\nStarting with iOS 10, it became easy to implement prefetching in a `UICollectionView` thanks to the [`UICollectionViewDataSourcePrefetching`](https://developer.apple.com/documentation/uikit/uicollectionviewdatasourceprefetching) API. All you need to do is set [`isPrefetchingEnabled`](https://developer.apple.com/documentation/uikit/uicollectionview/1771771-isprefetchingenabled) to `true` and set a [`prefetchDataSource`](https://developer.apple.com/documentation/uikit/uicollectionview/1771768-prefetchdatasource).\n\n```swift\nfinal class PrefetchingDemoViewController: UICollectionViewController {\n    private let prefetcher = ImagePrefetcher()\n    private var photos: [URL] = []\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        collectionView?.isPrefetchingEnabled = true\n        collectionView?.prefetchDataSource = self\n    }\n}\n\nextension PrefetchingDemoViewController: UICollectionViewDataSourcePrefetching {\n    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {\n        let urls = indexPaths.map { photos[$0.row] }\n        prefetcher.startPrefetching(with: urls)\n    }\n\n    func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {\n        let urls = indexPaths.map { photos[$0.row] }\n        prefetcher.stopPrefetching(with: urls)\n    }\n}\n```\n\n> Warning: If you are using any of the processors when displaying the images, e.g. ``ImageProcessors/Resize``, you need to use the same processors for prefetching. Otherwise, the prefetcher will bitmap and cache the original image, defeating the main purpose of prefetching to get images fully ready for display before the user even sees them.   \n\nThis code sample comes straight from [Nuke Demo](https://github.com/kean/NukeDemo).\n\nLet's say, there are 32 items on the screen (the last row is partially visible). When you open it for the first time, the prefetch API asks the app to start prefetching for indices `[32-55]`. As you scroll, the prefetch \"window\" changes. You receive `cancelPrefetchingForItemsAt` calls for items no longer in the prefetch window.\n\nWhen the user goes to another screen, you can either cancel all the prefetching tasks (but then you'll need to figure out a way to restart them when the user comes back) or you can also pause them.\n\n```swift\noverride func viewWillAppear(_ animated: Bool) {\n    super.viewWillAppear(animated)\n\n    prefetcher.isPaused = false\n}\n\noverride func viewWillDisappear(_ animated: Bool) {\n    super.viewWillDisappear(animated)\n\n    // When you pause, the prefetcher will finish outstanding tasks\n    // (by default, there are only 2 at a time), and pause the rest.\n    prefetcher.isPaused = true\n}\n```\n\n> Tip: Starting with [Nuke 9.5.0](https://github.com/kean/Nuke/releases/tag/9.5.0), you can also change the prefetcher priority. For example, when the user goes to another screen that also has image prefetching, you can lower it to `.veryLow`. This way, the prefetching will continue for both screens, but the top screen will have priority.\n\n## ImagePrefetcher\n \nYou typically create one ``ImagePrefetcher`` per screen.\n\nTo start prefetching, call ``ImagePrefetcher/startPrefetching(with:)-718dg`` method. When you need the same image later to display it, simply use the ``ImagePipeline`` or view extensions to load the image. The pipeline will take care of coalescing the requests without starting any new downloads:\n\n- ``ImagePrefetcher/startPrefetching(with:)-718dg``\n- ``ImagePrefetcher/stopPrefetching(with:)-8cdam``\n- ``ImagePrefetcher/stopPrefetching()``\n\nThe prefetcher automatically cancels all of the outstanding tasks when deallocated. All ``ImagePrefetcher`` methods are thread-safe and are optimized to be used even from the main thread during scrolling.\n\n> Important: Prefetching takes up users' data and puts extra pressure on CPU and memory. To reduce the CPU and memory usage, you have an option to choose only the disk cache as a prefetching destination: ``ImagePrefetcher/Destination/diskCache``. It doesn't require image decoding and processing and therefore uses less CPU. The images are stored on disk, so they also take up less memory. This policy doesn't work with ``ImagePipeline/DataCachePolicy/storeEncodedImages`` cache policy and other policies that affect `loadData()`. \n\n"
  },
  {
    "path": "Documentation/NukeExtensions.docc/ImageViewExtensions.md",
    "content": "# Image View Extensions\n\nLearn about extensions for image views.\n\n## Overview\n\nNuke provides a set of global functions that simplify loading of images into image views. It's a good starting point for some apps, but if you want to have more control, consider using Nuke's `ImagePipeline` directly.\n\n> Tip: For SwiftUI support, check out **NukeUI** module. It also includes custom image views for UIKit and AppKit designed to be a better replacement for global functions in `NukeExtensions`.\n\n## Image View\n\nDownload and display an image in an image view with a single line of code:\n\n```swift\nNukeExtensions.loadImage(with: url, into: imageView)\n```\n\nIf the image is stored in the memory cache, it is displayed immediately with no animations. If not, the image is first loaded using an image pipeline.\n\n## Table View\n\nBefore loading a new image, the view is prepared for reuse by canceling any outstanding requests and removing a previously displayed image, making it perfect for table views.\n\n```swift\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n    // ...\nNukeExtensions.loadImage(with: url, into: cell.imageView)\n}\n```\n\nWhat works for `UITableView`, also does for a `UICollectionView`. You can see `UICollectionView` in action in the [demo project](https://github.com/kean/NukeDemo).\n\n> When the view is deallocated, an associated request also gets canceled automatically. To manually cancel the request, call ``NukeExtensions/cancelRequest(for:)``.\n\n## ImageLoadingOptions\n\n``ImageLoadingOptions`` offers multiple options to control the image view extensions behavior.\n\n```swift\nlet options = ImageLoadingOptions(\n    placeholder: UIImage(named: \"placeholder\"),\n    transition: .fadeIn(duration: 0.33)\n)\nNukeExtensions.loadImage(with: url, options: options, into: imageView)\n```\n\n> Tip: The extensions have a limited set of options. If you need more, check out `LazyImageView` from **NukeUI**.\n\n### Placeholder\n\nPlaceholder to be displayed while the image is loading. `nil` by default.\n\n```swift\noptions.placeholder = UIImage(named: \"placeholder\")\n```\n\n### Failure Image\n\nImage to be displayed when the request fails. `nil` by default.\n\n```swift\noption.failureImage = UIImage(named: \"oopsie\")\n```\n\n### Transitions\n\nThe image transition animation performed when displaying a loaded image. Only runs when the image was not found in the memory cache (use ``ImageLoadingOptions/alwaysTransition``) to always run the animation). `nil` by default.\n\n```swift\noptions.transition = .fadeIn(duration: 0.33)\n```\n\nFor a complete list of available transitions see ``ImageLoadingOptions/Transition-swift.struct``. Use ``ImageLoadingOptions/failureImageTransition`` for the failure image.\n\n### Content Modes\n\nYou can change content mode for each of the image types: placeholder, success, failure. This is useful when a placeholder image needs to be displayed with `.center`, but image with `.scaleAspectFill`. By default, `nil` – don't change the content mode.\n\n```swift\noptions.contentModes = .init(success: .scaleAspectFill, failure: .center, placeholder: .center)\n```\n\n### Tint Colors\n\nYou can also specify custom tint colors to be used for each image type: placeholder, success, failure.\n\n```swift\noptions.tintColors = .init(success: .green, failure: .red, placeholder: .yellow)\n```\n\n### Shared Options\n\nIf you want to modify the default options, set ``ImageLoadingOptions/shared``.\n\n```swift\nImageLoadingOptions.shared.transition = .fadeIn(duration: 0.33)\n```\n\n### Other Options\n\nFor a complete list of options, see ``ImageLoadingOptions``. Some options, such as ``ImageLoadingOptions/isProgressiveRenderingEnabled`` will be covered later.\n\n> Built-in extensions for image views are designed to get you up and running as quickly as possible. But if you want to have more control, or use some of the advanced features, like animated images, it is recommended to use `ImagePipeline` directly.\n\n## Progressive Decoding\n\nNuke supports progressive JPEG out of the box.\n\n## Custom Views\n\nYou can use image view extensions with custom views by implementing ``Nuke_ImageDisplaying`` protocol.\n\n> The name of the protocol has a prefix because it's an Objective-C protocol. Objective-C runtime allows you to override methods declared in extensions in subclasses.\n\n```swift\nextension UIImageView: Nuke_ImageDisplaying {\n    open func nuke_display(image: UIImage?, data: Data?) {\n        self.image = image\n    }\n}\n```\n\nNuke provides built-in implementations for `UIImageView` and `NSImageView`.\n\n## Customizing Requests\n\nAll the examples from this guide used ``NukeExtensions/loadImage(with:options:into:completion:)-6ksm2`` with a `URL`. But you can have even more control over the image download by using `ImageRequest`. To learn more about `ImageRequest`, see the main Nuke documentation.\n"
  },
  {
    "path": "Documentation/NukeExtensions.docc/NukeExtensions.md",
    "content": "# ``NukeExtensions``\n\nNuke provides convenience extensions for image views with multiple display options.\n\n## Overview\n\nIn this guide, you'll learn about these extensions and all of the available options. \n"
  },
  {
    "path": "Documentation/NukeUI.docc/Extensions/FetchImage-Extensions.md",
    "content": "# ``NukeUI/FetchImage``\n\n## Overview\n\n``FetchImage`` is an observable object ([`ObservableObject`](https://developer.apple.com/documentation/combine/observableobject)) that allows you to manage the download of an image and observe the download status. It acts as a ViewModel that manages the image download state making it easy to add image loading to your custom SwiftUI views.\n\n## Creating Custom Views\n\n```swift\nstruct ImageView: View {\n    let url: URL\n\n    @StateObject private var image = FetchImage()\n\n    var body: some View {\n        ZStack {\n            Rectangle().fill(Color.gray)\n            image.image?\n                .resizable()\n                .aspectRatio(contentMode: .fill)\n                .clipped()\n        }\n        .onAppear { image.load(url) }\n        .onChange(of: url) { image.load($0) }\n        .onDisappear { image.reset() }\n    }\n}\n```\n\n``FetchImage`` gives you full control over how to manage the download and how to display the image. For example, if you want the download to continue when the view leaves the screen, change the appearance callbacks accordingly.\n\n```swift\nstruct ImageView: View {\n    let url: URL\n\n    @StateObject private var image = FetchImage()\n\n    var body: some View {\n        // ...\n        .onAppear {\n            image.priority = .normal\n            image.load(url)\n        }\n        .onDisappear {\n            image.priority = .low\n        }\n    }\n}\n```\n\n## Topics\n\n### Initializers\n\n- ``init()``\n\n### Loading Images\n\n- ``load(_:)-9my9q``\n- ``load(_:)-53ybw``\n- ``load(_:)-6pey2``\n- ``cancel()``\n- ``reset()``\n\n### State\n\n- ``result``\n- ``imageContainer``\n- ``isLoading``\n- ``progress-swift.property``\n\n### Options\n\n- ``priority``\n- ``processors``\n- ``pipeline``\n- ``transaction``\n"
  },
  {
    "path": "Documentation/NukeUI.docc/Extensions/Image-Extension.md",
    "content": "# ``NukeUI/Image``\n\n## Topics\n\n### Initializers\n\n- ``init(_:)``\n- ``init(_:onCreated:)``\n\n### Configuration\n\n- ``resizingMode(_:)``\n- ``videoRenderingEnabled(_:)``\n- ``videoLoopingEnabled(_:)``\n- ``animatedImageRenderingEnabled(_:)``\n"
  },
  {
    "path": "Documentation/NukeUI.docc/Extensions/LazyImage-Extensions.md",
    "content": "# ``NukeUI/LazyImage``\n\n## Using LazyImage\n\nThe view is instantiated with a [`URL`](https://developer.apple.com/documentation/foundation/url) or an ``ImageRequest``.\n\n```swift\nstruct ContainerView: View {\n    var body: some View {\n        LazyImage(url: URL(string: \"https://example.com/image.jpeg\"))\n    }\n}\n```\n\nThe view is called \"lazy\" because it loads the image only when it appears on the screen. And when it disappears, the current request automatically gets canceled. When the view reappears, the download picks up where it left off, thanks to [resumable downloads](https://kean.blog/post/resumable-downloads). \n\n> Tip: To change the `onDisappear` behavior, use ``LazyImage/onDisappear(_:)``.\n\nUntil the image loads, the view displays a standard placeholder that fills the available space, just like [AsyncImage](https://developer.apple.com/documentation/SwiftUI/AsyncImage) does. After the load completes successfully, the view updates to display the image.\n\n![nukeui demo](nukeui-preview)\n\nTo gain more control over the loading process and how the image is displayed, use ``LazyImage/init(url:transaction:content:)``, which takes a `content` closure that receives a ``LazyImageState``.\n\n```swift\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\")) { state in\n    if let image = state.image {\n        image.resizable().aspectRatio(contentMode: .fill)\n    } else if state.error != nil {\n        Color.red // Indicates an error\n    } else {\n        Color.blue // Acts as a placeholder\n    }\n}\n```\n\n> Important: You can’t apply image-specific modifiers, like `resizable(capInsets:resizingMode:)`, directly to a `LazyImage`. Instead, apply them to the `Image` instance that your content closure gets when defining the view’s appearance.\n\nWhen the image is loaded, it is displayed with no animation, which is a recommended option. If you add an animation, it's automatically applied when the image is downloaded, but not when it's retrieved from the memory cache. \n\n```swift\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\"))\n    .animation(.default)\n```\n\n`LazyImage` can be instantiated with an `ImageRequest` or configured using convenience modifiers.\n\n```swift\nLazyImage(request: ImageRequest(\n    url: URL(string: \"https://example.com/image.jpeg\"),\n    processors: [.resize(width: 44)]\n))\n\nLazyImage(url: URL(string: \"https://example.com/image.jpeg\"))\n    .processors([.resize(width: 44)])\n    .priority(.high)\n    .pipeline(customPipeline)\n```\n\n> Tip: ``LazyImage`` is built on top of ``FetchImage``. If you want even more control, you can use it directly instead.  \n\n## Topics\n\n### Initializers\n\n- ``init(url:)``\n- ``init(request:)``\n- ``init(url:transaction:content:)``\n- ``init(request:transaction:content:)``\n\n### Cancellation\n\n- ``onDisappear(_:)``\n\n### Request Options\n\n- ``priority(_:)``\n- ``processors(_:)``\n- ``pipeline(_:)``\n"
  },
  {
    "path": "Documentation/NukeUI.docc/Extensions/LazyImageView-Extensions.md",
    "content": "# ``NukeUI/LazyImageView``\n\n## Topics\n\n### Initializers\n\n- ``init(frame:)``\n- ``init(coder:)``\n\n### Loading Images\n\n- ``url``\n- ``request``\n- ``cancel()``\n- ``reset()``\n\n### Request Options\n\n- ``priority``\n- ``processors``\n- ``pipeline``\n\n### Displaying Images\n\n- ``placeholderImage``\n- ``placeholderView``\n- ``placeholderViewPosition``\n- ``failureImage``\n- ``failureView``\n- ``failureViewPosition``\n- ``isProgressiveImageRenderingEnabled``\n- ``isResetEnabled``\n- ``transition-swift.property``\n\n### Callbacks\n\n- ``onStart``\n- ``onProgress``\n- ``onPreview``\n- ``onSuccess``\n- ``onFailure``\n- ``onCompletion``\n\n### Accessing Underlying Views\n\n- ``imageView``\n"
  },
  {
    "path": "Documentation/NukeUI.docc/NukeUI.md",
    "content": "# ``NukeUI``\n\nImage loading for SwiftUI, UIKit, and AppKit views.\n\n## Overview\n\nThere are two main views provided by the framework:\n\n- ``LazyImage`` for SwiftUI\n- ``LazyImageView`` for UIKit and AppKit\n\n``LazyImage`` is designed similar to the native [`AsyncImage`](https://developer.apple.com/documentation/SwiftUI/AsyncImage), but it uses [Nuke](https://github.com/kean/Nuke) for loading images. You can take advantage of all of its features, such as caching, prefetching, task coalescing, smart background decompression, request priorities, and more.\n\n![nukeui demo](nukeui-preview)\n\n## Topics\n\n### Essentials\n\n- ``LazyImage``\n- ``LazyImageView``\n\n### Helpers\n\n- ``LazyImageState``\n- ``FetchImage``\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-2026 Alexander Grebenyuk\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Nuke.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 70;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t0C0023052863E81A00B018B0 /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };\n\t\t0C1B9880294E28D800C09310 /* Nuke.docc in Sources */ = {isa = PBXBuildFile; fileRef = 0C1B987F294E28D800C09310 /* Nuke.docc */; };\n\t\t0C1C201D29ABBF19004B38FD /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };\n\t\t0C1C201E29ABBF19004B38FD /* Nuke.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };\n\t\t0C222DE3294E2DEA00012288 /* NukeUI.docc in Sources */ = {isa = PBXBuildFile; fileRef = 0C222DE2294E2DEA00012288 /* NukeUI.docc */; };\n\t\t0C222DE5294E2E0300012288 /* NukeExtensions.docc in Sources */ = {isa = PBXBuildFile; fileRef = 0C222DE4294E2E0200012288 /* NukeExtensions.docc */; };\n\t\t0C27B34B2F5462D100F96DB1 /* NukeExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C55FCFD28567875000FD2C9 /* NukeExtensions.framework */; };\n\t\t0C27B34C2F5462D100F96DB1 /* NukeExtensions.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 0C55FCFD28567875000FD2C9 /* NukeExtensions.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };\n\t\t0C38DB1C28568FE20027F9FF /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };\n\t\t0C38DB48285690CF0027F9FF /* NukeUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C38DAE028568FAE0027F9FF /* NukeUI.framework */; };\n\t\t0C4F900322E4C4FB0070ECFD /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };\n\t\t0C55FD0528567875000FD2C9 /* NukeExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C55FCFD28567875000FD2C9 /* NukeExtensions.framework */; };\n\t\t0C55FD1028567875000FD2C9 /* NukeExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C55FCFD28567875000FD2C9 /* NukeExtensions.framework */; };\n\t\t0C55FD1128567875000FD2C9 /* NukeExtensions.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0C55FCFD28567875000FD2C9 /* NukeExtensions.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };\n\t\t0C55FD21285679A4000FD2C9 /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };\n\t\t0C7584A629A151FF00F985F8 /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };\n\t\t0C7C067C1BCA882A00089D7F /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };\n\t\t0C8D7BD31D9DBF1600D12EB7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD21D9DBF1600D12EB7 /* AppDelegate.swift */; };\n\t\t0C8D7BED1D9DC02B00D12EB7 /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };\n\t\t0C973E141D9FDB9F00C00AD9 /* Nuke.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t0C1C201F29ABBF19004B38FD /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0C9174871BAE99EE004A7905 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0C91748F1BAE99EE004A7905;\n\t\t\tremoteInfo = Nuke;\n\t\t};\n\t\t0C38DB49285690D20027F9FF /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0C9174871BAE99EE004A7905 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0C38DAA128568FAE0027F9FF;\n\t\t\tremoteInfo = NukeUI;\n\t\t};\n\t\t0C55FD0628567875000FD2C9 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0C9174871BAE99EE004A7905 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0C55FCFC28567875000FD2C9;\n\t\t\tremoteInfo = NukeExtensions;\n\t\t};\n\t\t0C55FD0E28567875000FD2C9 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0C9174871BAE99EE004A7905 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0C55FCFC28567875000FD2C9;\n\t\t\tremoteInfo = NukeExtensions;\n\t\t};\n\t\t0C8D7BEE1D9DC02B00D12EB7 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0C9174871BAE99EE004A7905 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0C91748F1BAE99EE004A7905;\n\t\t\tremoteInfo = \"Nuke iOS\";\n\t\t};\n\t\t0C8D7BF31D9DC03A00D12EB7 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0C9174871BAE99EE004A7905 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0C8D7BCF1D9DBF1600D12EB7;\n\t\t\tremoteInfo = \"Nuke iOS Tests Host\";\n\t\t};\n\t\t0CB644AD28567FC500916267 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0C9174871BAE99EE004A7905 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0C55FCFC28567875000FD2C9;\n\t\t\tremoteInfo = NukeExtensions;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t0C55FD1528567875000FD2C9 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t\t0C55FD1128567875000FD2C9 /* NukeExtensions.framework in Embed Frameworks */,\n\t\t\t\t0C1C201E29ABBF19004B38FD /* Nuke.framework in Embed Frameworks */,\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C973E131D9FDB9200C00AD9 /* CopyFiles */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t\t0C973E141D9FDB9F00C00AD9 /* Nuke.framework in CopyFiles */,\n\t\t\t\t0C27B34C2F5462D100F96DB1 /* NukeExtensions.framework in CopyFiles */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t0C09B16A1FE9A65C00E8FE3B /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t0C179C772282AC50008AB488 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = \"<group>\"; };\n\t\t0C1B987F294E28D800C09310 /* Nuke.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Nuke.docc; sourceTree = \"<group>\"; };\n\t\t0C222DE2294E2DEA00012288 /* NukeUI.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = NukeUI.docc; sourceTree = \"<group>\"; };\n\t\t0C222DE4294E2E0200012288 /* NukeExtensions.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = NukeExtensions.docc; sourceTree = \"<group>\"; };\n\t\t0C38DAE028568FAE0027F9FF /* NukeUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NukeUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C38DB3428568FE20027F9FF /* NukeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NukeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C4326262424338200799446 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = \"<group>\"; };\n\t\t0C4F8FDF22E4B6ED0070ECFD /* NukeThreadSafetyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NukeThreadSafetyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C55FCFD28567875000FD2C9 /* NukeExtensions.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NukeExtensions.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C55FD0428567875000FD2C9 /* NukeExtensionsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NukeExtensionsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C7584AA29A151FF00F985F8 /* NukeVideo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NukeVideo.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C7C06771BCA882A00089D7F /* NukeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NukeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C8D7BD01D9DBF1600D12EB7 /* NukeTestsHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NukeTestsHost.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C8D7BD21D9DBF1600D12EB7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t0C8D7BE81D9DC02B00D12EB7 /* NukePerformanceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NukePerformanceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0C9174901BAE99EE004A7905 /* Nuke.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Nuke.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0CDB92801DAF9BB900002905 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = \"<group>\"; };\n\t\t0CDB92821DAF9BC600002905 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = \"<group>\"; };\n\t\t0CDB92831DAF9BCB00002905 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */\n\t\t0C27B34D2F54632F00F96DB1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {\n\t\t\tisa = PBXFileSystemSynchronizedBuildFileExceptionSet;\n\t\t\tmembershipExceptions = (\n\t\t\t\tTestExpectation.swift,\n\t\t\t\tTestHelpers.swift,\n\t\t\t);\n\t\t\ttarget = 0C8D7BE71D9DC02B00D12EB7 /* NukePerformanceTests */;\n\t\t};\n/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */\n\n/* Begin PBXFileSystemSynchronizedRootGroup section */\n\t\t0C2782062F5299D600F96DB1 /* Mocks */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); name = Mocks; path = Tests/Mocks; sourceTree = SOURCE_ROOT; };\n\t\t0C27822C2F529A5F00F96DB1 /* Resources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Resources; sourceTree = \"<group>\"; };\n\t\t0C27829C2F529ABD00F96DB1 /* Helpers */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (0C27B34D2F54632F00F96DB1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Helpers; sourceTree = \"<group>\"; };\n\t\t0C5A6A2A2F67339800533FD1 /* Migrations */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Migrations; sourceTree = \"<group>\"; };\n\t\t0C8ECECB2F4BAF1200C9F423 /* NukeTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NukeTests; sourceTree = \"<group>\"; };\n\t\t0C8ECEF62F4BAF2F00C9F423 /* NukeUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NukeUITests; sourceTree = \"<group>\"; };\n\t\t0C8ECEFD2F4BAF3C00C9F423 /* NukeExtensionsTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NukeExtensionsTests; sourceTree = \"<group>\"; };\n\t\t0C8ECF042F4BAF5100C9F423 /* NukeThreadSafetyTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NukeThreadSafetyTests; sourceTree = \"<group>\"; };\n\t\t0C8ECF0C2F4BAF5E00C9F423 /* NukePerformanceTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NukePerformanceTests; sourceTree = \"<group>\"; };\n\t\t0C8ECF532F4BB02900C9F423 /* Nuke */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Nuke; sourceTree = \"<group>\"; };\n\t\t0C8ECF902F4BB03100C9F423 /* NukeUI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NukeUI; sourceTree = \"<group>\"; };\n\t\t0C8ECF992F4BB03B00C9F423 /* NukeVideo */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NukeVideo; sourceTree = \"<group>\"; };\n\t\t0C8ECFA42F4BB05300C9F423 /* NukeExtensions */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NukeExtensions; sourceTree = \"<group>\"; };\n/* End PBXFileSystemSynchronizedRootGroup section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t0C0023042863E81700B018B0 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C0023052863E81A00B018B0 /* Nuke.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C38DB1B28568FE20027F9FF /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C38DB48285690CF0027F9FF /* NukeUI.framework in Frameworks */,\n\t\t\t\t0C38DB1C28568FE20027F9FF /* Nuke.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C4F8FDC22E4B6ED0070ECFD /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C4F900322E4C4FB0070ECFD /* Nuke.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C55FCFA28567875000FD2C9 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C55FD21285679A4000FD2C9 /* Nuke.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C55FD0128567875000FD2C9 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C55FD0528567875000FD2C9 /* NukeExtensions.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C7584A529A151FF00F985F8 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C7584A629A151FF00F985F8 /* Nuke.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C7C06741BCA882A00089D7F /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C7C067C1BCA882A00089D7F /* Nuke.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C8D7BCD1D9DBF1600D12EB7 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C55FD1028567875000FD2C9 /* NukeExtensions.framework in Frameworks */,\n\t\t\t\t0C1C201D29ABBF19004B38FD /* Nuke.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C8D7BE51D9DC02B00D12EB7 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C8D7BED1D9DC02B00D12EB7 /* Nuke.framework in Frameworks */,\n\t\t\t\t0C27B34B2F5462D100F96DB1 /* NukeExtensions.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t0C096C7B1BAE9ADD007FE380 /* Sources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0C8ECF532F4BB02900C9F423 /* Nuke */,\n\t\t\t\t0C8ECFA42F4BB05300C9F423 /* NukeExtensions */,\n\t\t\t\t0C8ECF902F4BB03100C9F423 /* NukeUI */,\n\t\t\t\t0C8ECF992F4BB03B00C9F423 /* NukeVideo */,\n\t\t\t);\n\t\t\tpath = Sources;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0C7C06551BCA87EC00089D7F /* Tests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0C09B16A1FE9A65C00E8FE3B /* Info.plist */,\n\t\t\t\t0C8ECECB2F4BAF1200C9F423 /* NukeTests */,\n\t\t\t\t0C8ECEF62F4BAF2F00C9F423 /* NukeUITests */,\n\t\t\t\t0C8ECEFD2F4BAF3C00C9F423 /* NukeExtensionsTests */,\n\t\t\t\t0C8ECF042F4BAF5100C9F423 /* NukeThreadSafetyTests */,\n\t\t\t\t0C8ECF0C2F4BAF5E00C9F423 /* NukePerformanceTests */,\n\t\t\t\t0C8D7BD11D9DBF1600D12EB7 /* Host */,\n\t\t\t\t0C27829C2F529ABD00F96DB1 /* Helpers */,\n\t\t\t\t0C2782062F5299D600F96DB1 /* Mocks */,\n\t\t\t\t0C27822C2F529A5F00F96DB1 /* Resources */,\n\t\t\t);\n\t\t\tpath = Tests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0C8D7BD11D9DBF1600D12EB7 /* Host */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0C8D7BD21D9DBF1600D12EB7 /* AppDelegate.swift */,\n\t\t\t);\n\t\t\tpath = Host;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0C9174861BAE99EE004A7905 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0C096C7B1BAE9ADD007FE380 /* Sources */,\n\t\t\t\t0C7C06551BCA87EC00089D7F /* Tests */,\n\t\t\t\t0C9174911BAE99EE004A7905 /* Products */,\n\t\t\t\t0CDB92761DAF9BA500002905 /* Documentation */,\n\t\t\t\t0CDB927F1DAF9BA900002905 /* Metadata */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0C9174911BAE99EE004A7905 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0C9174901BAE99EE004A7905 /* Nuke.framework */,\n\t\t\t\t0C7C06771BCA882A00089D7F /* NukeTests.xctest */,\n\t\t\t\t0C8D7BD01D9DBF1600D12EB7 /* NukeTestsHost.app */,\n\t\t\t\t0C8D7BE81D9DC02B00D12EB7 /* NukePerformanceTests.xctest */,\n\t\t\t\t0C4F8FDF22E4B6ED0070ECFD /* NukeThreadSafetyTests.xctest */,\n\t\t\t\t0C55FCFD28567875000FD2C9 /* NukeExtensions.framework */,\n\t\t\t\t0C55FD0428567875000FD2C9 /* NukeExtensionsTests.xctest */,\n\t\t\t\t0C38DAE028568FAE0027F9FF /* NukeUI.framework */,\n\t\t\t\t0C38DB3428568FE20027F9FF /* NukeUITests.xctest */,\n\t\t\t\t0C7584AA29A151FF00F985F8 /* NukeVideo.framework */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0CDB92761DAF9BA500002905 /* Documentation */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0C5A6A2A2F67339800533FD1 /* Migrations */,\n\t\t\t\t0C1B987F294E28D800C09310 /* Nuke.docc */,\n\t\t\t\t0C222DE2294E2DEA00012288 /* NukeUI.docc */,\n\t\t\t\t0C222DE4294E2E0200012288 /* NukeExtensions.docc */,\n\t\t\t);\n\t\t\tpath = Documentation;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0CDB927F1DAF9BA900002905 /* Metadata */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0CDB92821DAF9BC600002905 /* README.md */,\n\t\t\t\t0CDB92801DAF9BB900002905 /* CHANGELOG.md */,\n\t\t\t\t0C4326262424338200799446 /* LICENSE */,\n\t\t\t\t0CDB92831DAF9BCB00002905 /* Package.swift */,\n\t\t\t\t0C179C772282AC50008AB488 /* .swiftlint.yml */,\n\t\t\t);\n\t\t\tname = Metadata;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t0C38DAA128568FAE0027F9FF /* NukeUI */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C38DADD28568FAE0027F9FF /* Build configuration list for PBXNativeTarget \"NukeUI\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C38DAA228568FAE0027F9FF /* Lint */,\n\t\t\t\t0C38DAA328568FAE0027F9FF /* Sources */,\n\t\t\t\t0C0023042863E81700B018B0 /* Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C8ECF902F4BB03100C9F423 /* NukeUI */,\n\t\t\t);\n\t\t\tname = NukeUI;\n\t\t\tproductName = Nuke;\n\t\t\tproductReference = 0C38DAE028568FAE0027F9FF /* NukeUI.framework */;\n\t\t\tproductType = \"com.apple.product-type.framework\";\n\t\t};\n\t\t0C38DAE428568FE20027F9FF /* NukeUITests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C38DB3128568FE20027F9FF /* Build configuration list for PBXNativeTarget \"NukeUITests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C38DAE528568FE20027F9FF /* Sources */,\n\t\t\t\t0C38DB1B28568FE20027F9FF /* Frameworks */,\n\t\t\t\t0C38DB1D28568FE20027F9FF /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t0C38DB4A285690D20027F9FF /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C2782062F5299D600F96DB1 /* Mocks */,\n\t\t\t\t0C27822C2F529A5F00F96DB1 /* Resources */,\n\t\t\t\t0C27829C2F529ABD00F96DB1 /* Helpers */,\n\t\t\t\t0C8ECEF62F4BAF2F00C9F423 /* NukeUITests */,\n\t\t\t);\n\t\t\tname = NukeUITests;\n\t\t\tproductName = \"Nuke Tests\";\n\t\t\tproductReference = 0C38DB3428568FE20027F9FF /* NukeUITests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t0C4F8FDE22E4B6ED0070ECFD /* NukeThreadSafetyTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C4F8FE422E4B6ED0070ECFD /* Build configuration list for PBXNativeTarget \"NukeThreadSafetyTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C4F8FDB22E4B6ED0070ECFD /* Sources */,\n\t\t\t\t0C4F8FDC22E4B6ED0070ECFD /* Frameworks */,\n\t\t\t\t0C4F8FDD22E4B6ED0070ECFD /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C2782062F5299D600F96DB1 /* Mocks */,\n\t\t\t\t0C27822C2F529A5F00F96DB1 /* Resources */,\n\t\t\t\t0C27829C2F529ABD00F96DB1 /* Helpers */,\n\t\t\t\t0C8ECF042F4BAF5100C9F423 /* NukeThreadSafetyTests */,\n\t\t\t);\n\t\t\tname = NukeThreadSafetyTests;\n\t\t\tproductName = NukeThreadSafetyTests;\n\t\t\tproductReference = 0C4F8FDF22E4B6ED0070ECFD /* NukeThreadSafetyTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t0C55FCFC28567875000FD2C9 /* NukeExtensions */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C55FD1228567875000FD2C9 /* Build configuration list for PBXNativeTarget \"NukeExtensions\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0CC04A952C4BD3BB00F1164D /* Lint */,\n\t\t\t\t0C55FCF928567875000FD2C9 /* Sources */,\n\t\t\t\t0C55FCFA28567875000FD2C9 /* Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C8ECFA42F4BB05300C9F423 /* NukeExtensions */,\n\t\t\t);\n\t\t\tname = NukeExtensions;\n\t\t\tproductName = NukeExtensions;\n\t\t\tproductReference = 0C55FCFD28567875000FD2C9 /* NukeExtensions.framework */;\n\t\t\tproductType = \"com.apple.product-type.framework\";\n\t\t};\n\t\t0C55FD0328567875000FD2C9 /* NukeExtensionsTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C55FD1628567875000FD2C9 /* Build configuration list for PBXNativeTarget \"NukeExtensionsTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C55FD0028567875000FD2C9 /* Sources */,\n\t\t\t\t0C55FD0128567875000FD2C9 /* Frameworks */,\n\t\t\t\t0C55FD0228567875000FD2C9 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t0C55FD0728567875000FD2C9 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C2782062F5299D600F96DB1 /* Mocks */,\n\t\t\t\t0C27822C2F529A5F00F96DB1 /* Resources */,\n\t\t\t\t0C27829C2F529ABD00F96DB1 /* Helpers */,\n\t\t\t\t0C8ECEFD2F4BAF3C00C9F423 /* NukeExtensionsTests */,\n\t\t\t);\n\t\t\tname = NukeExtensionsTests;\n\t\t\tproductName = NukeExtensionsTests;\n\t\t\tproductReference = 0C55FD0428567875000FD2C9 /* NukeExtensionsTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t0C75849B29A151FF00F985F8 /* NukeVideo */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C7584A729A151FF00F985F8 /* Build configuration list for PBXNativeTarget \"NukeVideo\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C75849C29A151FF00F985F8 /* Lint */,\n\t\t\t\t0C75849D29A151FF00F985F8 /* Sources */,\n\t\t\t\t0C7584A529A151FF00F985F8 /* Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C8ECF992F4BB03B00C9F423 /* NukeVideo */,\n\t\t\t);\n\t\t\tname = NukeVideo;\n\t\t\tproductName = Nuke;\n\t\t\tproductReference = 0C7584AA29A151FF00F985F8 /* NukeVideo.framework */;\n\t\t\tproductType = \"com.apple.product-type.framework\";\n\t\t};\n\t\t0C7C06761BCA882A00089D7F /* NukeTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C7C067F1BCA882A00089D7F /* Build configuration list for PBXNativeTarget \"NukeTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C7C06731BCA882A00089D7F /* Sources */,\n\t\t\t\t0C7C06741BCA882A00089D7F /* Frameworks */,\n\t\t\t\t0C7C06751BCA882A00089D7F /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C2782062F5299D600F96DB1 /* Mocks */,\n\t\t\t\t0C27822C2F529A5F00F96DB1 /* Resources */,\n\t\t\t\t0C27829C2F529ABD00F96DB1 /* Helpers */,\n\t\t\t\t0C8ECECB2F4BAF1200C9F423 /* NukeTests */,\n\t\t\t);\n\t\t\tname = NukeTests;\n\t\t\tproductName = \"Nuke Tests\";\n\t\t\tproductReference = 0C7C06771BCA882A00089D7F /* NukeTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t0C8D7BCF1D9DBF1600D12EB7 /* NukeTestsHost */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C8D7BE11D9DBF1600D12EB7 /* Build configuration list for PBXNativeTarget \"NukeTestsHost\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C8D7BCC1D9DBF1600D12EB7 /* Sources */,\n\t\t\t\t0C8D7BCD1D9DBF1600D12EB7 /* Frameworks */,\n\t\t\t\t0C8D7BCE1D9DBF1600D12EB7 /* Resources */,\n\t\t\t\t0C55FD1528567875000FD2C9 /* Embed Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t0C55FD0F28567875000FD2C9 /* PBXTargetDependency */,\n\t\t\t\t0C1C202029ABBF19004B38FD /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = NukeTestsHost;\n\t\t\tproductName = \"Nuke iOS Tests Host\";\n\t\t\tproductReference = 0C8D7BD01D9DBF1600D12EB7 /* NukeTestsHost.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\t0C8D7BE71D9DC02B00D12EB7 /* NukePerformanceTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C8D7BF01D9DC02B00D12EB7 /* Build configuration list for PBXNativeTarget \"NukePerformanceTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C8D7BE41D9DC02B00D12EB7 /* Sources */,\n\t\t\t\t0C8D7BE51D9DC02B00D12EB7 /* Frameworks */,\n\t\t\t\t0C8D7BE61D9DC02B00D12EB7 /* Resources */,\n\t\t\t\t0C973E131D9FDB9200C00AD9 /* CopyFiles */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t0C8D7BEF1D9DC02B00D12EB7 /* PBXTargetDependency */,\n\t\t\t\t0CB644AE28567FC500916267 /* PBXTargetDependency */,\n\t\t\t\t0C8D7BF41D9DC03A00D12EB7 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C2782062F5299D600F96DB1 /* Mocks */,\n\t\t\t\t0C27822C2F529A5F00F96DB1 /* Resources */,\n\t\t\t\t0C27829C2F529ABD00F96DB1 /* Helpers */,\n\t\t\t\t0C8ECF0C2F4BAF5E00C9F423 /* NukePerformanceTests */,\n\t\t\t);\n\t\t\tname = NukePerformanceTests;\n\t\t\tproductName = \"Nuke iOS Performance Tests\";\n\t\t\tproductReference = 0C8D7BE81D9DC02B00D12EB7 /* NukePerformanceTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t0C91748F1BAE99EE004A7905 /* Nuke */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0C9174981BAE99EE004A7905 /* Build configuration list for PBXNativeTarget \"Nuke\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0C06C96F22E47FD1000AA0A6 /* Lint */,\n\t\t\t\t0C91748B1BAE99EE004A7905 /* Sources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t0C8ECF532F4BB02900C9F423 /* Nuke */,\n\t\t\t);\n\t\t\tname = Nuke;\n\t\t\tproductName = Nuke;\n\t\t\tproductReference = 0C9174901BAE99EE004A7905 /* Nuke.framework */;\n\t\t\tproductType = \"com.apple.product-type.framework\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t0C9174871BAE99EE004A7905 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 1400;\n\t\t\t\tLastUpgradeCheck = 2630;\n\t\t\t\tORGANIZATIONNAME = \"Alexander Grebenyuk\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t0C4F8FDE22E4B6ED0070ECFD = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 10.2.1;\n\t\t\t\t\t};\n\t\t\t\t\t0C55FCFC28567875000FD2C9 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t};\n\t\t\t\t\t0C55FD0328567875000FD2C9 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 0C8D7BCF1D9DBF1600D12EB7;\n\t\t\t\t\t};\n\t\t\t\t\t0C7C06761BCA882A00089D7F = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.0.1;\n\t\t\t\t\t};\n\t\t\t\t\t0C8D7BCF1D9DBF1600D12EB7 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 8.0;\n\t\t\t\t\t\tDevelopmentTeam = NR8DLKJ7E6;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t};\n\t\t\t\t\t0C8D7BE71D9DC02B00D12EB7 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 8.0;\n\t\t\t\t\t\tTestTargetID = 0C8D7BCF1D9DBF1600D12EB7;\n\t\t\t\t\t};\n\t\t\t\t\t0C91748F1BAE99EE004A7905 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.0;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 0C91748A1BAE99EE004A7905 /* Build configuration list for PBXProject \"Nuke\" */;\n\t\t\tcompatibilityVersion = \"Xcode 10.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 0C9174861BAE99EE004A7905;\n\t\t\tproductRefGroup = 0C9174911BAE99EE004A7905 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t0C91748F1BAE99EE004A7905 /* Nuke */,\n\t\t\t\t0C7C06761BCA882A00089D7F /* NukeTests */,\n\t\t\t\t0C4F8FDE22E4B6ED0070ECFD /* NukeThreadSafetyTests */,\n\t\t\t\t0C8D7BE71D9DC02B00D12EB7 /* NukePerformanceTests */,\n\t\t\t\t0C8D7BCF1D9DBF1600D12EB7 /* NukeTestsHost */,\n\t\t\t\t0C38DAA128568FAE0027F9FF /* NukeUI */,\n\t\t\t\t0C38DAE428568FE20027F9FF /* NukeUITests */,\n\t\t\t\t0C55FCFC28567875000FD2C9 /* NukeExtensions */,\n\t\t\t\t0C55FD0328567875000FD2C9 /* NukeExtensionsTests */,\n\t\t\t\t0C75849B29A151FF00F985F8 /* NukeVideo */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t0C38DB1D28568FE20027F9FF /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C4F8FDD22E4B6ED0070ECFD /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C55FD0228567875000FD2C9 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C7C06751BCA882A00089D7F /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C8D7BCE1D9DBF1600D12EB7 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C8D7BE61D9DC02B00D12EB7 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t0C06C96F22E47FD1000AA0A6 /* Lint */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = Lint;\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \".scripts/lint.sh\\n\";\n\t\t};\n\t\t0C38DAA228568FAE0027F9FF /* Lint */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = Lint;\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \".scripts/lint.sh\\n\";\n\t\t};\n\t\t0C75849C29A151FF00F985F8 /* Lint */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = Lint;\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \".scripts/lint.sh\\n\";\n\t\t};\n\t\t0CC04A952C4BD3BB00F1164D /* Lint */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = Lint;\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \".scripts/lint.sh\\n\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t0C38DAA328568FAE0027F9FF /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C222DE3294E2DEA00012288 /* NukeUI.docc in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C38DAE528568FE20027F9FF /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C4F8FDB22E4B6ED0070ECFD /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C55FCF928567875000FD2C9 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C222DE5294E2E0300012288 /* NukeExtensions.docc in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C55FD0028567875000FD2C9 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C75849D29A151FF00F985F8 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C7C06731BCA882A00089D7F /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C8D7BCC1D9DBF1600D12EB7 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C8D7BD31D9DBF1600D12EB7 /* AppDelegate.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C8D7BE41D9DC02B00D12EB7 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0C91748B1BAE99EE004A7905 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C1B9880294E28D800C09310 /* Nuke.docc in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t0C1C202029ABBF19004B38FD /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0C91748F1BAE99EE004A7905 /* Nuke */;\n\t\t\ttargetProxy = 0C1C201F29ABBF19004B38FD /* PBXContainerItemProxy */;\n\t\t};\n\t\t0C38DB4A285690D20027F9FF /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0C38DAA128568FAE0027F9FF /* NukeUI */;\n\t\t\ttargetProxy = 0C38DB49285690D20027F9FF /* PBXContainerItemProxy */;\n\t\t};\n\t\t0C55FD0728567875000FD2C9 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0C55FCFC28567875000FD2C9 /* NukeExtensions */;\n\t\t\ttargetProxy = 0C55FD0628567875000FD2C9 /* PBXContainerItemProxy */;\n\t\t};\n\t\t0C55FD0F28567875000FD2C9 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0C55FCFC28567875000FD2C9 /* NukeExtensions */;\n\t\t\ttargetProxy = 0C55FD0E28567875000FD2C9 /* PBXContainerItemProxy */;\n\t\t};\n\t\t0C8D7BEF1D9DC02B00D12EB7 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0C91748F1BAE99EE004A7905 /* Nuke */;\n\t\t\ttargetProxy = 0C8D7BEE1D9DC02B00D12EB7 /* PBXContainerItemProxy */;\n\t\t};\n\t\t0C8D7BF41D9DC03A00D12EB7 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0C8D7BCF1D9DBF1600D12EB7 /* NukeTestsHost */;\n\t\t\ttargetProxy = 0C8D7BF31D9DC03A00D12EB7 /* PBXContainerItemProxy */;\n\t\t};\n\t\t0CB644AE28567FC500916267 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0C55FCFC28567875000FD2C9 /* NukeExtensions */;\n\t\t\ttargetProxy = 0CB644AD28567FC500916267 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin XCBuildConfiguration section */\n\t\t0C38DADE28568FAE0027F9FF /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = YES;\n\t\t\t\tDOCC_HOSTING_BASE_PATH = /nukeui/;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tOTHER_SWIFT_FLAGS = \"-D DEBUG\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukeui;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C38DADF28568FAE0027F9FF /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = YES;\n\t\t\t\tDOCC_HOSTING_BASE_PATH = /nukeui/;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukeui;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C38DB3228568FE20027F9FF /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tOTHER_SWIFT_FLAGS = \"-D DEBUG\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.nukeui-unit-tests\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C38DB3328568FE20027F9FF /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.nukeui-unit-tests\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C4F8FE522E4B6ED0070ECFD /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.Nuke-Thread-Safety-Tests\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C4F8FE622E4B6ED0070ECFD /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.Nuke-Thread-Safety-Tests\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C55FD1328567875000FD2C9 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = YES;\n\t\t\t\tDOCC_HOSTING_BASE_PATH = /nukeextensions/;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.nuke-extensions\";\n\t\t\t\tPRODUCT_NAME = NukeExtensions;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C55FD1428567875000FD2C9 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = YES;\n\t\t\t\tDOCC_HOSTING_BASE_PATH = /nukeextensions/;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.nuke-extensions\";\n\t\t\t\tPRODUCT_NAME = NukeExtensions;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C55FD1728567875000FD2C9 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\t\"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]\" = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t\t\"@loader_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.kean.NukeExtensionsTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C55FD1828567875000FD2C9 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\t\"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]\" = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t\t\"@loader_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.kean.NukeExtensionsTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C7584A829A151FF00F985F8 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = YES;\n\t\t\t\tDOCC_HOSTING_BASE_PATH = /nukevideo/;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tOTHER_SWIFT_FLAGS = \"-D DEBUG\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukevideo;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C7584A929A151FF00F985F8 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = YES;\n\t\t\t\tDOCC_HOSTING_BASE_PATH = /nukevideo/;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukevideo;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C7C06801BCA882A00089D7F /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 14.6;\n\t\t\t\tOTHER_SWIFT_FLAGS = \"-D DEBUG\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.Nuke-Tests\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C7C06811BCA882A00089D7F /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 14.6;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.Nuke-Tests\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C8D7BDF1D9DBF1600D12EB7 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_TEAM = NR8DLKJ7E6;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.Nuke-iOS-Tests-Host\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C8D7BE01D9DBF1600D12EB7 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_TEAM = NR8DLKJ7E6;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.Nuke-iOS-Tests-Host\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C8D7BF11D9DC02B00D12EB7 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_TEAM = NR8DLKJ7E6;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.Nuke-iOS-Performance-Tests\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphonesimulator iphoneos\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/NukeTestsHost.app/NukeTestsHost\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C8D7BF21D9DC02B00D12EB7 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_TEAM = NR8DLKJ7E6;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.github.kean.Nuke-iOS-Performance-Tests\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphonesimulator iphoneos\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/NukeTestsHost.app/NukeTestsHost\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C9174961BAE99EE004A7905 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.6;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 12.4;\n\t\t\t\tMARKETING_VERSION = 13.0.0;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos\";\n\t\t\t\tSUPPORTS_MACCATALYST = YES;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 6.0;\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 15.6;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t\tVERSION_INFO_PREFIX = \"\";\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 8.7;\n\t\t\t\tXROS_DEPLOYMENT_TARGET = 1.3;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C9174971BAE99EE004A7905 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.6;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 12.4;\n\t\t\t\tMARKETING_VERSION = 13.0.0;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos\";\n\t\t\t\tSUPPORTS_MACCATALYST = YES;\n\t\t\t\tSWIFT_VERSION = 6.0;\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 15.6;\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t\tVERSION_INFO_PREFIX = \"\";\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 8.7;\n\t\t\t\tXROS_DEPLOYMENT_TARGET = 1.3;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0C9174991BAE99EE004A7905 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = YES;\n\t\t\t\tDOCC_HOSTING_BASE_PATH = /nuke/;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tOTHER_SWIFT_FLAGS = \"-D DEBUG\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nuke;\n\t\t\t\tPRODUCT_NAME = Nuke;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0C91749A1BAE99EE004A7905 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = YES;\n\t\t\t\tDOCC_HOSTING_BASE_PATH = /nuke/;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nuke;\n\t\t\t\tPRODUCT_NAME = Nuke;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t0C38DADD28568FAE0027F9FF /* Build configuration list for PBXNativeTarget \"NukeUI\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C38DADE28568FAE0027F9FF /* Debug */,\n\t\t\t\t0C38DADF28568FAE0027F9FF /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C38DB3128568FE20027F9FF /* Build configuration list for PBXNativeTarget \"NukeUITests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C38DB3228568FE20027F9FF /* Debug */,\n\t\t\t\t0C38DB3328568FE20027F9FF /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C4F8FE422E4B6ED0070ECFD /* Build configuration list for PBXNativeTarget \"NukeThreadSafetyTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C4F8FE522E4B6ED0070ECFD /* Debug */,\n\t\t\t\t0C4F8FE622E4B6ED0070ECFD /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C55FD1228567875000FD2C9 /* Build configuration list for PBXNativeTarget \"NukeExtensions\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C55FD1328567875000FD2C9 /* Debug */,\n\t\t\t\t0C55FD1428567875000FD2C9 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C55FD1628567875000FD2C9 /* Build configuration list for PBXNativeTarget \"NukeExtensionsTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C55FD1728567875000FD2C9 /* Debug */,\n\t\t\t\t0C55FD1828567875000FD2C9 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C7584A729A151FF00F985F8 /* Build configuration list for PBXNativeTarget \"NukeVideo\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C7584A829A151FF00F985F8 /* Debug */,\n\t\t\t\t0C7584A929A151FF00F985F8 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C7C067F1BCA882A00089D7F /* Build configuration list for PBXNativeTarget \"NukeTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C7C06801BCA882A00089D7F /* Debug */,\n\t\t\t\t0C7C06811BCA882A00089D7F /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C8D7BE11D9DBF1600D12EB7 /* Build configuration list for PBXNativeTarget \"NukeTestsHost\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C8D7BDF1D9DBF1600D12EB7 /* Debug */,\n\t\t\t\t0C8D7BE01D9DBF1600D12EB7 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C8D7BF01D9DC02B00D12EB7 /* Build configuration list for PBXNativeTarget \"NukePerformanceTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C8D7BF11D9DC02B00D12EB7 /* Debug */,\n\t\t\t\t0C8D7BF21D9DC02B00D12EB7 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C91748A1BAE99EE004A7905 /* Build configuration list for PBXProject \"Nuke\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C9174961BAE99EE004A7905 /* Debug */,\n\t\t\t\t0C9174971BAE99EE004A7905 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0C9174981BAE99EE004A7905 /* Build configuration list for PBXNativeTarget \"Nuke\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0C9174991BAE99EE004A7905 /* Debug */,\n\t\t\t\t0C91749A1BAE99EE004A7905 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 0C9174871BAE99EE004A7905 /* Project object */;\n}\n"
  },
  {
    "path": "Nuke.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:Nuke.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "Nuke.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/Nuke.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C91748F1BAE99EE004A7905\"\n               BuildableName = \"Nuke.framework\"\n               BlueprintName = \"Nuke\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      codeCoverageEnabled = \"YES\"\n      onlyGenerateCoverageForSpecifiedTargets = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C91748F1BAE99EE004A7905\"\n            BuildableName = \"Nuke.framework\"\n            BlueprintName = \"Nuke\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <CodeCoverageTargets>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C91748F1BAE99EE004A7905\"\n            BuildableName = \"Nuke.framework\"\n            BlueprintName = \"Nuke\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </CodeCoverageTargets>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C7C06761BCA882A00089D7F\"\n               BuildableName = \"NukeTests.xctest\"\n               BlueprintName = \"NukeTests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C91748F1BAE99EE004A7905\"\n            BuildableName = \"Nuke.framework\"\n            BlueprintName = \"Nuke\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <EnvironmentVariables>\n         <EnvironmentVariable\n            key = \"NUKE_PRINT_ALL_ALLOCATIONS\"\n            value = \"true\"\n            isEnabled = \"NO\">\n         </EnvironmentVariable>\n         <EnvironmentVariable\n            key = \"NUKE_ALLOCATIONS_PERIODIC_LOG\"\n            value = \"true\"\n            isEnabled = \"YES\">\n         </EnvironmentVariable>\n      </EnvironmentVariables>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C91748F1BAE99EE004A7905\"\n            BuildableName = \"Nuke.framework\"\n            BlueprintName = \"Nuke\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukeExtensions.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C55FCFC28567875000FD2C9\"\n               BuildableName = \"NukeExtensions.framework\"\n               BlueprintName = \"NukeExtensions\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C55FD0328567875000FD2C9\"\n               BuildableName = \"NukeExtensionsTests.xctest\"\n               BlueprintName = \"NukeExtensionsTests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C55FCFC28567875000FD2C9\"\n            BuildableName = \"NukeExtensions.framework\"\n            BlueprintName = \"NukeExtensions\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukeExtensionsTests.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C55FD0328567875000FD2C9\"\n               BuildableName = \"NukeExtensionsTests.xctest\"\n               BlueprintName = \"NukeExtensionsTests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukePerformanceTests.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Release\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C8D7BE71D9DC02B00D12EB7\"\n               BuildableName = \"NukePerformanceTests.xctest\"\n               BlueprintName = \"NukePerformanceTests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukeTests.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      enableThreadSanitizer = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C7C06761BCA882A00089D7F\"\n               BuildableName = \"NukeTests.xctest\"\n               BlueprintName = \"NukeTests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      enableThreadSanitizer = \"YES\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukeTestsHost.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C8D7BCF1D9DBF1600D12EB7\"\n               BuildableName = \"NukeTestsHost.app\"\n               BlueprintName = \"NukeTestsHost\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C55FD0328567875000FD2C9\"\n               BuildableName = \"NukeExtensionsTests.xctest\"\n               BlueprintName = \"NukeExtensionsTests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C8D7BCF1D9DBF1600D12EB7\"\n            BuildableName = \"NukeTestsHost.app\"\n            BlueprintName = \"NukeTestsHost\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C8D7BCF1D9DBF1600D12EB7\"\n            BuildableName = \"NukeTestsHost.app\"\n            BlueprintName = \"NukeTestsHost\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukeThreadSafetyTests.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      enableThreadSanitizer = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C4F8FDE22E4B6ED0070ECFD\"\n               BuildableName = \"NukeThreadSafetyTests.xctest\"\n               BlueprintName = \"NukeThreadSafetyTests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      enableThreadSanitizer = \"YES\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukeUI.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C38DAA128568FAE0027F9FF\"\n               BuildableName = \"NukeUI.framework\"\n               BlueprintName = \"NukeUI\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C38DAE428568FE20027F9FF\"\n               BuildableName = \"NukeUITests.xctest\"\n               BlueprintName = \"NukeUITests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C38DAA128568FAE0027F9FF\"\n            BuildableName = \"NukeUI.framework\"\n            BlueprintName = \"NukeUI\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukeUITests.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C38DAE428568FE20027F9FF\"\n               BuildableName = \"NukeUITests.xctest\"\n               BlueprintName = \"NukeUITests\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Nuke.xcodeproj/xcshareddata/xcschemes/NukeVideo.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2630\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0C75849B29A151FF00F985F8\"\n               BuildableName = \"NukeVideo.framework\"\n               BlueprintName = \"NukeVideo\"\n               ReferencedContainer = \"container:Nuke.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0C75849B29A151FF00F985F8\"\n            BuildableName = \"NukeVideo.framework\"\n            BlueprintName = \"NukeVideo\"\n            ReferencedContainer = \"container:Nuke.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version:6.0\nimport PackageDescription\n\nlet package = Package(\n    name: \"Nuke\",\n    platforms: [\n        .iOS(.v15),\n        .tvOS(.v15),\n        .macOS(.v12),\n        .watchOS(.v8),\n        .visionOS(.v1),\n    ],\n    products: [\n        .library(name: \"Nuke\", targets: [\"Nuke\"]),\n        .library(name: \"NukeUI\", targets: [\"NukeUI\"]),\n        .library(name: \"NukeVideo\", targets: [\"NukeVideo\"]),\n        .library(name: \"NukeExtensions\", targets: [\"NukeExtensions\"])\n    ],\n    targets: [\n        .target(name: \"Nuke\"),\n        .target(name: \"NukeUI\", dependencies: [\"Nuke\"]),\n        .target(name: \"NukeVideo\", dependencies: [\"Nuke\"]),\n        .target(name: \"NukeExtensions\", dependencies: [\"Nuke\"])\n    ]\n)\n"
  },
  {
    "path": "README.md",
    "content": "<br/>\n<img src=\"https://user-images.githubusercontent.com/1567433/114792417-57c1d080-9d56-11eb-8035-dc07cfd7557f.png\" height=\"170px\">\n\n# Image Loading System\n\n<p align=\"left\">\n<img src=\"https://img.shields.io/badge/platforms-iOS%2C%20macOS%2C%20watchOS%2C%20tvOS%2C%20visionOS-lightgrey.svg\">\n<img src=\"https://img.shields.io/badge/Licence-MIT-green\">\n</p>\n\n> *Serving Images Since 2015*\n\nLoad 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.\n\nThe 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.\n\n> **Memory and Disk Cache** · **Image Processing & Decompression** · **Request Coalescing & Priority** · **Prefetching** · **Resumable Downloads** · **Progressive JPEG** · **HEIF, WebP, GIF** · **SwiftUI** · **Async/Await**\n\n## Sponsors\n\n<table>\n  <tr>\n    <td valign=\"center\" align=\"center\">\n        <a href=\"https://proxyman.io\">\n          <img src=\"https://kean.blog/images/logos/proxyman.png\" height=\"50px\" alt=\"Proxyman Logo\">\n          <p>Proxyman</p>\n        </a>\n    </td>\n  </tr>\n</table>\n\n## Documentation\n\nLoad images using `ImagePipeline` from the lean core [**Nuke**](https://kean-docs.github.io/nuke/documentation/nuke) module:\n\n```swift\nfunc loadImage() async throws {\n    let imageTask = ImagePipeline.shared.imageTask(with: url)\n    for await progress in imageTask.progress {\n        // Update progress\n    }\n    imageView.image = try await imageTask.image\n}\n```\n\nOr use the built-in UI components from [**NukeUI**](https://kean-docs.github.io/nukeui/documentation/nukeui/):\n\n```swift\nstruct ContentView: View {\n    var body: some View {\n        LazyImage(url: URL(string: \"https://example.com/image.jpeg\"))\n    }\n}\n```\n\nThe [**Getting Started**](https://kean-docs.github.io/nuke/documentation/nuke/getting-started/) guide is the best place to start. Check out [**Nuke Demo**](https://github.com/kean/NukeDemo) for more examples.\n\n<a href=\"https://kean-docs.github.io/nuke/documentation/nuke/getting-started\">\n<img width=\"690\" alt=\"Nuke Docs\" src=\"https://user-images.githubusercontent.com/1567433/175793167-b7e0c557-b887-444f-b18a-57d6f5ecf01a.png\">\n</a>\n\n## Installation\n\nNuke 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).\n\nThe package ships with four modules that you can install depending on your needs:\n\n|Module|Description|\n|--|--|\n|[**Nuke**](https://kean-docs.github.io/nuke/documentation/nuke)|The lean core framework with `ImagePipeline`, `ImageRequest`, and more|\n|[**NukeUI**](https://kean-docs.github.io/nukeui/documentation/nukeui/)|The UI components: `LazyImage` (SwiftUI) and `ImageView` (UIKit, AppKit)|\n|[**NukeExtensions**](https://kean-docs.github.io/nukeextensions/documentation/nukeextensions/)|The extensions for `UIImageView` (UIKit, AppKit)|\n|[**NukeVideo**](https://kean-docs.github.io/nukevideo/documentation/nukevideo/)|The components for decoding and playing short videos|\n\n## Extensions\n\nThe image pipeline is easy to customize and extend. Check out the following first-class extensions and packages built by the community.\n\n|Name|Description|\n|--|--|\n|[**Alamofire Plugin**](https://github.com/kean/Nuke-Alamofire-Plugin)|Replace networking layer with [Alamofire](https://github.com/Alamofire/Alamofire)|\n|[**NukeWebP**](https://github.com/makleso6/NukeWebP)| **Community**. [WebP](https://developers.google.com/speed/webp/) support, built by [Maxim Kolesnik](https://github.com/makleso6)|\n|[**WebP Plugin**](https://github.com/ryokosuge/Nuke-WebP-Plugin)| **Community**. [WebP](https://developers.google.com/speed/webp/) support, built by [Ryo Kosuge](https://github.com/ryokosuge)|\n|[**AVIF Plugin**](https://github.com/delneg/Nuke-AVIF-Plugin)| **Community**. [AVIF](https://caniuse.com/avif) support, built by [Denis](https://github.com/delneg)|\n|[**RxNuke**](https://github.com/kean/RxNuke)|[RxSwift](https://github.com/ReactiveX/RxSwift) extensions for Nuke with examples|\n\n> Looking for a way to log your network requests, including image requests? Check out [**Pulse**](https://github.com/kean/Pulse).\n\n## Minimum Requirements\n\n> Upgrading from the previous version? Use a [**Migration Guide**](https://github.com/kean/Nuke/tree/master/Documentation/Migrations).\n\n| Nuke      | Swift     | Xcode      | Platforms                                                   |\n|-----------|-----------|------------|-------------------------------------------------------------|\n| Nuke 13.0 | Swift 6.2 | Xcode 26.0 | iOS 15.0, watchOS 8.0, macOS 12.0, tvOS 13.0, visionOS 1.0  |\n| Nuke 12.0 | Swift 5.7 | Xcode 15.0 | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0               |\n\n## License\n\nNuke is available under the MIT license. See the LICENSE file for more info.\n\n----\n\n> <a name=\"footnote-1\">¹</a> Measured on MacBook Pro 14\" 2021 (10-core M1 Pro)\n"
  },
  {
    "path": "Sources/Nuke/Caching/Cache.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if os(iOS) || os(tvOS) || os(visionOS)\nimport UIKit.UIApplication\n#endif\n\n// Internal memory-cache implementation.\nfinal class Cache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {\n    // Can't use `NSCache` because it is not LRU\n\n    struct Configuration {\n        var costLimit: Int\n        var countLimit: Int\n        var ttl: TimeInterval?\n        var entryCostLimit: Double\n    }\n\n    var conf: Configuration {\n        get { lock.withLock { _conf } }\n        set { lock.withLock { _conf = newValue } }\n    }\n\n    private var _conf: Configuration {\n        didSet { _trim() }\n    }\n\n    var totalCost: Int {\n        lock.withLock { _totalCost }\n    }\n\n    var totalCount: Int {\n        lock.withLock { map.count }\n    }\n\n    private var _totalCost = 0\n    private var map = [Key: LinkedList<Entry>.Node]()\n    private let list = LinkedList<Entry>()\n    private let lock = NSLock()\n    private let memoryPressure: DispatchSourceMemoryPressure\n    private var notificationObserver: AnyObject?\n\n    init(costLimit: Int, countLimit: Int) {\n        self._conf = Configuration(costLimit: costLimit, countLimit: countLimit, ttl: nil, entryCostLimit: 0.1)\n\n        self.memoryPressure = DispatchSource.makeMemoryPressureSource(eventMask: [.warning, .critical], queue: .main)\n        self.memoryPressure.setEventHandler { [weak self] in\n            self?.removeAllCachedValues()\n        }\n        self.memoryPressure.resume()\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n        Task {\n            await registerForEnterBackground()\n        }\n#endif\n    }\n\n    deinit {\n        memoryPressure.cancel()\n    }\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n    @MainActor private func registerForEnterBackground() {\n        notificationObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in\n            self?.clearCacheOnEnterBackground()\n        }\n    }\n#endif\n\n    func value(forKey key: Key) -> Value? {\n        lock.lock()\n        defer { lock.unlock() }\n\n        guard let node = map[key] else {\n            return nil\n        }\n\n        guard !node.value.isExpired else {\n            _remove(node: node)\n            return nil\n        }\n\n        // bubble node up to make it last added (most recently used)\n        list.remove(node)\n        list.append(node)\n\n        return node.value.value\n    }\n\n    func set(_ value: Value, forKey key: Key, cost: Int = 0, ttl: TimeInterval? = nil) {\n        lock.lock()\n        defer { lock.unlock() }\n\n        // Take care of overflow or cache size big enough to fit any\n        // reasonable content (and also of costLimit = Int.max).\n        let sanitizedEntryLimit = max(0, min(_conf.entryCostLimit, 1))\n        guard _conf.costLimit > 2_147_483_647 || cost < Int(sanitizedEntryLimit * Double(_conf.costLimit)) else {\n            return\n        }\n\n        let ttl = ttl ?? _conf.ttl\n        let expiration = ttl.map { Date() + $0 }\n        let entry = Entry(value: value, key: key, cost: cost, expiration: expiration)\n        _add(entry)\n        _trim() // _trim is extremely fast, it's OK to call it each time\n    }\n\n    @discardableResult\n    func removeValue(forKey key: Key) -> Value? {\n        lock.lock()\n        defer { lock.unlock() }\n\n        guard let node = map[key] else {\n            return nil\n        }\n        _remove(node: node)\n        return node.value.value\n    }\n\n    private func _add(_ element: Entry) {\n        if let existingNode = map[element.key] {\n            // This is slightly faster than calling _remove because of the\n            // skipped dictionary access\n            list.remove(existingNode)\n            _totalCost -= existingNode.value.cost\n        }\n        map[element.key] = list.append(element)\n        _totalCost += element.cost\n    }\n\n    private func _remove(node: LinkedList<Entry>.Node) {\n        list.remove(node)\n        map[node.value.key] = nil\n        _totalCost -= node.value.cost\n    }\n\n    func removeAllCachedValues() {\n        lock.lock()\n        defer { lock.unlock() }\n\n        map.removeAll()\n        list.removeAllElements()\n        _totalCost = 0\n    }\n\n    private dynamic func clearCacheOnEnterBackground() {\n        // Remove most of the stored items when entering background.\n        // This behavior is similar to `NSCache` (which removes all\n        // items). This feature is not documented and may be subject\n        // to change in future Nuke versions.\n        lock.lock()\n        defer { lock.unlock() }\n\n        _trim(toCost: Int(Double(_conf.costLimit) * 0.1))\n        _trim(toCount: Int(Double(_conf.countLimit) * 0.1))\n    }\n\n    private func _trim() {\n        _trim(toCost: _conf.costLimit)\n        _trim(toCount: _conf.countLimit)\n    }\n\n    func trim(toCost limit: Int) {\n        lock.withLock { _trim(toCost: limit) }\n    }\n\n    private func _trim(toCost limit: Int) {\n        _trim(while: { _totalCost > limit })\n    }\n\n    func trim(toCount limit: Int) {\n        lock.withLock { _trim(toCount: limit) }\n    }\n\n    private func _trim(toCount limit: Int) {\n        _trim(while: { map.count > limit })\n    }\n\n    private func _trim(while condition: () -> Bool) {\n        while condition(), let node = list.first { // least recently used\n            _remove(node: node)\n        }\n    }\n\n    private struct Entry {\n        let value: Value\n        let key: Key\n        let cost: Int\n        let expiration: Date?\n        var isExpired: Bool {\n            guard let expiration else {\n                return false\n            }\n            return expiration.timeIntervalSinceNow < 0\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Caching/DataCache.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// An LRU disk cache that stores data in separate files.\n///\n/// ``DataCache`` uses LRU cleanup policy (least recently used items are removed\n/// first). The elements stored in the cache are automatically discarded if\n/// the size limit is reached. The sweeps are performed periodically.\n///\n/// DataCache always writes and removes data asynchronously. It also allows for\n/// reading and writing data in parallel. It is implemented using a staging\n/// area which stores changes until they are flushed to disk:\n///\n/// ```swift\n/// // Schedules data to be written asynchronously and returns immediately\n/// cache[key] = data\n///\n/// // The data is returned from the staging area\n/// let data = cache[key]\n///\n/// // Schedules data to be removed asynchronously and returns immediately\n/// cache[key] = nil\n///\n/// // Data is nil\n/// let data = cache[key]\n/// ```\n///\n/// - important: It's possible to have more than one instance of ``DataCache`` with\n/// the same path but it is not recommended.\npublic final class DataCache: DataCaching, @unchecked Sendable {\n    /// Size limit in bytes. `150 MB` by default.\n    ///\n    /// Changes to the size limit will take effect when the next LRU sweep is run.\n    public var sizeLimit: Int = 1024 * 1024 * 150\n\n    /// When performing a sweep, the cache will remove entries until the size of\n    /// the remaining items is lower than or equal to `sizeLimit * trimRatio`. `0.7`\n    /// by default.\n    var trimRatio = 0.7\n\n    /// The path for the directory managed by the cache.\n    public let path: URL\n\n    /// The time interval between cache sweeps. The default value is 30 minutes.\n    public var sweepInterval: TimeInterval = 1800\n\n    /// If `false`, the automatic LRU sweep is disabled. The default value is `true`.\n    public var isSweepEnabled: Bool = true\n\n    // Staging\n\n    private let lock = NSLock()\n    private var staging = Staging()\n    private var isFlushNeeded = false\n    private var isFlushScheduled = false\n\n    var flushInterval: DispatchTimeInterval = .seconds(1)\n    var sweepDelay: DispatchTimeInterval = .seconds(5)\n    var onSweepCompleted: (@Sendable () -> Void)?\n\n    private struct Metadata: Codable {\n        var lastSweepDate: Date?\n    }\n\n    /// The queue used for disk I/O.\n    public let queue = DispatchQueue(label: \"com.github.kean.Nuke.DataCache.WriteQueue\", qos: .utility)\n\n    /// A function that generates a filename for the given key. A good candidate\n    /// is a hash function with low collision probability, such as SHA1.\n    ///\n    /// The reason filenames need to be generated is that filesystems have a\n    /// size limit for filenames (e.g. 255 UTF-8 characters in APFS) and do not\n    /// allow certain characters.\n    public typealias FilenameGenerator = (_ key: String) -> String?\n\n    private let filenameGenerator: FilenameGenerator\n\n    /// Creates a cache instance with a given `name`. The cache creates a directory\n    /// with the given `name` in a `.cachesDirectory` in `.userDomainMask`.\n    /// - parameter filenameGenerator: Generates a filename for the given URL.\n    /// The default implementation generates a filename using SHA1 hash function.\n    public convenience init(name: String, filenameGenerator: @escaping (String) -> String? = DataCache.filename(for:)) throws {\n        // This should be replaced with URL.cachesDirectory on iOS 16, which never fails\n        guard let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {\n            throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)\n        }\n        try self.init(path: root.appendingPathComponent(name, isDirectory: true), filenameGenerator: filenameGenerator)\n    }\n\n    /// Creates a cache instance with a given path.\n    /// - parameter filenameGenerator: Generates a filename for the given URL.\n    /// The default implementation generates a filename using SHA1 hash function.\n    public init(path: URL, filenameGenerator: @escaping (String) -> String? = DataCache.filename(for:)) throws {\n        self.path = path\n        self.filenameGenerator = filenameGenerator\n        try self.didInit()\n    }\n\n    init(\n        name: String,\n        filenameGenerator: @escaping (String) -> String? = DataCache.filename(for:),\n        sweepDelay: DispatchTimeInterval,\n        onSweepCompleted: @escaping @Sendable () -> Void\n    ) throws {\n        guard let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {\n            throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)\n        }\n        self.path = root.appendingPathComponent(name, isDirectory: true)\n        self.filenameGenerator = filenameGenerator\n        self.sweepDelay = sweepDelay\n        self.onSweepCompleted = onSweepCompleted\n        try self.didInit()\n    }\n\n    /// A ``FilenameGenerator`` implementation that uses SHA1 to generate a\n    /// filename from the given key.\n    public static func filename(for key: String) -> String? {\n        key.isEmpty ? nil : key.sha1\n    }\n\n    private func didInit() throws {\n        try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true, attributes: nil)\n        scheduleSweep()\n    }\n\n    private func scheduleSweep() {\n        if let lastSweepDate = getMetadata().lastSweepDate,\n           Date().timeIntervalSince(lastSweepDate) < sweepInterval {\n            return // Already completed recently\n        }\n        // Add a bit of a delay to free the resources during launch\n        queue.asyncAfter(deadline: .now() + sweepDelay, qos: .background) { [weak self] in\n            guard let self, self.isSweepEnabled else { return }\n            self.performSweep()\n            self.updateMetadata {\n                $0.lastSweepDate = Date()\n            }\n            self.onSweepCompleted?()\n        }\n    }\n\n    // MARK: DataCaching\n\n    /// Retrieves data for the given key.\n    public func cachedData(for key: String) -> Data? {\n        if let change = change(for: key) {\n            switch change { // Change wasn't flushed to disk yet\n            case let .add(data):\n                return data\n            case .remove:\n                return nil\n            }\n        }\n        guard var url = url(for: key) else {\n            return nil\n        }\n        guard let data = try? Data(contentsOf: url) else {\n            return nil\n        }\n        var values = URLResourceValues()\n        values.contentAccessDate = Date()\n        try? url.setResourceValues(values)\n        return data\n    }\n\n    /// Returns `true` if the cache contains the data for the given key.\n    public func containsData(for key: String) -> Bool {\n        if let change = change(for: key) {\n            switch change { // Change wasn't flushed to disk yet\n            case .add:\n                return true\n            case .remove:\n                return false\n            }\n        }\n        guard let url = url(for: key) else {\n            return false\n        }\n        return FileManager.default.fileExists(atPath: url.path)\n    }\n\n    private func change(for key: String) -> Staging.ChangeType? {\n        lock.withLock { staging.change(for: key) }\n    }\n\n    /// Stores data for the given key. The method returns instantly and the data\n    /// is written asynchronously.\n    public func storeData(_ data: Data, for key: String) {\n        stage { staging.add(data: data, for: key) }\n    }\n\n    /// Removes data for the given key. The method returns instantly, the data\n    /// is removed asynchronously.\n    public func removeData(for key: String) {\n        stage { staging.removeData(for: key) }\n    }\n\n    /// Removes all items. The method returns instantly, the data is removed\n    /// asynchronously.\n    public func removeAll() {\n        stage { staging.removeAllStagedChanges() }\n    }\n\n    private func stage(_ change: () -> Void) {\n        lock.withLock {\n            change()\n            setNeedsFlushChanges()\n        }\n    }\n\n    /// Accesses the data associated with the given key for reading and writing.\n    ///\n    /// When you assign data for a key that already exists, the cache overwrites\n    /// the existing entry. Reads and writes are backed by a staging area, so\n    /// they can occur in parallel without blocking. All writes are flushed to\n    /// disk asynchronously.\n    public subscript(key: String) -> Data? {\n        get {\n            cachedData(for: key)\n        }\n        set {\n            if let data = newValue {\n                storeData(data, for: key)\n            } else {\n                removeData(for: key)\n            }\n        }\n    }\n\n    // MARK: Managing URLs\n\n    /// Uses the filename generator that the cache was initialized with to\n    /// generate and return a filename for the given key.\n    public func filename(for key: String) -> String? {\n        filenameGenerator(key)\n    }\n\n    /// Returns `url` for the given cache key.\n    public func url(for key: String) -> URL? {\n        guard let filename = self.filename(for: key) else { return nil }\n        return self.path.appendingPathComponent(filename, isDirectory: false)\n    }\n\n    // MARK: Flush Changes\n\n    /// Synchronously waits on the caller's thread until all outstanding disk I/O\n    /// operations are finished.\n    public func flush() {\n        queue.sync { self.flushChangesIfNeeded() }\n    }\n\n    /// Synchronously waits on the caller's thread until all outstanding disk I/O\n    /// operations for the given key are finished.\n    public func flush(for key: String) {\n        queue.sync {\n            guard let change = lock.withLock({ staging.changes[key] }) else { return }\n            perform(change)\n            lock.withLock { staging.flushed(change) }\n        }\n    }\n\n    private func setNeedsFlushChanges() {\n        guard !isFlushNeeded else { return }\n        isFlushNeeded = true\n        scheduleNextFlush()\n    }\n\n    private func scheduleNextFlush() {\n        guard !isFlushScheduled else { return }\n        isFlushScheduled = true\n        queue.asyncAfter(deadline: .now() + flushInterval) { self.flushChangesIfNeeded() }\n    }\n\n    private func flushChangesIfNeeded() {\n        // Create a snapshot of the recently made changes\n        let staging: Staging? = lock.withLock {\n            guard isFlushNeeded else { return nil }\n            isFlushNeeded = false\n            return self.staging\n        }\n        guard let staging else { return }\n\n        // Apply the snapshot to disk\n        performChanges(for: staging)\n\n        // Update the staging area and schedule the next flush if needed\n        lock.lock()\n        self.staging.flushed(staging)\n        isFlushScheduled = false\n        if isFlushNeeded {\n            scheduleNextFlush()\n        }\n        lock.unlock()\n    }\n\n    // MARK: - I/O\n\n    private func performChanges(for staging: Staging) {\n        autoreleasepool {\n            if let change = staging.changeRemoveAll {\n                perform(change)\n            }\n            for change in staging.changes.values {\n                perform(change)\n            }\n        }\n    }\n\n    private func perform(_ change: Staging.ChangeRemoveAll) {\n        try? FileManager.default.removeItem(at: self.path)\n        try? FileManager.default.createDirectory(at: self.path, withIntermediateDirectories: true, attributes: nil)\n    }\n\n    /// Performs the IO for the given change.\n    private func perform(_ change: Staging.Change) {\n        guard let url = url(for: change.key) else {\n            return\n        }\n        switch change.type {\n        case let .add(data):\n            do {\n                try data.write(to: url)\n            } catch let error as NSError {\n                guard error.code == CocoaError.fileNoSuchFile.rawValue && error.domain == CocoaError.errorDomain else { return }\n                try? FileManager.default.createDirectory(at: self.path, withIntermediateDirectories: true, attributes: nil)\n                try? data.write(to: url) // re-create a directory and try again\n            }\n        case .remove:\n            try? FileManager.default.removeItem(at: url)\n        }\n    }\n\n    // MARK: Sweep\n\n    /// Synchronously performs a cache sweep and removes the least recently used\n    /// items that no longer fit in the cache.\n    public func sweep() {\n        queue.sync { self.performSweep() }\n    }\n\n    /// Discards the least recently used items first.\n    private func performSweep() {\n        var items = contents(keys: [.contentAccessDateKey, .totalFileAllocatedSizeKey])\n        guard !items.isEmpty else {\n            return\n        }\n        var size = items.reduce(0) { $0 + ($1.meta.totalFileAllocatedSize ?? 0) }\n\n        guard size > sizeLimit else {\n            return // All good, no need to perform any work.\n        }\n\n        let targetSizeLimit = Int(Double(sizeLimit) * trimRatio)\n\n        // Most recently accessed items first\n        let past = Date.distantPast\n        items.sort { // Sort in place\n            ($0.meta.contentAccessDate ?? past) > ($1.meta.contentAccessDate ?? past)\n        }\n\n        // Remove the items until it satisfies both size and count limits.\n        while size > targetSizeLimit, let item = items.popLast() {\n            size -= (item.meta.totalFileAllocatedSize ?? 0)\n            try? FileManager.default.removeItem(at: item.url)\n        }\n    }\n\n    // MARK: Contents\n\n    struct Entry {\n        let url: URL\n        let meta: URLResourceValues\n    }\n\n    func contents(keys: [URLResourceKey] = []) -> [Entry] {\n        guard let urls = try? FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: keys, options: .skipsHiddenFiles) else {\n            return []\n        }\n        let keys = Set(keys)\n        return urls.compactMap {\n            guard let meta = try? $0.resourceValues(forKeys: keys) else {\n                return nil\n            }\n            return Entry(url: $0, meta: meta)\n        }\n    }\n\n    // MARK: Metadata\n\n    private func getMetadata() -> Metadata {\n        if let data = try? Data(contentsOf: metadataFileURL),\n           let metadata = try? JSONDecoder().decode(Metadata.self, from: data) {\n            return metadata\n        }\n        return Metadata()\n    }\n\n    private func updateMetadata(_ closure: (inout Metadata) -> Void) {\n        var metadata = getMetadata()\n        closure(&metadata)\n        try? JSONEncoder().encode(metadata).write(to: metadataFileURL)\n    }\n\n    private var metadataFileURL: URL {\n        path.appendingPathComponent(\".data-cache-info\", isDirectory: false)\n    }\n\n    // MARK: Inspection\n\n    /// The total number of items in the cache.\n    ///\n    /// - important: Requires disk IO, avoid using from the main thread.\n    public var totalCount: Int {\n        contents().count\n    }\n\n    /// The total file size of items written on disk.\n    ///\n    /// Uses `URLResourceKey.fileSizeKey` to calculate the size of each entry.\n    /// The total allocated size (see ``totalAllocatedSize``) on disk might\n    /// actually be bigger.\n    ///\n    /// - important: Requires disk IO, avoid using from the main thread.\n    public var totalSize: Int {\n        contents(keys: [.fileSizeKey]).reduce(0) {\n            $0 + ($1.meta.fileSize ?? 0)\n        }\n    }\n\n    /// The total file allocated size of all the items written on disk.\n    ///\n    /// Uses `URLResourceKey.totalFileAllocatedSizeKey`.\n    ///\n    /// - important: Requires disk IO, avoid using from the main thread.\n    public var totalAllocatedSize: Int {\n        contents(keys: [.totalFileAllocatedSizeKey]).reduce(0) {\n            $0 + ($1.meta.totalFileAllocatedSize ?? 0)\n        }\n    }\n}\n\n// MARK: - Staging\n\n/// DataCache allows for parallel reads and writes. This is made possible by\n/// DataCacheStaging.\n///\n/// For example, when the data is added in cache, it is first added to staging\n/// and is removed from staging only after data is written to disk. Removal works\n/// the same way.\nprivate struct Staging {\n    private(set) var changes = [String: Change]()\n    private(set) var changeRemoveAll: ChangeRemoveAll?\n\n    struct ChangeRemoveAll {\n        let id: Int\n    }\n\n    struct Change {\n        let key: String\n        let id: Int\n        let type: ChangeType\n    }\n\n    enum ChangeType {\n        case add(Data)\n        case remove\n    }\n\n    private var nextChangeId = 0\n\n    // MARK: Changes\n\n    func change(for key: String) -> ChangeType? {\n        if let change = changes[key] {\n            return change.type\n        }\n        if changeRemoveAll != nil {\n            return .remove\n        }\n        return nil\n    }\n\n    // MARK: Register Changes\n\n    mutating func add(data: Data, for key: String) {\n        nextChangeId += 1\n        changes[key] = Change(key: key, id: nextChangeId, type: .add(data))\n    }\n\n    mutating func removeData(for key: String) {\n        nextChangeId += 1\n        changes[key] = Change(key: key, id: nextChangeId, type: .remove)\n    }\n\n    mutating func removeAllStagedChanges() {\n        nextChangeId += 1\n        changeRemoveAll = ChangeRemoveAll(id: nextChangeId)\n        changes.removeAll()\n    }\n\n    // MARK: Flush Changes\n\n    mutating func flushed(_ staging: Staging) {\n        for change in staging.changes.values {\n            flushed(change)\n        }\n        if let change = staging.changeRemoveAll {\n            flushed(change)\n        }\n    }\n\n    mutating func flushed(_ change: Change) {\n        if let index = changes.index(forKey: change.key),\n            changes[index].value.id == change.id {\n            changes.remove(at: index)\n        }\n    }\n\n    mutating func flushed(_ change: ChangeRemoveAll) {\n        if changeRemoveAll?.id == change.id {\n            changeRemoveAll = nil\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Caching/DataCaching.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Data cache.\n///\n/// - important: The implementation must be thread safe.\npublic protocol DataCaching: Sendable {\n    /// Retrieves data from cache for the given key.\n    func cachedData(for key: String) -> Data?\n\n    /// Returns `true` if the cache contains data for the given key.\n    func containsData(for key: String) -> Bool\n\n    /// Stores data for the given key.\n    /// - note: The implementation must return immediately and store data\n    /// asynchronously.\n    func storeData(_ data: Data, for key: String)\n\n    /// Removes data for the given key.\n    func removeData(for key: String)\n\n    /// Removes all items.\n    func removeAll()\n}\n"
  },
  {
    "path": "Sources/Nuke/Caching/ImageCache.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n#if !os(macOS)\nimport UIKit\n#else\nimport Cocoa\n#endif\n\n/// An LRU memory cache.\n///\n/// The elements stored in cache are automatically discarded if either *cost* or\n/// *count* limit is reached. The default cost limit represents a number of bytes\n/// and is calculated based on the amount of physical memory available on the\n/// device. The default count limit is set to `Int.max`.\n///\n/// ``ImageCache`` automatically removes all stored elements when it receives a\n/// memory warning. It also automatically removes *most* stored elements\n/// when the app enters the background.\npublic final class ImageCache: ImageCaching {\n    private let impl: Cache<ImageCacheKey, ImageContainer>\n\n    /// The maximum total cost that the cache can hold.\n    public var costLimit: Int {\n        get { impl.conf.costLimit }\n        set { impl.conf.costLimit = newValue }\n    }\n\n    /// The maximum number of items that the cache can hold.\n    public var countLimit: Int {\n        get { impl.conf.countLimit }\n        set { impl.conf.countLimit = newValue }\n    }\n\n    /// Default TTL (time to live) for each entry. Can be used to make sure that\n    /// the entries get validated at some point. `nil` (never expire) by default.\n    public var ttl: TimeInterval? {\n        get { impl.conf.ttl }\n        set { impl.conf.ttl = newValue }\n    }\n\n    /// The maximum cost of an entry in proportion to the ``costLimit``.\n    /// By default, `0.1`.\n    public var entryCostLimit: Double {\n        get { impl.conf.entryCostLimit }\n        set { impl.conf.entryCostLimit = newValue }\n    }\n\n    /// The total number of items in the cache.\n    public var totalCount: Int { impl.totalCount }\n\n    /// The total cost of items in the cache.\n    public var totalCost: Int { impl.totalCost }\n\n    /// The shared ``ImageCache`` instance.\n    public static let shared = ImageCache()\n\n    /// Initializes an ``ImageCache`` instance.\n    /// - parameter costLimit: The cost limit in bytes. Defaults to a value\n    /// calculated based on the amount of physical memory available on the device.\n    /// - parameter countLimit: `Int.max` by default.\n    public init(costLimit: Int = ImageCache.defaultCostLimit, countLimit: Int = Int.max) {\n        impl = Cache(costLimit: costLimit, countLimit: countLimit)\n    }\n\n    /// Returns a cost limit computed based on the amount of the physical memory\n    /// available on the device. The limit is set to 15% of the device's physical\n    /// memory with no hard cap. The cache uses a custom LRU eviction policy that\n    /// enforces this limit precisely, unlike `NSCache` which treats cost limits\n    /// as hints.\n    public static var defaultCostLimit: Int {\n        Int(Double(ProcessInfo.processInfo.physicalMemory) * 0.15)\n    }\n\n    public subscript(key: ImageCacheKey) -> ImageContainer? {\n        get { impl.value(forKey: key) }\n        set {\n            if let image = newValue {\n                impl.set(image, forKey: key, cost: cost(for: image))\n            } else {\n                impl.removeValue(forKey: key)\n            }\n        }\n    }\n\n    /// Removes all cached images.\n    public func removeAll() {\n        impl.removeAllCachedValues()\n    }\n\n    /// Removes least recently used items from the cache until the total cost\n    /// of the remaining items is less than the given cost limit.\n    public func trim(toCost limit: Int) {\n        impl.trim(toCost: limit)\n    }\n\n    /// Removes least recently used items from the cache until the total count\n    /// of the remaining items is less than the given count limit.\n    public func trim(toCount limit: Int) {\n        impl.trim(toCount: limit)\n    }\n\n    /// Returns cost for the given image by approximating its bitmap size in bytes in memory.\n    func cost(for container: ImageContainer) -> Int {\n        let dataCost = container.data?.count ?? 0\n\n        // bytesPerRow * height gives a rough estimation of how much memory\n        // image uses in bytes. In practice this algorithm combined with a\n        // conservative default cost limit works OK.\n        guard let cgImage = container.image.cgImage else {\n            return 1 + dataCost\n        }\n        return cgImage.bytesPerRow * cgImage.height + dataCost\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Caching/ImageCaching.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// In-memory image cache.\n///\n/// - important: The implementation must be thread safe.\npublic protocol ImageCaching: AnyObject, Sendable {\n    /// Access the image cached for the given request.\n    subscript(key: ImageCacheKey) -> ImageContainer? { get set }\n\n    /// Removes all cached items.\n    func removeAll()\n}\n\n/// An opaque container that acts as a memory cache key.\n///\n/// Typically, you don't construct this directly - use the ``ImagePipeline`` or\n/// ``ImagePipeline/Cache-swift.struct`` APIs instead.\npublic struct ImageCacheKey: Hashable, Sendable {\n    let key: Inner\n\n    // This is faster than using AnyHashable (and it shows in performance tests).\n    enum Inner: Hashable, Sendable {\n        case custom(String)\n        case `default`(MemoryCacheKey)\n    }\n\n    public init(key: String) {\n        self.key = .custom(key)\n    }\n\n    public init(request: ImageRequest) {\n        self.key = .default(MemoryCacheKey(request))\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Decoding/AssetType.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// A uniform type identifier (UTI).\npublic struct AssetType: ExpressibleByStringLiteral, Hashable, Sendable {\n    public let rawValue: String\n\n    public init(rawValue: String) {\n        self.rawValue = rawValue\n    }\n\n    public init(stringLiteral value: String) {\n        self.rawValue = value\n    }\n\n    /// PNG (Portable Network Graphics).\n    public static let png: AssetType = \"public.png\"\n\n    /// JPEG.\n    public static let jpeg: AssetType = \"public.jpeg\"\n\n    /// GIF (Graphics Interchange Format).\n    public static let gif: AssetType = \"com.compuserve.gif\"\n\n    /// HEIF (High Efficiency Image Format) by Apple.\n    public static let heic: AssetType = \"public.heic\"\n\n    /// WebP.\n    ///\n    /// Native decoding support only available on the following platforms: macOS 11,\n    /// iOS 14, watchOS 7, tvOS 14.\n    public static let webp: AssetType = \"public.webp\"\n\n    /// MPEG-4 video.\n    public static let mp4: AssetType = \"public.mpeg4\"\n\n    /// M4V video container developed by Apple, similar to MP4. May optionally\n    /// be protected by DRM copy protection.\n    public static let m4v: AssetType = \"public.m4v\"\n\n    /// QuickTime movie.\n    public static let mov: AssetType = \"public.mov\"\n\n    /// ICO (Windows icon format).\n    public static let ico: AssetType = \"com.microsoft.ico\"\n}\n\nextension AssetType {\n    /// Determines a type of the image based on the given data.\n    public init?(_ data: Data) {\n        guard let type = AssetType.make(data) else {\n            return nil\n        }\n        self = type\n    }\n\n    private static func make(_ data: Data) -> AssetType? {\n        func _match(_ numbers: [UInt8?], offset: Int = 0) -> Bool {\n            guard data.count >= numbers.count + offset else {\n                return false\n            }\n            return zip(numbers.indices, numbers).allSatisfy { index, number in\n                guard let number else { return true }\n                guard let index = data.index(data.startIndex, offsetBy: index + offset, limitedBy: data.endIndex) else {\n                    return false\n                }\n                return data[index] == number\n            }\n        }\n\n        // JPEG magic numbers https://en.wikipedia.org/wiki/JPEG\n        if _match([0xFF, 0xD8, 0xFF]) { return .jpeg }\n\n        // PNG Magic numbers https://en.wikipedia.org/wiki/Portable_Network_Graphics\n        if _match([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) { return .png }\n\n        // GIF magic numbers https://en.wikipedia.org/wiki/GIF\n        if _match([0x47, 0x49, 0x46]) { return .gif }\n\n        // WebP magic numbers https://en.wikipedia.org/wiki/List_of_file_signatures\n        if _match([0x52, 0x49, 0x46, 0x46, nil, nil, nil, nil, 0x57, 0x45, 0x42, 0x50]) { return .webp }\n\n        if _match([0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63], offset: 4) { return .heic }\n\n        // see https://stackoverflow.com/questions/21879981/avfoundation-avplayer-supported-formats-no-vob-or-mpg-containers\n        // https://en.wikipedia.org/wiki/List_of_file_signatures\n        if _match([0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D], offset: 4) { return .mp4 }\n\n        // https://www.garykessler.net/library/file_sigs.html\n        if _match([0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32], offset: 4) { return .m4v }\n\n        if _match([0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x56, 0x20], offset: 4) { return .m4v }\n\n        // MOV magic numbers https://www.garykessler.net/library/file_sigs.html\n        if _match([0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20], offset: 4) { return .mov }\n\n        // ICO magic numbers https://en.wikipedia.org/wiki/ICO_(file_format)\n        if _match([0x00, 0x00, 0x01, 0x00]) { return .ico }\n\n        // Either not enough data, or we just don't support this format.\n        return nil\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Decoding/ImageDecoderRegistry.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// A registry of image codecs.\npublic final class ImageDecoderRegistry: @unchecked Sendable {\n    /// A shared registry.\n    public static let shared = ImageDecoderRegistry()\n\n    private var matches = [(ImageDecodingContext) -> (any ImageDecoding)?]()\n    private let lock = NSLock()\n\n    /// Initializes a custom registry.\n    public init() {\n        register(ImageDecoders.Default.init)\n    }\n\n    /// Returns a decoder that matches the given context.\n    public func decoder(for context: ImageDecodingContext) -> (any ImageDecoding)? {\n        lock.lock()\n        defer { lock.unlock() }\n\n        for match in matches.reversed() {\n            if let decoder = match(context) {\n                return decoder\n            }\n        }\n        return nil\n    }\n\n    /// Registers a decoder to be used in a given decoding context.\n    ///\n    /// **Progressive Decoding**\n    ///\n    /// The decoder is created once and is used for the entire decoding session,\n    /// including progressively decoded images. If the decoder doesn't support\n    /// progressive decoding, return `nil` when `isCompleted` is `false`.\n    public func register(_ match: @escaping (ImageDecodingContext) -> (any ImageDecoding)?) {\n        lock.lock()\n        defer { lock.unlock() }\n\n        matches.append(match)\n    }\n\n    /// Removes all registered decoders.\n    public func clear() {\n        lock.lock()\n        defer { lock.unlock() }\n\n        matches = []\n    }\n}\n\n/// Image decoding context used when selecting which decoder to use.\npublic struct ImageDecodingContext: Sendable {\n    public var request: ImageRequest\n    public var data: Data\n    /// Returns `true` if the download was completed.\n    public var isCompleted: Bool\n    public var urlResponse: URLResponse?\n    public var cacheType: ImageResponse.CacheType?\n    /// The preview policy for progressive decoding. Set by the pipeline\n    /// delegate for partial data; defaults to `.incremental`.\n    public var previewPolicy: ImagePipeline.PreviewPolicy = .incremental\n    /// The maximum decoded image size in bytes before automatic downscaling.\n    /// `nil` disables the check. Set by the pipeline from its configuration.\n    public var maximumDecodedImageSize: Int?\n\n    public init(request: ImageRequest, data: Data, isCompleted: Bool = true, urlResponse: URLResponse? = nil, cacheType: ImageResponse.CacheType? = nil) {\n        self.request = request\n        self.data = data\n        self.isCompleted = isCompleted\n        self.urlResponse = urlResponse\n        self.cacheType = cacheType\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Decoding/ImageDecoders+Default.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\n#if !os(macOS)\nimport UIKit\n#else\nimport Cocoa\n#endif\n\nimport ImageIO\n\n/// A namespace with all available decoders.\npublic enum ImageDecoders {}\n\nextension ImageDecoders {\n\n    /// A decoder that supports all of the formats natively supported by the system.\n    ///\n    /// - note: The decoder automatically sets the scale of the decoded images to\n    /// match the scale of the screen.\n    ///\n    /// - note: The default decoder supports progressive JPEG. It produces a new\n    /// preview every time it encounters a new full frame.\n    public final class Default: ImageDecoding, @unchecked Sendable {\n        private(set) var numberOfScans = 0\n        private var incrementalSource: CGImageSource?\n\n        private var isPreviewForGIFGenerated = false\n        private var didAttemptThumbnailFallback = false\n        private var scale: CGFloat = 1.0\n        private var thumbnail: ImageRequest.ThumbnailOptions?\n        private(set) var previewPolicy: ImagePipeline.PreviewPolicy = .incremental\n        private var maximumDecodedImageSize: Int?\n        private let lock = NSLock()\n\n        /// Returns `true` when thumbnail decoding is requested, because\n        /// thumbnail generation requires reading image dimensions from disk and\n        /// must run on the decoding queue rather than blocking the pipeline queue.\n        public var isAsynchronous: Bool { thumbnail != nil }\n\n        public init() { }\n\n        public init?(context: ImageDecodingContext) {\n            self.scale = CGFloat(context.request.scale)\n            self.thumbnail = context.request.thumbnail\n            self.previewPolicy = context.previewPolicy\n            self.maximumDecodedImageSize = context.maximumDecodedImageSize\n        }\n\n        public func decode(_ data: Data) throws -> ImageContainer {\n            lock.lock()\n            defer { lock.unlock() }\n\n            func makeImage() -> PlatformImage? {\n                if let thumbnail {\n                    return makeThumbnail(data: data, options: thumbnail, scale: scale)\n                }\n                return _decodeDownscalingIfNeeded(data) ?? ImageDecoders.Default._decode(data, scale: scale)\n            }\n            guard let image = makeImage() else {\n                throw ImageDecodingError.unknown\n            }\n            let type = AssetType(data)\n            var container = ImageContainer(image: image)\n            container.type = type\n            if type == .gif {\n                container.data = data\n            }\n            if numberOfScans > 0 {\n                container.userInfo[.scanNumberKey] = numberOfScans\n            }\n            if thumbnail != nil {\n                container.userInfo[.isThumbnailKey] = true\n            }\n            return container\n        }\n\n        public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? {\n            lock.lock()\n            defer { lock.unlock() }\n\n            let assetType = AssetType(data)\n\n            // GIF preview is always allowed regardless of policy\n            if assetType == .gif {\n                if !isPreviewForGIFGenerated, let image = ImageDecoders.Default._decode(data, scale: scale) {\n                    isPreviewForGIFGenerated = true\n                    return ImageContainer(image: image, type: .gif, isPreview: true, userInfo: [:])\n                }\n                return nil\n            }\n\n            switch previewPolicy {\n            case .disabled:\n                return nil\n\n            case .thumbnail:\n                if numberOfScans > 0 { return nil } // Already generated\n                guard let source = CGImageSourceCreateWithData(data as CFData, nil),\n                      let thumb = CGImageSourceCreateThumbnailAtIndex(source, 0, [\n                          kCGImageSourceCreateThumbnailFromImageAlways: false,\n                          kCGImageSourceCreateThumbnailFromImageIfAbsent: false\n                      ] as CFDictionary) else {\n                    return nil\n                }\n                numberOfScans += 1\n                let image = ImageDecoders.Default._make(thumb, scale: scale)\n                return ImageContainer(image: image, type: assetType, isPreview: true, userInfo: [.scanNumberKey: numberOfScans])\n\n            case .incremental:\n                if incrementalSource == nil {\n                    incrementalSource = CGImageSourceCreateIncremental(nil)\n                }\n\n                let source = incrementalSource!\n                CGImageSourceUpdateData(source, data as CFData, false)\n\n                // Check that Image I/O has parsed the image dimensions before\n                // attempting to create a (potentially expensive) CGImage.\n                guard _hasImageDimensions(source) else {\n                    // Fallback: for JPEGs with large EXIF headers, the\n                    // incremental source may never produce dimensions. Try\n                    // generating a thumbnail from a non-incremental source once.\n                    return _thumbnailFallback(data: data, assetType: assetType)\n                }\n\n                guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else {\n                    return nil\n                }\n\n                numberOfScans += 1\n\n                let image = ImageDecoders.Default._make(cgImage, scale: scale)\n                return ImageContainer(image: image, type: assetType, isPreview: true, userInfo: [.scanNumberKey: numberOfScans])\n            }\n        }\n    }\n}\n\nextension ImageDecoders.Default {\n    /// Attempts to generate a thumbnail from a non-incremental source when\n    /// `CGImageSourceCreateIncremental` can't parse the image (e.g. JPEGs\n    /// with large EXIF headers). Only tried once per decoder instance.\n    private func _thumbnailFallback(data: Data, assetType: AssetType?) -> ImageContainer? {\n        guard !didAttemptThumbnailFallback else { return nil }\n\n        guard let source = CGImageSourceCreateWithData(data as CFData, nil),\n              let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, [\n                  kCGImageSourceCreateThumbnailFromImageIfAbsent: true,\n                  kCGImageSourceThumbnailMaxPixelSize: 160\n              ] as CFDictionary) else {\n            return nil\n        }\n        didAttemptThumbnailFallback = true\n        numberOfScans += 1\n        let image = ImageDecoders.Default._make(cgImage, scale: scale)\n        return ImageContainer(image: image, type: assetType, isPreview: true, userInfo: [.scanNumberKey: numberOfScans])\n    }\n\n    /// Returns `true` if Image I/O has parsed non-zero pixel dimensions for the\n    /// first image in the source. Checking this before calling\n    /// `CGImageSourceCreateImageAtIndex` avoids an expensive no-op when the\n    /// source doesn't have enough data yet.\n    private func _hasImageDimensions(_ source: CGImageSource) -> Bool {\n        guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {\n            return false\n        }\n        let width = properties[kCGImagePropertyPixelWidth] as? Int ?? 0\n        let height = properties[kCGImagePropertyPixelHeight] as? Int ?? 0\n        return width > 0 && height > 0\n    }\n\n    private static func _decode(_ data: Data, scale: CGFloat) -> PlatformImage? {\n#if os(macOS)\n        return NSImage(data: data)\n#else\n        return UIImage(data: data, scale: scale)\n#endif\n    }\n\n    private static func _make(_ cgImage: CGImage, scale: CGFloat) -> PlatformImage {\n#if os(macOS)\n        return NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))\n#else\n        return UIImage(cgImage: cgImage, scale: scale, orientation: .up)\n#endif\n    }\n\n    /// Decodes the image at a reduced resolution if the estimated decoded\n    /// bitmap size exceeds `maximumDecodedImageSize`. Returns `nil` when\n    /// downscaling is not needed (or disabled).\n    private func _decodeDownscalingIfNeeded(_ data: Data) -> PlatformImage? {\n        guard let limit = maximumDecodedImageSize, limit > 0 else { return nil }\n\n        let bytesPerPixel = 4\n        let limitPixels = limit / bytesPerPixel\n\n        guard let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary),\n              let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],\n              let width = properties[kCGImagePropertyPixelWidth] as? Int,\n              let height = properties[kCGImagePropertyPixelHeight] as? Int else {\n            return nil\n        }\n\n        let totalPixels = width * height\n        guard totalPixels > limitPixels else { return nil }\n\n        let ratio = sqrt(Double(limitPixels) / Double(totalPixels))\n        let maxPixelSize = Int(ratio * Double(max(width, height)))\n\n        let options: [CFString: Any] = [\n            kCGImageSourceCreateThumbnailFromImageAlways: true,\n            kCGImageSourceCreateThumbnailWithTransform: true,\n            kCGImageSourceShouldCacheImmediately: true,\n            kCGImageSourceThumbnailMaxPixelSize: maxPixelSize\n        ]\n        guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {\n            return nil\n        }\n        return makeImage(from: cgImage, source: source, scale: scale)\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Decoding/ImageDecoders+Empty.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\nextension ImageDecoders {\n    /// A decoder that returns an empty placeholder image and attaches image\n    /// data to the image container.\n    public struct Empty: ImageDecoding, Sendable {\n        public let isProgressive: Bool\n        private let assetType: AssetType?\n\n        public var isAsynchronous: Bool { false }\n\n        /// Initializes the decoder.\n        ///\n        /// - parameters:\n        ///   - type: Image type to be associated with an image container.\n        ///   `nil` by default.\n        ///   - isProgressive: If `false`, returns `nil` for every progressive\n        ///   scan. `false` by default.\n        public init(assetType: AssetType? = nil, isProgressive: Bool = false) {\n            self.assetType = assetType\n            self.isProgressive = isProgressive\n        }\n\n        public func decode(_ data: Data) throws -> ImageContainer {\n            ImageContainer(image: PlatformImage(), type: assetType, data: data, userInfo: [:])\n        }\n\n        public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? {\n            isProgressive ? ImageContainer(image: PlatformImage(), type: assetType, data: data, userInfo: [:]) : nil\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Decoding/ImageDecoding.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// An image decoder.\n///\n/// A decoder is a one-shot object created for a single image decoding session.\n///\n/// - note: If you need additional information in the decoder, you can pass\n/// anything that you might need from the ``ImageDecodingContext``.\npublic protocol ImageDecoding: Sendable {\n    /// Returns `true` if you want the decoding to be performed on the decoding\n    /// queue (see ``ImagePipeline/Configuration-swift.struct/imageDecodingQueue``). If `false`, the decoding will be\n    /// performed synchronously on the pipeline operation queue. By default, `true`.\n    var isAsynchronous: Bool { get }\n\n    /// Produces an image from the given image data.\n    func decode(_ data: Data) throws -> ImageContainer\n\n    /// Produces an image from the given partially downloaded image data.\n    /// This method might be called multiple times during a single decoding\n    /// session. When the image download is complete, ``decode(_:)`` method is called.\n    ///\n    /// - returns: nil by default.\n    func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer?\n}\n\nextension ImageDecoding {\n    /// Returns `true` by default.\n    public var isAsynchronous: Bool { true }\n\n    /// The default implementation which simply returns `nil` (no progressive\n    /// decoding available).\n    public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { nil }\n}\n\npublic enum ImageDecodingError: Error, CustomStringConvertible, Sendable {\n    case unknown\n\n    public var description: String { \"Unknown\" }\n}\n\nextension ImageDecoding {\n    func decode(_ context: ImageDecodingContext) throws -> ImageResponse {\n        let container: ImageContainer = try autoreleasepool {\n            if context.isCompleted {\n                return try decode(context.data)\n            } else {\n                if let preview = decodePartiallyDownloadedData(context.data) {\n                    return preview\n                }\n                throw ImageDecodingError.unknown\n            }\n        }\n#if !os(macOS)\n        if container.userInfo[.isThumbnailKey] == nil && !container.isPreview {\n            ImageDecompression.setDecompressionNeeded(true, for: container.image)\n        }\n#endif\n        return ImageResponse(container: container, request: context.request, urlResponse: context.urlResponse, cacheType: context.cacheType)\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Encoding/ImageEncoders+Default.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageEncoders {\n    /// A default adaptive encoder which uses best encoder available depending\n    /// on the input image and its configuration.\n    public struct Default: ImageEncoding {\n        public var compressionQuality: Float\n\n        /// Set to `true` to switch to HEIF when it is available on the current hardware.\n        /// `false` by default.\n        public var isHEIFPreferred = false\n\n        public init(compressionQuality: Float = 0.8) {\n            self.compressionQuality = compressionQuality\n        }\n\n        public func encode(_ image: PlatformImage) -> Data? {\n            guard let cgImage = image.cgImage else {\n                return nil\n            }\n            let type: AssetType\n            if cgImage.isOpaque {\n                if isHEIFPreferred && ImageEncoders.ImageIO.isSupported(type: .heic) {\n                    type = .heic\n                } else {\n                    type = .jpeg\n                }\n            } else {\n                type = .png\n            }\n            let encoder = ImageEncoders.ImageIO(type: type, compressionRatio: compressionQuality)\n            return encoder.encode(image)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport CoreGraphics\nimport ImageIO\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageEncoders {\n    /// An Image I/O based encoder.\n    ///\n    /// Image I/O is a system framework that allows applications to read and\n    /// write most image file formats. This framework offers high efficiency,\n    /// color management, and access to image metadata.\n    public struct ImageIO: ImageEncoding {\n        public let type: AssetType\n        public let compressionRatio: Float\n\n        /// - parameter format: The output format. Make sure that the format is\n        /// supported on the current hardware.\n        /// - parameter compressionRatio: 0.8 by default.\n        public init(type: AssetType, compressionRatio: Float = 0.8) {\n            self.type = type\n            self.compressionRatio = compressionRatio\n        }\n\n        private static let availability = Mutex<[AssetType: Bool]>(value: [:])\n\n        /// Returns `true` if the encoding is available for the given format on\n        /// the current hardware. Some of the most recent formats might not be\n        /// available so its best to check before using them.\n        public static func isSupported(type: AssetType) -> Bool {\n            if let isAvailable = availability.value[type] {\n                return isAvailable\n            }\n            let isAvailable = CGImageDestinationCreateWithData(\n                NSMutableData() as CFMutableData, type.rawValue as CFString, 1, nil\n            ) != nil\n            availability.withLock { $0[type] = isAvailable }\n            return isAvailable\n        }\n\n        public func encode(_ image: PlatformImage) -> Data? {\n            guard let source = image.cgImage,\n                let data = CFDataCreateMutable(nil, 0),\n                  let destination = CGImageDestinationCreateWithData(data, type.rawValue as CFString, 1, nil) else {\n                return nil\n            }\n            var options: [CFString: Any] = [\n                kCGImageDestinationLossyCompressionQuality: compressionRatio\n            ]\n#if canImport(UIKit)\n            options[kCGImagePropertyOrientation] = CGImagePropertyOrientation(image.imageOrientation).rawValue\n#endif\n            CGImageDestinationAddImage(destination, source, options as CFDictionary)\n            guard CGImageDestinationFinalize(destination) else {\n                return nil\n            }\n            return data as Data\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Encoding/ImageEncoders.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// A namespace with all available encoders.\npublic enum ImageEncoders {}\n\nextension ImageEncoding where Self == ImageEncoders.Default {\n    public static func `default`(compressionQuality: Float = 0.8) -> ImageEncoders.Default {\n        ImageEncoders.Default(compressionQuality: compressionQuality)\n    }\n}\n\nextension ImageEncoding where Self == ImageEncoders.ImageIO {\n    public static func imageIO(type: AssetType, compressionRatio: Float = 0.8) -> ImageEncoders.ImageIO {\n        ImageEncoders.ImageIO(type: type, compressionRatio: compressionRatio)\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Encoding/ImageEncoding.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\nimport ImageIO\n\n// MARK: - ImageEncoding\n\n/// An image encoder.\npublic protocol ImageEncoding: Sendable {\n    /// Encodes the given image.\n    func encode(_ image: PlatformImage) -> Data?\n\n    /// An optional method which encodes the given image container.\n    func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data?\n}\n\nextension ImageEncoding {\n    public func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data? {\n        if container.type == .gif {\n            return container.data\n        }\n        return self.encode(container.image)\n    }\n}\n\n/// Image encoding context used when selecting which encoder to use.\npublic struct ImageEncodingContext: Sendable {\n    public let request: ImageRequest\n    public let image: PlatformImage\n    public let urlResponse: URLResponse?\n}\n"
  },
  {
    "path": "Sources/Nuke/ImageContainer.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\n#if !os(watchOS)\nimport AVKit\n#endif\n\nimport Foundation\n\n#if !os(macOS)\nimport UIKit.UIImage\n/// Alias for `UIImage`.\npublic typealias PlatformImage = UIImage\n/// Alias for `UIColor`.\npublic typealias PlatformColor = UIColor\n#else\nimport AppKit.NSImage\n/// Alias for `NSImage`.\npublic typealias PlatformImage = NSImage\n/// Alias for `NSColor`.\npublic typealias PlatformColor = NSColor\n#endif\n\n/// An image container with an image and associated metadata.\npublic struct ImageContainer: @unchecked Sendable {\n    /// The fetched image.\n#if os(macOS)\n    public var image: NSImage {\n        get { ref.image }\n        set { mutate { $0.image = newValue } }\n    }\n#else\n    public var image: UIImage {\n        get { ref.image }\n        set { mutate { $0.image = newValue } }\n    }\n#endif\n\n    /// The detected format of the image data, such as JPEG, PNG, or GIF.\n    /// `nil` if the format is unknown or not relevant.\n    public var type: AssetType? {\n        get { ref.type }\n        set { mutate { $0.type = newValue } }\n    }\n\n    /// Returns `true` if the image is a progressive preview rather than the\n    /// final decoded image.\n    public var isPreview: Bool {\n        get { ref.isPreview }\n        set { mutate { $0.isPreview = newValue } }\n    }\n\n    /// Contains the original image `data`, but only if the decoder decides to\n    /// attach it to the image.\n    ///\n    /// The default decoder (``ImageDecoders/Default``) attaches data to GIFs to\n    /// allow to display them using a rendering engine of your choice.\n    ///\n    /// - note: The `data`, along with the image container itself gets stored\n    /// in the memory cache.\n    public var data: Data? {\n        get { ref.data }\n        set { mutate { $0.data = newValue } }\n    }\n\n    /// Metadata provided by the user.\n    public var userInfo: [UserInfoKey: any Sendable] {\n        get { ref.userInfo }\n        set { mutate { $0.userInfo = newValue } }\n    }\n\n    private var ref: Container\n\n    /// Initializes the container with the given image.\n    public init(image: PlatformImage, type: AssetType? = nil, isPreview: Bool = false, data: Data? = nil, userInfo: [UserInfoKey: any Sendable] = [:]) {\n        self.ref = Container(image: image, type: type, isPreview: isPreview, data: data, userInfo: userInfo)\n    }\n\n    consuming func map(_ closure: (PlatformImage) throws -> PlatformImage) rethrows -> ImageContainer {\n        var copy = self\n        copy.image = try closure(copy.image)\n        return copy\n    }\n\n    /// A key used in ``userInfo``.\n    public struct UserInfoKey: Hashable, ExpressibleByStringLiteral, Sendable {\n        public let rawValue: String\n\n        public init(_ rawValue: String) {\n            self.rawValue = rawValue\n        }\n\n        public init(stringLiteral value: String) {\n            self.rawValue = value\n        }\n\n        // For internal purposes.\n        static let isThumbnailKey: UserInfoKey = \"com.github/kean/nuke/skip-decompression\"\n\n        /// A user info key to get the scan number (Int).\n        public static let scanNumberKey: UserInfoKey = \"com.github/kean/nuke/scan-number\"\n    }\n\n    // MARK: - Copy-on-Write\n\n    private mutating func mutate(_ closure: (Container) -> Void) {\n        if !isKnownUniquelyReferenced(&ref) {\n            ref = Container(ref)\n        }\n        closure(ref)\n    }\n\n    private final class Container: @unchecked Sendable {\n        var image: PlatformImage\n        var type: AssetType?\n        var isPreview: Bool\n        var data: Data?\n        var userInfo: [UserInfoKey: any Sendable]\n\n        init(image: PlatformImage, type: AssetType?, isPreview: Bool, data: Data? = nil, userInfo: [UserInfoKey: any Sendable]) {\n            self.image = image\n            self.type = type\n            self.isPreview = isPreview\n            self.data = data\n            self.userInfo = userInfo\n        }\n\n        init(_ ref: Container) {\n            self.image = ref.image\n            self.type = ref.type\n            self.isPreview = ref.isPreview\n            self.data = ref.data\n            self.userInfo = ref.userInfo\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/ImageRequest.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport CoreGraphics\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\n/// Represents an image request that specifies what images to download, how to\n/// process them, set the request priority, and more.\n///\n/// Creating a request:\n///\n/// ```swift\n/// let request = ImageRequest(\n///     url: URL(string: \"http://example.com/image.jpeg\"),\n///     processors: [.resize(width: 320)],\n///     priority: .high,\n///     options: [.reloadIgnoringCachedData]\n/// )\n/// let image = try await pipeline.image(for: request)\n/// ```\npublic struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStringLiteral {\n    // MARK: Options\n\n    /// The relative priority of the request. The priority affects the order in\n    /// which the requests are performed. ``Priority-swift.enum/normal`` by default.\n    ///\n    /// - note: You can change the priority of a running task using ``ImageTask/priority``.\n    public var priority: Priority {\n        get { ref.priority }\n        set { mutate { $0.priority = newValue } }\n    }\n\n    /// Processors to be applied to the image. Empty by default.\n    ///\n    /// See <doc:image-processing> to learn more.\n    public var processors: [any ImageProcessing] {\n        get { ref.processors }\n        set { mutate { $0.processors = newValue } }\n    }\n\n    /// The request options. For a complete list of options, see ``ImageRequest/Options-swift.struct``.\n    public var options: Options {\n        get { ref.options }\n        set { mutate { $0.options = newValue } }\n    }\n\n    /// Custom info passed alongside the request.\n    public var userInfo: [UserInfoKey: any Sendable] {\n        get { ref.userInfo ?? [:] }\n        set { mutate { $0.userInfo = newValue } }\n    }\n\n    // MARK: Instance Properties\n\n    /// Returns the request `URLRequest`.\n    ///\n    /// Returns `nil` for async data requests.\n    public var urlRequest: URLRequest? {\n        switch ref.resource {\n        case .url(let url): return url.map { URLRequest(url: $0) } // create lazily\n        case .urlRequest(let urlRequest): return urlRequest\n        case .data: return nil\n        case .image: return nil\n        }\n    }\n\n    /// Returns the request `URL`.\n    ///\n    /// Returns `nil` for async data requests.\n    public var url: URL? {\n        switch ref.resource {\n        case .url(let url): return url\n        case .urlRequest(let request): return request.url\n        case .data: return nil\n        case .image: return nil\n        }\n    }\n\n    /// The image identifier used for caching and task coalescing.\n    ///\n    /// By default, returns the absolute URL string for URL-based requests, or\n    /// the custom ID for async data requests.\n    ///\n    /// Set this to override the default identifier, for example, to strip\n    /// transient query parameters from the cache key:\n    ///\n    /// ```swift\n    /// var request = ImageRequest(url: URL(string: \"http://example.com/image.jpeg?token=123\"))\n    /// request.imageID = \"http://example.com/image.jpeg\"\n    /// ```\n    ///\n    /// - note: If the URL contains a short-lived auth token, consider using\n    /// ``ImagePipeline/Delegate-swift.protocol/willLoadData(for:urlRequest:pipeline:)``\n    /// instead. Store the base URL in the request and inject the token into\n    /// the `URLRequest` dynamically — this keeps the cache key stable without\n    /// having to set `imageID` on every request.\n    public var imageID: String? {\n        get { ref.customImageID ?? ref.originalImageID }\n        set { mutate { $0.customImageID = newValue } }\n    }\n\n    /// The display scale of the image. By default, `1`.\n    public var scale: Float {\n        get { ref.scale }\n        set { mutate { $0.scale = newValue } }\n    }\n\n    /// Thumbnail options. When set, the pipeline generates a thumbnail instead\n    /// of a full image. Thumbnail creation is generally significantly more\n    /// efficient, especially in terms of memory usage, than image resizing\n    /// (``ImageProcessors/Resize``).\n    ///\n    /// - note: Requires the default image decoder.\n    public var thumbnail: ThumbnailOptions? {\n        get { ref.thumbnail }\n        set { mutate { $0.thumbnail = newValue } }\n    }\n\n    /// Returns a debug request description.\n    public var description: String {\n        \"ImageRequest(resource: \\(ref.resource), priority: \\(priority), processors: \\(processors), options: \\(options), userInfo: \\(userInfo))\"\n    }\n\n    // MARK: Initializers\n\n    /// Initializes the request with the given string.\n    public init(stringLiteral value: String) {\n        self.init(url: URL(string: value))\n    }\n\n    /// Initializes a request with the given `URL`.\n    ///\n    /// - parameters:\n    ///   - url: The request URL.\n    ///   - processors: Processors to be applied to the image. See <doc:image-processing> to learn more.\n    ///   - priority: The priority of the request.\n    ///   - options: Image loading options.\n    ///   - userInfo: Soft-deprecated in Nuke 13.0, but still available as a dedicated property.\n    ///\n    /// ```swift\n    /// let request = ImageRequest(\n    ///     url: URL(string: \"http://...\"),\n    ///     processors: [.resize(size: imageView.bounds.size)],\n    ///     priority: .high\n    /// )\n    /// ```\n    public init(\n        url: URL?,\n        processors: [any ImageProcessing] = [],\n        priority: Priority = .normal,\n        options: Options = [],\n        userInfo: [UserInfoKey: any Sendable]? = nil\n    ) {\n        self.ref = Container(\n            resource: Resource.url(url),\n            originalImageID: url?.absoluteString,\n            processors: processors,\n            priority: priority,\n            options: options,\n            userInfo: userInfo\n        )\n    }\n\n    /// Initializes a request with the given `URLRequest`.\n    ///\n    /// - parameters:\n    ///   - urlRequest: The URLRequest describing the image request.\n    ///   - processors: Processors to be applied to the image. See <doc:image-processing> to learn more.\n    ///   - priority: The priority of the request.\n    ///   - options: Image loading options.\n    ///   - userInfo: Soft-deprecated in Nuke 13.0, but still available as a dedicated property.\n    ///\n    /// ```swift\n    /// let request = ImageRequest(\n    ///     url: URLRequest(url: URL(string: \"http://...\")),\n    ///     processors: [.resize(size: imageView.bounds.size)],\n    ///     priority: .high\n    /// )\n    /// ```\n    public init(\n        urlRequest: URLRequest,\n        processors: [any ImageProcessing] = [],\n        priority: Priority = .normal,\n        options: Options = [],\n        userInfo: [UserInfoKey: any Sendable]? = nil\n    ) {\n        self.ref = Container(\n            resource: Resource.urlRequest(urlRequest),\n            originalImageID: urlRequest.url?.absoluteString,\n            processors: processors,\n            priority: priority,\n            options: options,\n            userInfo: userInfo\n        )\n    }\n\n    /// Initializes a request with the given async function.\n    ///\n    /// For example, you can use it with the Photos framework after wrapping its\n    /// API in an async function.\n    ///\n    /// ```swift\n    /// ImageRequest(\n    ///     id: asset.localIdentifier,\n    ///     data: { try await PHAssetManager.default.imageData(for: asset) }\n    /// )\n    /// ```\n    ///\n    /// - important: If the pipeline uses a ``DataCaching`` disk cache, the\n    /// fetched data will be stored in it. Use ``Options-swift.struct/disableDiskCache``\n    /// to prevent this.\n    ///\n    /// - note: If the resource is identifiable with a `URL`, consider\n    /// implementing a custom data loader instead. See <doc:loading-data>.\n    ///\n    /// - parameters:\n    ///   - id: Uniquely identifies the fetched image.\n    ///   - data: An async function to be used to fetch image data.\n    ///   - processors: Processors to be applied to the image. See <doc:image-processing> to learn more.\n    ///   - priority: The priority of the request.\n    ///   - options: Image loading options.\n    ///   - userInfo: Soft-deprecated in Nuke 13.0, but still available as a dedicated property.\n    public init(\n        id: String,\n        data: @Sendable @escaping () async throws -> Data,\n        processors: [any ImageProcessing] = [],\n        priority: Priority = .normal,\n        options: Options = [],\n        userInfo: [UserInfoKey: any Sendable]? = nil\n    ) {\n        self.ref = Container(\n            resource: .data(fetch: data),\n            originalImageID: id,\n            processors: processors,\n            priority: priority,\n            options: options,\n            userInfo: userInfo\n        )\n    }\n\n    /// Initializes a request with the given async function that returns an image\n    /// container directly.\n    ///\n    /// Use this initializer to process images already in memory or integrate\n    /// with systems that provide pre-decoded images, such as the Photos framework.\n    ///\n    /// - note: Unlike ``init(id:data:)``, the image is never stored in the disk\n    /// cache because no raw data is available.\n    ///\n    /// - parameters:\n    ///   - id: Uniquely identifies the fetched image.\n    ///   - image: An async function returning an ``ImageContainer``.\n    ///   - processors: Processors to be applied to the image. See <doc:image-processing> to learn more.\n    ///   - priority: The priority of the request.\n    ///   - options: Image loading options.\n    ///   - userInfo: Soft-deprecated in Nuke 13.0, but still available as a dedicated property.\n    public init(\n        id: String,\n        image: @Sendable @escaping () async throws -> ImageContainer,\n        processors: [any ImageProcessing] = [],\n        priority: Priority = .normal,\n        options: Options = [],\n        userInfo: [UserInfoKey: any Sendable]? = nil\n    ) {\n        self.ref = Container(\n            resource: .image(fetch: image),\n            originalImageID: id,\n            processors: processors,\n            priority: priority,\n            options: options,\n            userInfo: userInfo\n        )\n    }\n\n    // MARK: Nested Types\n\n    /// The priority affecting the order in which the requests are performed.\n    @frozen public enum Priority: Int, Comparable, Sendable {\n        case veryLow = 0, low, normal, high, veryHigh\n\n        public static func < (lhs: Priority, rhs: Priority) -> Bool {\n            lhs.rawValue < rhs.rawValue\n        }\n    }\n\n    /// Image request options.\n    ///\n    /// By default, the pipeline makes full use of all of its caching layers. You can change this behavior using options. For example, you can ignore local caches using ``ImageRequest/Options-swift.struct/reloadIgnoringCachedData`` option.\n    ///\n    /// ```swift\n    /// request.options = [.reloadIgnoringCachedData]\n    /// ```\n    ///\n    /// Another useful cache policy is ``ImageRequest/Options-swift.struct/returnCacheDataDontLoad``\n    /// that terminates the request if no cached data is available.\n    public struct Options: OptionSet, Hashable, Sendable {\n        /// Returns a raw value.\n        public let rawValue: UInt16\n\n        /// Initializes options with a given raw value.\n        public init(rawValue: UInt16) {\n            self.rawValue = rawValue\n        }\n\n        /// Disables memory cache reads (see ``ImageCaching``).\n        public static let disableMemoryCacheReads = Options(rawValue: 1 << 0)\n\n        /// Disables memory cache writes (see ``ImageCaching``).\n        public static let disableMemoryCacheWrites = Options(rawValue: 1 << 1)\n\n        /// Disables both memory cache reads and writes (see ``ImageCaching``).\n        public static let disableMemoryCache: Options = [.disableMemoryCacheReads, .disableMemoryCacheWrites]\n\n        /// Disables disk cache reads (see ``DataCaching``).\n        public static let disableDiskCacheReads = Options(rawValue: 1 << 2)\n\n        /// Disables disk cache writes (see ``DataCaching``).\n        public static let disableDiskCacheWrites = Options(rawValue: 1 << 3)\n\n        /// Disables both disk cache reads and writes (see ``DataCaching``).\n        public static let disableDiskCache: Options = [.disableDiskCacheReads, .disableDiskCacheWrites]\n\n        /// The image should be loaded only from the originating source.\n        ///\n        /// This option only works with ``ImageCaching`` and ``DataCaching``, but not\n        /// `URLCache`. If you want to ignore `URLCache`, initialize the request\n        /// with `URLRequest` with the respective policy.\n        public static let reloadIgnoringCachedData: Options = [.disableMemoryCacheReads, .disableDiskCacheReads]\n\n        /// Use existing cache data and fail if no cached data is available.\n        public static let returnCacheDataDontLoad = Options(rawValue: 1 << 4)\n\n        /// Skip decompression (\"bitmapping\") for the given image. Decompression\n        /// will happen lazily when you display the image.\n        public static let skipDecompression = Options(rawValue: 1 << 5)\n\n        /// Perform data loading immediately, ignoring ``ImagePipeline/Configuration-swift.struct/dataLoadingQueue``. It\n        /// can be used to elevate priority of certain tasks.\n        ///\n        /// - important: If there is an outstanding task for loading the same\n        /// resource but without this option, a new task will be created.\n        public static let skipDataLoadingQueue = Options(rawValue: 1 << 6)\n    }\n\n    /// A key used in `userInfo` for providing custom request options.\n    public struct UserInfoKey: Hashable, ExpressibleByStringLiteral, Sendable {\n        /// Returns a key raw value.\n        public let rawValue: String\n\n        /// Initializes the key with a raw value.\n        public init(_ rawValue: String) {\n            self.rawValue = rawValue\n        }\n\n        /// Initializes the key with a raw value.\n        public init(stringLiteral value: String) {\n            self.rawValue = value\n        }\n    }\n\n    /// Thumbnail options.\n    ///\n    /// For more info, see https://developer.apple.com/documentation/imageio/cgimagesource/image_source_option_dictionary_keys\n    public struct ThumbnailOptions: Hashable, Sendable {\n        var size: ImageTargetSize\n        var options: Options = .defaults\n\n        /// Whether a thumbnail should be automatically created for an image if\n        /// a thumbnail isn't present in the image source file. The thumbnail is\n        /// created from the full image, subject to the limit specified by size.\n        public var createThumbnailFromImageIfAbsent: Bool {\n            get { options.contains(.createThumbnailFromImageIfAbsent) }\n            set { options.set(.createThumbnailFromImageIfAbsent, newValue) }\n        }\n\n        /// Whether a thumbnail should be created from the full image even if a\n        /// thumbnail is present in the image source file. The thumbnail is created\n        /// from the full image, subject to the limit specified by size.\n        public var createThumbnailFromImageAlways: Bool {\n            get { options.contains(.createThumbnailFromImageAlways) }\n            set { options.set(.createThumbnailFromImageAlways, newValue) }\n        }\n\n        /// Whether the thumbnail should be rotated and scaled according to the\n        /// orientation and pixel aspect ratio of the full image.\n        public var createThumbnailWithTransform: Bool {\n            get { options.contains(.createThumbnailWithTransform) }\n            set { options.set(.createThumbnailWithTransform, newValue) }\n        }\n\n        /// Specifies whether image decoding and caching should happen at image\n        /// creation time.\n        public var shouldCacheImmediately: Bool {\n            get { options.contains(.shouldCacheImmediately) }\n            set { options.set(.shouldCacheImmediately, newValue) }\n        }\n\n        /// Initializes the options with the given pixel size. The thumbnail is\n        /// resized to fit the target size.\n        ///\n        /// This option performs slightly faster than ``ImageRequest/ThumbnailOptions/init(size:unit:contentMode:)``\n        /// because it doesn't need to read the image size.\n        public init(maxPixelSize: Float) {\n            self.size = ImageTargetSize(maxPixelSize: maxPixelSize)\n        }\n\n        /// Initializes the options with the given size.\n        ///\n        /// - parameters:\n        ///   - size: The target size.\n        ///   - unit: Unit of the target size.\n        ///   - contentMode: A target content mode.\n        public init(size: CGSize, unit: ImageProcessingOptions.Unit = .points, contentMode: ImageProcessingOptions.ContentMode = .aspectFill) {\n            self.size = ImageTargetSize(size: size, unit: unit)\n            options.insert(.flexible)\n            if contentMode == .aspectFit { options.insert(.aspectFit) }\n        }\n\n        /// The content mode for flexible-size thumbnails.\n        var contentMode: ImageProcessingOptions.ContentMode {\n            options.contains(.aspectFit) ? .aspectFit : .aspectFill\n        }\n\n        /// Generates a thumbnail from the given image data.\n        public func makeThumbnail(with data: Data) -> PlatformImage? {\n            Nuke.makeThumbnail(data: data, options: self)\n        }\n\n        var identifier: String {\n            let sizeStr = options.contains(.flexible)\n                ? \"width=\\(size.cgSize.width),height=\\(size.cgSize.height),contentMode=\\(contentMode)\"\n                : \"maxPixelSize=\\(size.width)\"\n            return \"com.github/kean/nuke/thumbnail?\\(sizeStr),options=\\(createThumbnailFromImageIfAbsent)\\(createThumbnailFromImageAlways)\\(createThumbnailWithTransform)\\(shouldCacheImmediately)\"\n        }\n\n        struct Options: OptionSet, Hashable, Sendable {\n            let rawValue: UInt8\n\n            init(rawValue: UInt8) { self.rawValue = rawValue }\n\n            static let createThumbnailFromImageIfAbsent = Options(rawValue: 1 << 0)\n            static let createThumbnailFromImageAlways = Options(rawValue: 1 << 1)\n            static let createThumbnailWithTransform = Options(rawValue: 1 << 2)\n            static let shouldCacheImmediately = Options(rawValue: 1 << 3)\n            static let aspectFit = Options(rawValue: 1 << 4)\n            static let flexible = Options(rawValue: 1 << 5)\n\n            static let defaults: Options = [\n                .createThumbnailFromImageIfAbsent,\n                .createThumbnailFromImageAlways,\n                .createThumbnailWithTransform,\n                .shouldCacheImmediately\n            ]\n\n            mutating func set(_ option: Options, _ value: Bool) {\n                if value { insert(option) } else { remove(option) }\n            }\n        }\n    }\n\n    // MARK: Internal\n\n    private var ref: Container\n\n    private mutating func mutate(_ closure: (Container) -> Void) {\n        if !isKnownUniquelyReferenced(&ref) {\n            ref = Container(ref)\n        }\n        closure(ref)\n    }\n\n    var resource: Resource { ref.resource }\n\n    consuming func withProcessors(_ processors: [any ImageProcessing]) -> ImageRequest {\n        var request = self\n        request.processors = processors\n        return request\n    }\n\n    consuming func withoutThumbnail() -> ImageRequest {\n        var copy = self\n        copy.thumbnail = nil\n        return copy\n    }\n\n    /// The underlying resource image ID (URL string or data ID), never the\n    /// user-supplied ``imageID`` override. Used for data-loading task keys\n    /// where the actual URL determines what gets fetched.\n    var originalImageID: String? { ref.originalImageID }\n\n    static var _containerInstanceSize: Int { class_getInstanceSize(Container.self) }\n}\n\n// MARK: - ImageRequest (Private)\n\nextension ImageRequest {\n    /// Just like many Swift built-in types, ``ImageRequest`` uses CoW approach to\n    /// avoid memberwise retain/releases when ``ImageRequest`` is passed around.\n    private final class Container: @unchecked Sendable {\n        // It's beneficial to put these fields in that order to align them\n        // as they perfeclty align at the boundary due to their size\n        let resource: Resource\n        var priority: Priority\n        var options: Options\n        var scale: Float = 1.0\n\n        // It is stored partially for performance reasons (`absoluteString` can be expensive to compute)\n        var originalImageID: String?\n        var customImageID: String?\n        var processors: [any ImageProcessing]\n        var userInfo: [UserInfoKey: any Sendable]?\n        var thumbnail: ThumbnailOptions?\n\n        init(resource: Resource, originalImageID: String?, processors: [any ImageProcessing], priority: Priority, options: Options, userInfo: [UserInfoKey: any Sendable]?) {\n            self.resource = resource\n            self.processors = processors\n            self.priority = priority\n            self.options = options\n            self.originalImageID = originalImageID\n            self.userInfo = userInfo\n        }\n\n        /// Creates a copy.\n        init(_ ref: Container) {\n            self.resource = ref.resource\n            self.processors = ref.processors\n            self.priority = ref.priority\n            self.options = ref.options\n            self.originalImageID = ref.originalImageID\n            self.userInfo = ref.userInfo\n            self.customImageID = ref.customImageID\n            self.scale = ref.scale\n            self.thumbnail = ref.thumbnail\n        }\n    }\n\n    enum Resource: CustomStringConvertible {\n        case url(URL?)\n        case urlRequest(URLRequest)\n        case data(fetch: @Sendable () async throws -> Data)\n        case image(fetch: @Sendable () async throws -> ImageContainer)\n\n        var description: String {\n            switch self {\n            case .url(let url): return \"\\(url?.absoluteString ?? \"nil\")\"\n            case .urlRequest(let urlRequest): return \"\\(urlRequest)\"\n            case .data: return \"<closure>\"\n            case .image: return \"<closure>\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/ImageResponse.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\n/// An image response that contains a fetched image and some metadata.\npublic struct ImageResponse: Sendable {\n    /// An image container with an image and associated metadata.\n    public var container: ImageContainer\n\n    /// The image from the response container.\n#if os(macOS)\n    public var image: NSImage { container.image }\n#else\n    public var image: UIImage { container.image }\n#endif\n\n    /// Returns `true` if the image is a progressive preview rather than the\n    /// final decoded image.\n    public var isPreview: Bool { container.isPreview }\n\n    /// The request for which the response was created.\n    public var request: ImageRequest\n\n    /// A response. `nil` unless the resource was fetched from the network or an\n    /// HTTP cache.\n    public var urlResponse: URLResponse?\n\n    /// Contains a cache type in case the image was returned from one of the\n    /// pipeline caches (not including any of the HTTP caches if enabled).\n    public var cacheType: CacheType?\n\n    /// Initializes the response with the given image.\n    public init(container: ImageContainer, request: ImageRequest, urlResponse: URLResponse? = nil, cacheType: CacheType? = nil) {\n        self.container = container\n        self.request = request\n        self.urlResponse = urlResponse\n        self.cacheType = cacheType\n    }\n\n    /// A cache type.\n    @frozen public enum CacheType: Sendable {\n        /// Memory cache (see ``ImageCaching``).\n        case memory\n        /// Disk cache (see ``DataCaching``).\n        case disk\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/ImageTask.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\n/// A task performed by the ``ImagePipeline``.\n///\n/// The pipeline maintains a strong reference to the task until the request\n/// finishes or fails; you do not need to maintain a reference to the task unless\n/// it is useful for your app.\npublic final class ImageTask: Hashable, CustomStringConvertible, @unchecked Sendable {\n    /// An identifier that uniquely identifies the task within a given pipeline.\n    public let taskId: UInt64\n\n    /// The original request that the task was created with.\n    public let request: ImageRequest\n\n    /// The priority of the task. The priority can be updated dynamically even\n    /// for a task that is already running.\n    public var priority: ImageRequest.Priority {\n        get { withNonisolatedStateLock { $0.priority } }\n        set { setPriority(newValue) }\n    }\n\n    /// Returns the current download progress. Returns zeros until the download\n    /// starts and the total resource size is known.\n    public var currentProgress: Progress {\n        withNonisolatedStateLock { $0.progress }\n    }\n\n    /// The download progress.\n    public struct Progress: Hashable, Sendable {\n        /// The number of bytes that the task has received.\n        public let completed: Int64\n        /// A best-guess upper bound on the number of bytes of the resource.\n        public let total: Int64\n\n        /// Returns the fraction of the completion.\n        public var fraction: Float {\n            guard total > 0 else { return 0 }\n            return min(1, Float(completed) / Float(total))\n        }\n\n        /// Initializes progress with the given byte counts.\n        public init(completed: Int64, total: Int64) {\n            (self.completed, self.total) = (completed, total)\n        }\n    }\n\n    /// The current state of the task.\n    public var state: State {\n        withNonisolatedStateLock { $0.state }\n    }\n\n    /// The state of the image task.\n    @frozen public enum State {\n        /// The task is currently running.\n        case running\n        /// The task has received a cancel message.\n        case cancelled\n        /// The task has completed (without being canceled).\n        case completed\n    }\n\n    // MARK: - Async/Await\n\n    /// Returns the response image.\n    ///\n    /// Throws ``ImagePipeline/Error/cancelled`` if the task is cancelled.\n    public var image: PlatformImage {\n        get async throws(ImagePipeline.Error) {\n            try await response.image\n        }\n    }\n\n    /// Returns the image response.\n    ///\n    /// Throws ``ImagePipeline/Error/cancelled`` if the task is cancelled.\n    public var response: ImageResponse {\n        get async throws(ImagePipeline.Error) {\n            do {\n                return try await withTaskCancellationHandler {\n                    try await _task.value\n                } onCancel: {\n                    cancel()\n                }\n            } catch let error as ImagePipeline.Error {\n                throw error\n            } catch {\n                preconditionFailure(\"Unexpected error type: \\(error)\")\n            }\n        }\n    }\n\n    /// The stream of progress updates.\n    public var progress: AsyncCompactMapSequence<AsyncStream<Event>, Progress> {\n        events.compactMap {\n            if case .progress(let value) = $0 { return value }\n            return nil\n        }\n    }\n\n    /// The stream of image previews generated for images that support\n    /// progressive decoding.\n    ///\n    /// - seealso: ``ImagePipeline/Configuration-swift.struct/isProgressiveDecodingEnabled``\n    public var previews: AsyncCompactMapSequence<AsyncStream<Event>, ImageResponse> {\n        events.compactMap {\n            if case .preview(let value) = $0 { return value }\n            return nil\n        }\n    }\n\n    // MARK: - Events\n\n    /// The events sent by the pipeline during the task execution.\n    public var events: AsyncStream<Event> { makeStream() }\n\n    /// An event produced during the runtime of the task.\n    @frozen public enum Event: Sendable {\n        /// The task was started by the pipeline.\n        case started\n        /// The download progress was updated.\n        case progress(Progress)\n        /// The pipeline generated a progressive scan of the image.\n        case preview(ImageResponse)\n        /// The task finished with the given response.\n        ///\n        /// When the task is cancelled, this is called with\n        /// `.failure(``ImagePipeline/Error/cancelled``)`.\n        case finished(Result<ImageResponse, ImagePipeline.Error>)\n    }\n\n    private var nonisolatedState: NonisolatedState\n    private let isDataTask: Bool\n    private let onEvent: ((Event, ImageTask) -> Void)?\n    private let lock: os_unfair_lock_t\n    private weak var pipeline: ImagePipeline?\n\n    // Set once during creation, then read-only from `response` getter.\n    nonisolated(unsafe) var _task: Task<ImageResponse, any Error>!\n    @ImagePipelineActor var _continuation: UnsafeContinuation<ImageResponse, any Error>?\n    @ImagePipelineActor var _state: State = .running\n    @ImagePipelineActor var _streamContinuations = ContiguousArray<AsyncStream<Event>.Continuation>()\n\n    deinit {\n        lock.deinitialize(count: 1)\n        lock.deallocate()\n    }\n\n    init(taskId: UInt64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) {\n        self.taskId = taskId\n        self.request = request\n        self.nonisolatedState = NonisolatedState(priority: request.priority)\n        self.isDataTask = isDataTask\n        self.pipeline = pipeline\n        self.onEvent = onEvent\n\n        lock = .allocate(capacity: 1)\n        lock.initialize(to: os_unfair_lock())\n    }\n\n    /// Marks task as being cancelled.\n    ///\n    /// The pipeline will immediately cancel any work associated with a task\n    /// unless there is an equivalent outstanding task running.\n    public func cancel() {\n        let didChange: Bool = withNonisolatedStateLock {\n            guard $0.state == .running else { return false }\n            $0.state = .cancelled\n            return true\n        }\n        guard didChange else { return } // Make sure it gets called once (expensive)\n        Task { @ImagePipelineActor in\n            self.pipeline?.imageTaskCancelCalled(self)\n        }\n    }\n\n    private func setPriority(_ newValue: ImageRequest.Priority) {\n        let didChange: Bool = withNonisolatedStateLock {\n            guard $0.priority != newValue else { return false }\n            $0.priority = newValue\n            return $0.state == .running\n        }\n        guard didChange else { return }\n        Task { @ImagePipelineActor in\n            self.pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue)\n        }\n    }\n\n    // MARK: Internals\n\n    /// Cancels the task directly from an actor-isolated context, bypassing\n    /// the lock and the actor hop used by the public `cancel()` method.\n    @ImagePipelineActor func _cancelTask() {\n        pipeline?.imageTaskCancelCalled(self)\n    }\n\n    /// Gets called when the task is cancelled either by the user or by an\n    /// external event such as session invalidation.\n    @ImagePipelineActor func _cancel() {\n        guard _setState(.cancelled) else { return }\n        _dispatch(.finished(.failure(.cancelled)))\n    }\n\n    /// Gets called when the associated task sends a new event.\n    @ImagePipelineActor func _process(_ event: AsyncTask<ImageResponse, ImagePipeline.Error>.Event) {\n        switch event {\n        case let .value(response, isCompleted):\n            if isCompleted {\n                _finish(.success(response))\n            } else {\n                _dispatch(.preview(response))\n            }\n        case let .progress(value):\n            withNonisolatedStateLock { $0.progress = value }\n            _dispatch(.progress(value))\n        case let .error(error):\n            _finish(.failure(error))\n        }\n    }\n\n    @ImagePipelineActor private func _finish(_ result: Result<ImageResponse, ImagePipeline.Error>) {\n        guard _setState(.completed) else { return }\n        _dispatch(.finished(result))\n    }\n\n    @ImagePipelineActor func _setState(_ state: State) -> Bool {\n        guard _state == .running else { return false }\n        _state = state\n        withNonisolatedStateLock { $0.state = state }\n        return true\n    }\n\n    /// Dispatches the given event to the observers.\n    ///\n    /// - warning: The task needs to be fully wired (`_continuation` present)\n    /// before it can start sending the events.\n    @ImagePipelineActor func _dispatch(_ event: Event) {\n        guard _continuation != nil else {\n            return // Task isn't fully wired yet\n        }\n\n        for continuation in _streamContinuations {\n            continuation.yield(event)\n        }\n        switch event {\n        case .finished(let result):\n            for continuation in _streamContinuations {\n                continuation.finish()\n            }\n            _streamContinuations.removeAll()\n            _continuation?.resume(with: result)\n        default:\n            break\n        }\n\n        onEvent?(event, self)\n        pipeline?.imageTask(self, didProcessEvent: event, isDataTask: isDataTask)\n    }\n\n    // MARK: Hashable\n\n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(ObjectIdentifier(self))\n    }\n\n    public static func == (lhs: ImageTask, rhs: ImageTask) -> Bool {\n        ObjectIdentifier(lhs) == ObjectIdentifier(rhs)\n    }\n\n    // MARK: CustomStringConvertible\n\n    public var description: String {\n        \"ImageTask(id: \\(taskId), priority: \\(priority), progress: \\(currentProgress.completed) / \\(currentProgress.total), state: \\(state))\"\n    }\n}\n\n\n// MARK: - ImageTask (Private)\n\nextension ImageTask {\n    /// Creates a new stream of events for this task.\n    ///\n    /// - note: Each call creates an independent stream. Subscribing after the\n    /// task has already finished or been cancelled produces an empty stream —\n    /// no `.finished` terminal event is replayed. Subscribe before the task\n    /// completes if you need to observe this event.\n    private func makeStream() -> AsyncStream<Event> {\n        AsyncStream { continuation in\n            Task { @ImagePipelineActor in\n                guard self._state == .running else {\n                    return continuation.finish()\n                }\n                self._streamContinuations.append(continuation)\n            }\n        }\n    }\n\n    private func withNonisolatedStateLock<T>(_ closure: (inout NonisolatedState) -> T) -> T {\n        os_unfair_lock_lock(lock)\n        defer { os_unfair_lock_unlock(lock) }\n        return closure(&nonisolatedState)\n    }\n\n    /// Contains the state synchronized using the internal lock.\n    ///\n    /// - warning: Must be accessed using `withNonisolatedState`.\n    private struct NonisolatedState {\n        var state: ImageTask.State = .running\n        var priority: ImageRequest.Priority\n        var progress = Progress(completed: 0, total: 0)\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/Extensions.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport CryptoKit\n\nextension String {\n    /// Calculates SHA1 from the given string and returns its hex representation.\n    ///\n    /// ```swift\n    /// print(\"http://test.com\".sha1)\n    /// // prints \"50334ee0b51600df6397ce93ceed4728c37fee4e\"\n    /// ```\n    var sha1: String? {\n        guard let input = self.data(using: .utf8) else {\n            return nil // The conversion to .utf8 should never fail\n        }\n        let digest = Insecure.SHA1.hash(data: input)\n        var output = \"\"\n        for byte in digest {\n            output.append(String(format: \"%02x\", byte))\n        }\n        return output\n    }\n}\n\nextension URL {\n    var isLocalResource: Bool {\n        scheme == \"file\" || scheme == \"data\"\n    }\n}\n\nextension ImageRequest.Priority {\n    var taskPriority: TaskPriority {\n        switch self {\n        case .veryLow: return .veryLow\n        case .low: return .low\n        case .normal: return .normal\n        case .high: return .high\n        case .veryHigh: return .veryHigh\n        }\n    }\n}\n\n@concurrent func performInBackground<T>(_ closure: @Sendable () -> T) async -> T {\n    closure()\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/Graphics.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if os(watchOS)\nimport ImageIO\nimport CoreGraphics\nimport WatchKit.WKInterfaceDevice\n#endif\n\n#if canImport(UIKit)\n import UIKit\n #endif\n\n #if canImport(AppKit)\n import AppKit\n #endif\n\nextension PlatformImage {\n    var processed: ImageProcessingExtensions {\n        ImageProcessingExtensions(image: self)\n    }\n}\n\nstruct ImageProcessingExtensions {\n    let image: PlatformImage\n\n    func byResizing(to targetSize: CGSize,\n                    contentMode: ImageProcessingOptions.ContentMode,\n                    upscale: Bool) -> PlatformImage? {\n        guard let cgImage = image.cgImage else {\n            return nil\n        }\n#if canImport(UIKit)\n        let targetSize = targetSize.rotatedForOrientation(CGImagePropertyOrientation(image.imageOrientation))\n#endif\n        let scale = cgImage.size.getScale(targetSize: targetSize, contentMode: contentMode)\n        guard scale < 1 || upscale else {\n            return image // The image doesn't require scaling\n        }\n        let size = cgImage.size.scaled(by: scale).rounded()\n        return image.draw(inCanvasWithSize: size)\n    }\n\n    /// Crops the input image to the given size and resizes it if needed.\n    /// - note: this method will always upscale.\n    func byResizingAndCropping(to targetSize: CGSize) -> PlatformImage? {\n        guard let cgImage = image.cgImage else {\n            return nil\n        }\n#if canImport(UIKit)\n        let targetSize = targetSize.rotatedForOrientation(CGImagePropertyOrientation(image.imageOrientation))\n#endif\n        let scale = cgImage.size.getScale(targetSize: targetSize, contentMode: .aspectFill)\n        let scaledSize = cgImage.size.scaled(by: scale)\n        let drawRect = scaledSize.centeredInRectWithSize(targetSize)\n        return image.draw(inCanvasWithSize: targetSize, drawRect: drawRect)\n    }\n\n    func byDrawingInCircle(border: ImageProcessingOptions.Border?) -> PlatformImage? {\n        guard let squared = byCroppingToSquare(), let cgImage = squared.cgImage else {\n            return nil\n        }\n        let radius = CGFloat(cgImage.width) // Can use any dimension since image is a square\n        return squared.processed.byAddingRoundedCorners(radius: radius / 2.0, border: border)\n    }\n\n    /// Draws an image in square by preserving an aspect ratio and filling the\n    /// square if needed. If the image is already a square, returns an original image.\n    func byCroppingToSquare() -> PlatformImage? {\n        guard let cgImage = image.cgImage else {\n            return nil\n        }\n\n        guard cgImage.width != cgImage.height else {\n            return image // Already a square\n        }\n\n        let imageSize = cgImage.size\n        let side = min(cgImage.width, cgImage.height)\n        let targetSize = CGSize(width: side, height: side)\n        let cropRect = CGRect(origin: .zero, size: targetSize).offsetBy(\n            dx: max(0, (imageSize.width - targetSize.width) / 2).rounded(.down),\n            dy: max(0, (imageSize.height - targetSize.height) / 2).rounded(.down)\n        )\n        guard let cropped = cgImage.cropping(to: cropRect) else {\n            return nil\n        }\n        return PlatformImage.make(cgImage: cropped, source: image)\n    }\n\n    /// Adds rounded corners with the given radius to the image.\n    /// - parameter radius: Radius in pixels.\n    /// - parameter border: Optional stroke border.\n    func byAddingRoundedCorners(radius: CGFloat, border: ImageProcessingOptions.Border? = nil) -> PlatformImage? {\n        guard let cgImage = image.cgImage else {\n            return nil\n        }\n        guard let ctx = CGContext.make(cgImage, size: cgImage.size, alphaInfo: .premultipliedLast) else {\n            return nil\n        }\n        let rect = CGRect(origin: CGPoint.zero, size: cgImage.size)\n        let path = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)\n        ctx.addPath(path)\n        ctx.clip()\n        ctx.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: cgImage.size))\n\n        if let border {\n            ctx.setStrokeColor(border.color.cgColor)\n            ctx.addPath(path)\n            ctx.setLineWidth(border.width)\n            ctx.strokePath()\n        }\n        guard let outputCGImage = ctx.makeImage() else {\n            return nil\n        }\n        return PlatformImage.make(cgImage: outputCGImage, source: image)\n    }\n}\n\nextension PlatformImage {\n    /// Draws the image in a `CGContext` in a canvas with the given size using\n    /// the specified draw rect.\n    ///\n    /// For example, if the canvas size is `CGSize(width: 10, height: 10)` and\n    /// the draw rect is `CGRect(x: -5, y: 0, width: 20, height: 10)` it would\n    /// draw the input image (which is horizontal based on the known draw rect)\n    /// in a square by centering it in the canvas.\n    ///\n    /// - parameter drawRect: `nil` by default. If `nil` will use the canvas rect.\n    func draw(inCanvasWithSize canvasSize: CGSize, drawRect: CGRect? = nil) -> PlatformImage? {\n        guard let cgImage else {\n            return nil\n        }\n        guard let ctx = CGContext.make(cgImage, size: canvasSize) else {\n            return nil\n        }\n        ctx.draw(cgImage, in: drawRect ?? CGRect(origin: .zero, size: canvasSize))\n        guard let outputCGImage = ctx.makeImage() else {\n            return nil\n        }\n        return PlatformImage.make(cgImage: outputCGImage, source: self)\n    }\n\n    /// Decompresses the input image by drawing in the `CGContext`.\n    func decompressed(isUsingPrepareForDisplay: Bool) -> PlatformImage? {\n#if os(iOS) || os(tvOS) || os(visionOS)\n        if isUsingPrepareForDisplay {\n            return preparingForDisplay()\n        }\n#endif\n        guard let cgImage else {\n            return nil\n        }\n        return draw(inCanvasWithSize: cgImage.size, drawRect: CGRect(origin: .zero, size: cgImage.size))\n    }\n}\n\nextension CGContext {\n    static func make(_ image: CGImage, size: CGSize, alphaInfo: CGImageAlphaInfo? = nil) -> CGContext? {\n        if let ctx = CGContext.make(image, size: size, alphaInfo: alphaInfo, colorSpace: image.colorSpace ?? CGColorSpaceCreateDeviceRGB()) {\n            return ctx\n        }\n        // In case the combination of parameters (color space, bits per component, etc)\n        // is nit supported by Core Graphics, switch to default context.\n        // - Quartz 2D Programming Guide\n        // - https://github.com/kean/Nuke/issues/35\n        // - https://github.com/kean/Nuke/issues/57\n        return CGContext.make(image, size: size, alphaInfo: alphaInfo, colorSpace: CGColorSpaceCreateDeviceRGB())\n    }\n\n    static func make(_ image: CGImage, size: CGSize, alphaInfo: CGImageAlphaInfo?, colorSpace: CGColorSpace) -> CGContext? {\n        CGContext(\n            data: nil,\n            width: Int(size.width),\n            height: Int(size.height),\n            bitsPerComponent: 8,\n            bytesPerRow: 0,\n            space: colorSpace,\n            bitmapInfo: (alphaInfo ?? preferredAlphaInfo(for: image, colorSpace: colorSpace)).rawValue\n        )\n    }\n\n    /// - See https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB\n    private static func preferredAlphaInfo(for image: CGImage, colorSpace: CGColorSpace) -> CGImageAlphaInfo {\n        guard image.isOpaque else {\n            return .premultipliedLast\n        }\n        if colorSpace.numberOfComponents == 1 && image.bitsPerPixel == 8 {\n            return .none // The only pixel format supported for grayscale CS\n        }\n        return .noneSkipLast\n    }\n}\n\nextension CGFloat {\n    func converted(to unit: ImageProcessingOptions.Unit) -> CGFloat {\n        switch unit {\n        case .pixels: return self\n        case .points: return self * Screen.scale\n        }\n    }\n}\n\nextension CGSize {\n    func getScale(targetSize: CGSize, contentMode: ImageProcessingOptions.ContentMode) -> CGFloat {\n        let scaleHor = targetSize.width / width\n        let scaleVert = targetSize.height / height\n\n        switch contentMode {\n        case .aspectFill:\n            return max(scaleHor, scaleVert)\n        case .aspectFit:\n            return min(scaleHor, scaleVert)\n        }\n    }\n\n    /// Calculates a rect such that the output rect will be in the center of\n    /// the rect of the input size (assuming origin: .zero)\n    func centeredInRectWithSize(_ targetSize: CGSize) -> CGRect {\n        // First, resize the original size to fill the target size.\n        CGRect(origin: .zero, size: self).offsetBy(\n            dx: -(width - targetSize.width) / 2,\n            dy: -(height - targetSize.height) / 2\n        )\n    }\n}\n\n#if canImport(UIKit)\nextension CGImagePropertyOrientation {\n    init(_ orientation: UIImage.Orientation) {\n        switch orientation {\n        case .up: self = .up\n        case .upMirrored: self = .upMirrored\n        case .down: self = .down\n        case .downMirrored: self = .downMirrored\n        case .left: self = .left\n        case .leftMirrored: self = .leftMirrored\n        case .right: self = .right\n        case .rightMirrored: self = .rightMirrored\n        @unknown default: self = .up\n        }\n    }\n}\n\nextension UIImage.Orientation {\n    init(_ cgOrientation: CGImagePropertyOrientation) {\n        switch cgOrientation {\n        case .up: self = .up\n        case .upMirrored: self = .upMirrored\n        case .down: self = .down\n        case .downMirrored: self = .downMirrored\n        case .left: self = .left\n        case .leftMirrored: self = .leftMirrored\n        case .right: self = .right\n        case .rightMirrored: self = .rightMirrored\n        }\n    }\n}\n\nprivate extension CGSize {\n    func rotatedForOrientation(_ imageOrientation: CGImagePropertyOrientation) -> CGSize {\n        switch imageOrientation {\n        case .left, .leftMirrored, .right, .rightMirrored:\n            return CGSize(width: height, height: width) // Rotate 90 degrees\n        case .up, .upMirrored, .down, .downMirrored:\n            return self\n        }\n    }\n\n}\n#endif\n\n#if os(macOS)\nextension NSImage {\n    var cgImage: CGImage? {\n        cgImage(forProposedRect: nil, context: nil, hints: nil)\n    }\n\n    var ciImage: CIImage? {\n        cgImage.map { CIImage(cgImage: $0) }\n    }\n\n    static func make(cgImage: CGImage, source: NSImage) -> NSImage {\n        NSImage(cgImage: cgImage, size: .zero)\n    }\n\n    convenience init(cgImage: CGImage) {\n        self.init(cgImage: cgImage, size: .zero)\n    }\n}\n#else\nextension UIImage {\n    static func make(cgImage: CGImage, source: UIImage) -> UIImage {\n        UIImage(cgImage: cgImage, scale: source.scale, orientation: source.imageOrientation)\n    }\n}\n#endif\n\nextension CGImage {\n    /// Returns `true` if the image doesn't contain alpha channel.\n    var isOpaque: Bool {\n        let alpha = alphaInfo\n        return alpha == .none || alpha == .noneSkipFirst || alpha == .noneSkipLast\n    }\n\n    var size: CGSize {\n        CGSize(width: width, height: height)\n    }\n}\n\nextension CGSize {\n    func scaled(by scale: CGFloat) -> CGSize {\n        CGSize(width: width * scale, height: height * scale)\n    }\n\n    func rounded() -> CGSize {\n        CGSize(width: CGFloat(round(width)), height: CGFloat(round(height)))\n    }\n}\n\nenum Screen {\n#if os(iOS) || os(tvOS)\n    /// Returns the current screen scale.\n    static let scale: CGFloat = UITraitCollection.current.displayScale\n#elseif os(watchOS)\n    /// Returns the current screen scale.\n    static let scale: CGFloat = WKInterfaceDevice.current().screenScale\n#else\n    /// Always returns 1.\n    static let scale: CGFloat = 1\n#endif\n}\n\n#if os(macOS)\ntypealias Color = NSColor\n#else\ntypealias Color = UIColor\n#endif\n\nextension Color {\n    /// Returns a hex representation of the color, e.g. \"#FFFFAA\".\n    var hex: String {\n        var (r, g, b, a) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0))\n        getRed(&r, green: &g, blue: &b, alpha: &a)\n        let components = [r, g, b, a < 1 ? a : nil]\n        return \"#\" + components\n            .compactMap { $0 }\n            .map { String(format: \"%02lX\", lroundf(Float($0) * 255)) }\n            .joined()\n    }\n}\n\n/// Creates an image thumbnail. Uses significantly less memory than other options.\n/// - parameter data: Data object from which to read the image.\n/// - parameter options: Image loading options.\n/// - parameter scale: The scale factor to assume when interpreting the image data, defaults to 1.\nfunc makeThumbnail(data: Data, options: ImageRequest.ThumbnailOptions, scale: CGFloat = 1.0) -> PlatformImage? {\n    guard let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else {\n        return nil\n    }\n\n    let maxPixelSize = getMaxPixelSize(for: source, options: options)\n    let flags = options.options\n    let options = [\n        kCGImageSourceCreateThumbnailFromImageAlways: flags.contains(.createThumbnailFromImageAlways),\n        kCGImageSourceCreateThumbnailFromImageIfAbsent: flags.contains(.createThumbnailFromImageIfAbsent),\n        kCGImageSourceShouldCacheImmediately: flags.contains(.shouldCacheImmediately),\n        kCGImageSourceCreateThumbnailWithTransform: flags.contains(.createThumbnailWithTransform),\n        kCGImageSourceThumbnailMaxPixelSize: maxPixelSize] as [CFString: Any]\n    guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {\n        return nil\n    }\n    return makeImage(from: image, source: source, scale: scale)\n}\n\n/// Creates a `PlatformImage` from a `CGImage` produced by a `CGImageSource`,\n/// reading EXIF orientation from the source properties.\nfunc makeImage(from cgImage: CGImage, source: CGImageSource, scale: CGFloat = 1.0) -> PlatformImage {\n#if canImport(UIKit)\n    var orientation: UIImage.Orientation = .up\n    if let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [AnyHashable: Any],\n       let orientationValue = imageProperties[kCGImagePropertyOrientation as String] as? UInt32,\n       let cgOrientation = CGImagePropertyOrientation(rawValue: orientationValue) {\n        orientation = UIImage.Orientation(cgOrientation)\n    }\n    return PlatformImage(cgImage: cgImage, scale: scale, orientation: orientation)\n#else\n    return PlatformImage(cgImage: cgImage)\n#endif\n}\n\nprivate func getMaxPixelSize(for source: CGImageSource, options thumbnailOptions: ImageRequest.ThumbnailOptions) -> CGFloat {\n    guard thumbnailOptions.options.contains(.flexible) else {\n        return CGFloat(thumbnailOptions.size.width)\n    }\n    var targetSize = thumbnailOptions.size.cgSize\n    let contentMode = thumbnailOptions.contentMode\n    let options = [kCGImageSourceShouldCache: false] as CFDictionary\n    guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, options) as? [CFString: Any],\n          let width = properties[kCGImagePropertyPixelWidth] as? CGFloat,\n          let height = properties[kCGImagePropertyPixelHeight] as? CGFloat else {\n        return max(targetSize.width, targetSize.height)\n    }\n    let orientation = (properties[kCGImagePropertyOrientation] as? UInt32).flatMap(CGImagePropertyOrientation.init) ?? .up\n#if canImport(UIKit)\n    targetSize = targetSize.rotatedForOrientation(orientation)\n#endif\n    let imageSize = CGSize(width: width, height: height)\n    let scale = imageSize.getScale(targetSize: targetSize, contentMode: contentMode)\n    let size = imageSize.scaled(by: scale).rounded()\n    return max(size.width, size.height)\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/ImagePublisher.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2020-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Combine\n\n/// A publisher that starts a new `ImageTask` when a subscriber is added.\n///\n/// If the requested image is available in the memory cache, the value is\n/// delivered immediately. When the subscription is cancelled, the task also\n/// gets cancelled.\n///\n/// - note: In case the pipeline has `isProgressiveDecodingEnabled` option enabled\n/// and the image being downloaded supports progressive decoding, the publisher\n/// might emit more than a single value.\nstruct ImagePublisher: Publisher, Sendable {\n    typealias Output = ImageResponse\n    typealias Failure = ImagePipeline.Error\n\n    let request: ImageRequest\n    let pipeline: ImagePipeline\n\n    func receive<S>(subscriber: S) where S: Subscriber, S: Sendable, Failure == S.Failure, Output == S.Input {\n        let subscription = ImageSubscription(\n            request: self.request,\n            pipeline: self.pipeline,\n            subscriber: subscriber\n        )\n        subscriber.receive(subscription: subscription)\n    }\n}\n\nprivate final class ImageSubscription<S>: Subscription where S: Subscriber, S: Sendable, S.Input == ImageResponse, S.Failure == ImagePipeline.Error {\n    private var task: ImageTask?\n    private let subscriber: S?\n    private let request: ImageRequest\n    private let pipeline: ImagePipeline\n\n    init(request: ImageRequest, pipeline: ImagePipeline, subscriber: S) {\n        self.pipeline = pipeline\n        self.request = request\n        self.subscriber = subscriber\n\n    }\n\n    func request(_ demand: Subscribers.Demand) {\n        guard demand > 0 else { return }\n        guard let subscriber else { return }\n\n        if let image = pipeline.cache[request] {\n            _ = subscriber.receive(ImageResponse(container: image, request: request, cacheType: .memory))\n\n            if !image.isPreview {\n                subscriber.receive(completion: .finished)\n                return\n            }\n        }\n\n        task = pipeline.loadImage(\n             with: request,\n             progress: { response, _, _ in\n                 if let response {\n                    // Send progressively decoded image (if enabled and if any)\n                     _ = subscriber.receive(response)\n                 }\n             },\n             completion: { result in\n                 switch result {\n                 case let .success(response):\n                    _ = subscriber.receive(response)\n                    subscriber.receive(completion: .finished)\n                 case let .failure(error):\n                     subscriber.receive(completion: .failure(error))\n                 }\n             }\n         )\n    }\n\n    func cancel() {\n        task?.cancel()\n        task = nil\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/ImageRequestKeys.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Uniquely identifies a cache processed image.\nfinal class MemoryCacheKey: Hashable, Sendable {\n    // Using a reference type turned out to be significantly faster\n    private let imageId: String?\n    private let scale: Float\n    private let thumbnail: ImageRequest.ThumbnailOptions?\n    private let processors: [any ImageProcessing]\n\n    init(_ request: ImageRequest) {\n        self.imageId = request.imageID\n        self.scale = request.scale\n        self.thumbnail = request.thumbnail\n        self.processors = request.processors\n    }\n\n    func hash(into hasher: inout Hasher) {\n        hasher.combine(imageId)\n        hasher.combine(scale)\n        hasher.combine(thumbnail)\n        hasher.combine(processors.count)\n    }\n\n    static func == (lhs: MemoryCacheKey, rhs: MemoryCacheKey) -> Bool {\n        lhs.imageId == rhs.imageId && lhs.scale == rhs.scale && lhs.thumbnail == rhs.thumbnail && lhs.processors == rhs.processors\n    }\n}\n\n// MARK: - Identifying Tasks\n\n/// Uniquely identifies a task of retrieving the processed image.\nfinal class TaskLoadImageKey: Hashable, Sendable {\n    private let loadKey: TaskFetchOriginalImageKey\n    private let options: ImageRequest.Options\n    private let processors: [any ImageProcessing]\n\n    init(_ request: ImageRequest) {\n        self.loadKey = TaskFetchOriginalImageKey(request)\n        self.options = request.options\n        self.processors = request.processors\n    }\n\n    func hash(into hasher: inout Hasher) {\n        hasher.combine(loadKey)\n        hasher.combine(options)\n        hasher.combine(processors.count)\n    }\n\n    static func == (lhs: TaskLoadImageKey, rhs: TaskLoadImageKey) -> Bool {\n        lhs.loadKey == rhs.loadKey && lhs.options == rhs.options && lhs.processors == rhs.processors\n    }\n}\n\n/// Uniquely identifies a task of retrieving the original image.\nstruct TaskFetchOriginalImageKey: Hashable {\n    private let dataLoadKey: TaskFetchOriginalDataKey\n    private let scale: Float\n    private let thumbnail: ImageRequest.ThumbnailOptions?\n\n    init(_ request: ImageRequest) {\n        self.dataLoadKey = TaskFetchOriginalDataKey(request)\n        self.scale = request.scale\n        self.thumbnail = request.thumbnail\n    }\n}\n\n/// Uniquely identifies a task of retrieving the original image data.\nstruct TaskFetchOriginalDataKey: Hashable {\n    private let imageId: String?\n    private let cachePolicy: URLRequest.CachePolicy\n    private let allowsCellularAccess: Bool\n\n    init(_ request: ImageRequest) {\n        self.imageId = request.originalImageID\n        switch request.resource {\n        case .url, .data, .image:\n            self.cachePolicy = .useProtocolCachePolicy\n            self.allowsCellularAccess = true\n        case let .urlRequest(urlRequest):\n            self.cachePolicy = urlRequest.cachePolicy\n            self.allowsCellularAccess = urlRequest.allowsCellularAccess\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/LinkedList.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// A doubly linked list.\nfinal class LinkedList<Element> {\n    // first <-> node <-> ... <-> last\n    private(set) var first: Node?\n    private(set) var last: Node?\n\n    deinit {\n        // This way we make sure that the deallocations do no happen recursively\n        // (and potentially overflow the stack).\n        removeAllElements()\n    }\n\n    var isEmpty: Bool {\n        last == nil\n    }\n\n    /// Adds an element to the end of the list.\n    @discardableResult\n    func append(_ element: Element) -> Node {\n        let node = Node(value: element)\n        append(node)\n        return node\n    }\n\n    /// Adds a node to the beginning of the list.\n    func prepend(_ node: Node) {\n        if let first {\n            first.previous = node\n            node.next = first\n            self.first = node\n        } else {\n            first = node\n            last = node\n        }\n    }\n\n    /// Adds a node to the end of the list.\n    func append(_ node: Node) {\n        if let last {\n            last.next = node\n            node.previous = last\n            self.last = node\n        } else {\n            last = node\n            first = node\n        }\n    }\n\n    func remove(_ node: Node) {\n        node.next?.previous = node.previous // node.previous is nil if node=first\n        node.previous?.next = node.next // node.next is nil if node=last\n        if node === last {\n            last = node.previous\n        }\n        if node === first {\n            first = node.next\n        }\n        node.next = nil\n        node.previous = nil\n    }\n\n    func removeAllElements() {\n        // avoid recursive Nodes deallocation\n        var node = first\n        while let next = node?.next {\n            node?.next = nil\n            next.previous = nil\n            node = next\n        }\n        last = nil\n        first = nil\n    }\n\n    final class Node {\n        let value: Element\n        fileprivate var next: Node?\n        fileprivate var previous: Node?\n\n        init(value: Element) {\n            self.value = value\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/Log.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport os\n\nfunc signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType, _ message: @autoclosure () -> String) {\n    guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return }\n\n    let log = log.value\n    let signpostId = OSSignpostID(log: log, object: object)\n    os_signpost(type, log: log, name: name, signpostID: signpostId, \"%{public}s\", message())\n}\n\nfunc signpost<T>(_ name: StaticString, _ work: () throws -> T) rethrows -> T {\n    guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return try work() }\n\n    let log = log.value\n    let signpostId = OSSignpostID(log: log)\n    os_signpost(.begin, log: log, name: name, signpostID: signpostId)\n    let result = try work()\n    os_signpost(.end, log: log, name: name, signpostID: signpostId)\n    return result\n}\n\nprivate let log = Mutex(value: OSLog(subsystem: \"com.github.kean.Nuke.ImagePipeline\", category: \"Image Loading\"))\n\nenum Formatter {\n    static func bytes(_ count: Int) -> String {\n        bytes(Int64(count))\n    }\n\n    static func bytes(_ count: Int64) -> String {\n        ByteCountFormatter().string(fromByteCount: count)\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/Mutex.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\nfinal class Mutex<T>: @unchecked Sendable {\n    private var _value: T\n    private let lock: os_unfair_lock_t\n\n    init(value: T) {\n        self._value = value\n        self.lock = .allocate(capacity: 1)\n        self.lock.initialize(to: os_unfair_lock())\n    }\n\n    deinit {\n        lock.deinitialize(count: 1)\n        lock.deallocate()\n    }\n\n    var value: T {\n        get {\n            os_unfair_lock_lock(lock)\n            defer { os_unfair_lock_unlock(lock) }\n            return _value\n        }\n        set {\n            os_unfair_lock_lock(lock)\n            defer { os_unfair_lock_unlock(lock) }\n            _value = newValue\n        }\n    }\n\n    func withLock<U>(_ closure: (inout T) -> U) -> U {\n        os_unfair_lock_lock(lock)\n        defer { os_unfair_lock_unlock(lock) }\n        return closure(&_value)\n    }\n}\n\nextension Mutex where T: Equatable {\n    /// Atomically sets the value if it differs from the current one.\n    /// Returns `true` if the value was changed.\n    @discardableResult\n    func testAndSet(_ newValue: T) -> Bool {\n        withLock {\n            guard $0 != newValue else { return false }\n            $0 = newValue\n            return true\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/RateLimiter.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Controls the rate at which the work is executed. Uses the classic [token\n/// bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm.\n///\n/// The main use case for rate limiter is to support large (infinite) collections\n/// of images by preventing thrashing of underlying systems, primarily URLSession.\n///\n/// The implementation supports quick bursts of requests which can be executed\n/// without any delays when \"the bucket is full\". This is important to prevent\n/// rate limiter from affecting \"normal\" requests flow.\n@ImagePipelineActor\nfinal class RateLimiter {\n    private var bucket: TokenBucket\n    private var pending = LinkedList<Work>() // fast append, fast remove first\n    private var isExecutingPendingTasks = false\n\n    typealias Work = () -> Bool\n\n    /// Initializes the `RateLimiter` with the given configuration.\n    /// - parameters:\n    ///   - rate: Maximum number of requests per second. 80 by default.\n    ///   - burst: Maximum number of requests which can be executed without any\n    ///   delays when \"bucket is full\". 25 by default.\n    nonisolated init(rate: Int = 80, burst: Int = 25) {\n        self.bucket = TokenBucket(rate: Double(rate), burst: Double(burst))\n    }\n\n    /// - parameter closure: Returns `true` if the closure was executed, `false`\n    /// if the work was cancelled.\n    func execute( _ work: @escaping Work) {\n        if !pending.isEmpty || !bucket.execute(work) {\n            pending.append(work)\n            setNeedsExecutePendingTasks()\n        }\n    }\n\n    private func setNeedsExecutePendingTasks() {\n        guard !isExecutingPendingTasks else {\n            return\n        }\n        isExecutingPendingTasks = true\n        // Compute a delay such that by the time the closure is executed the\n        // bucket is refilled to a point that is able to execute at least one\n        // pending task. With a rate of 80 tasks we expect a refill every ~26 ms\n        // or as soon as the new tasks are added.\n        let bucketRate = 1000.0 / bucket.rate\n        let delay = Int(2.1 * bucketRate) // 14 ms for rate 80 (default)\n        let bounds = min(100, max(15, delay))\n        Task { @ImagePipelineActor in\n            try? await Task.sleep(nanoseconds: UInt64(bounds) * 1_000_000)\n            self.executePendingTasks()\n        }\n    }\n\n    private func executePendingTasks() {\n        while let node = pending.first, bucket.execute(node.value) {\n            pending.remove(node)\n        }\n        isExecutingPendingTasks = false\n        if !pending.isEmpty { // Not all pending items were executed\n            setNeedsExecutePendingTasks()\n        }\n    }\n}\n\nprivate struct TokenBucket {\n    let rate: Double\n    private let burst: Double // maximum bucket size\n    private var bucket: Double\n    private var timestamp: TimeInterval // last refill timestamp\n\n    /// - parameter rate: Rate (tokens/second) at which bucket is refilled.\n    /// - parameter burst: Bucket size (maximum number of tokens).\n    init(rate: Double, burst: Double) {\n        self.rate = rate\n        self.burst = burst\n        self.bucket = burst\n        self.timestamp = CFAbsoluteTimeGetCurrent()\n    }\n\n    /// Returns `true` if the closure was executed, `false` if dropped.\n    mutating func execute(_ work: () -> Bool) -> Bool {\n        refill()\n        guard bucket >= 1.0 else {\n            return false // bucket is empty\n        }\n        if work() {\n            bucket -= 1.0\n        }\n        // If work was cancelled (returned false), don't reduce the bucket\n        return true\n    }\n\n    private mutating func refill() {\n        let now = CFAbsoluteTimeGetCurrent()\n        bucket += rate * max(0, now - timestamp) // rate * (time delta)\n        timestamp = now\n        if bucket > burst { // prevent bucket overflow\n            bucket = burst\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/ResumableData.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Resumable data support. For more info see:\n/// - https://developer.apple.com/library/content/qa/qa1761/_index.html\nstruct ResumableData: Sendable {\n    let data: Data\n    let validator: String // Either Last-Modified or ETag\n\n    init?(response: URLResponse, data: Data) {\n        // Check if \"Accept-Ranges\" is present and the response is valid.\n        guard !data.isEmpty,\n            let response = response as? HTTPURLResponse,\n            data.count < response.expectedContentLength,\n            response.statusCode == 200 /* OK */ || response.statusCode == 206, /* Partial Content */\n            let acceptRanges = response.allHeaderFields[\"Accept-Ranges\"] as? String,\n            acceptRanges.lowercased() == \"bytes\",\n            let validator = ResumableData._validator(from: response) else {\n                return nil\n        }\n\n        // NOTE: https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields\n        // HTTP headers are case insensitive. To simplify your code, certain\n        // header field names are canonicalized into their standard form.\n        // For example, if the server sends a content-length header,\n        // it is automatically adjusted to be Content-Length.\n\n        self.data = data; self.validator = validator\n    }\n\n    private static func _validator(from response: HTTPURLResponse) -> String? {\n        if let entityTag = response.allHeaderFields[\"ETag\"] as? String {\n            return entityTag // Prefer ETag\n        }\n        // There seems to be a bug with ETag where HTTPURLResponse would canonicalize\n        // it to Etag instead of ETag\n        // https://bugs.swift.org/browse/SR-2429\n        if let entityTag = response.allHeaderFields[\"Etag\"] as? String {\n            return entityTag // Prefer ETag\n        }\n        if let lastModified = response.allHeaderFields[\"Last-Modified\"] as? String {\n            return lastModified\n        }\n        return nil\n    }\n\n    func resume(request: inout URLRequest) {\n        var headers = request.allHTTPHeaderFields ?? [:]\n        // \"bytes=1000-\" means bytes from 1000 up to the end (inclusive)\n        headers[\"Range\"] = \"bytes=\\(data.count)-\"\n        headers[\"If-Range\"] = validator\n        request.allHTTPHeaderFields = headers\n    }\n\n    // Check if the server decided to resume the response.\n    static func isResumedResponse(_ response: URLResponse) -> Bool {\n        // \"206 Partial Content\" (server accepted \"If-Range\")\n        (response as? HTTPURLResponse)?.statusCode == 206\n    }\n}\n\n/// Shared cache, uses the same memory pool across multiple pipelines.\n@ImagePipelineActor\nfinal class ResumableDataStorage {\n    static let shared = ResumableDataStorage()\n\n    private var registeredPipelines = Set<UUID>()\n\n    private var cache: Cache<Key, ResumableData>?\n\n    // MARK: Registration\n\n    /// Cost limit for resumable data: 1% of physical memory, capped at 32 MB.\n    static var defaultCostLimit: Int {\n        Int(Double(ProcessInfo.processInfo.physicalMemory) * 0.01)\n    }\n\n    func register(_ id: UUID) {\n        if registeredPipelines.isEmpty {\n            cache = Cache(costLimit: ResumableDataStorage.defaultCostLimit, countLimit: 100)\n        }\n        registeredPipelines.insert(id)\n    }\n\n    func unregister(_ id: UUID) {\n        registeredPipelines.remove(id)\n        if registeredPipelines.isEmpty {\n            cache = nil // Deallocate storage\n        }\n    }\n\n    func removeAllResponses() {\n        cache?.removeAllCachedValues()\n    }\n\n    // MARK: Storage\n\n    func removeResumableData(for request: ImageRequest, pipeline: ImagePipeline) -> ResumableData? {\n        guard let key = Key(request: request, pipeline: pipeline) else { return nil }\n        return cache?.removeValue(forKey: key)\n    }\n\n    func storeResumableData(_ data: ResumableData, for request: ImageRequest, pipeline: ImagePipeline) {\n        guard let key = Key(request: request, pipeline: pipeline) else { return }\n        cache?.set(data, forKey: key, cost: data.data.count)\n    }\n\n    private struct Key: Hashable {\n        let pipelineId: UUID\n        let imageId: String\n\n        init?(request: ImageRequest, pipeline: ImagePipeline) {\n            guard let imageId = request.imageID else {\n                return nil\n            }\n            self.pipelineId = pipeline.id\n            self.imageId = imageId\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Internal/TaskQueue.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// A priority-aware, concurrency-limited work queue that runs on `@ImagePipelineActor`.\n///\n/// `TaskQueue` manages a configurable number of concurrent operations, each backed\n/// by a Swift `Task`. Pending operations are stored in per-priority buckets so the\n/// highest-priority work is always dequeued first (FIFO within the same priority).\n@ImagePipelineActor\npublic final class TaskQueue: Sendable {\n    var runningCount = 0\n    var pendingCount = 0\n    private let buckets = (0..<TaskPriority.allCases.count).map { _ in LinkedList<TaskQueue.Operation>() }\n\n    /// Controls whether the queue drains pending work.\n    ///\n    /// Setting to `true` prevents new work from starting. Already-running\n    /// operations continue to completion. Setting back to `false` resumes\n    /// draining from any context.\n    ///\n    /// Concurrency-safe: concurrent resume calls each spawn a Task on\n    /// `@ImagePipelineActor`, where `drain()` serializes. Double-drains are\n    /// no-ops because the loop condition checks counts.\n    nonisolated public var isSuspended: Bool {\n        get { _isSuspended.value }\n        set {\n            if _isSuspended.testAndSet(newValue), !newValue {\n                Task { @ImagePipelineActor in drain() }\n            }\n        }\n    }\n\n    /// The default value matches the number of cores on the machine. For\n    /// operations like image processing, it's recommended to use a lower number\n    /// to avoid fully saturating the CPU.\n    nonisolated public var maxConcurrentOperationCount: Int {\n        get { _maxConcurrentOperationCount.value }\n        set {\n            let oldValue = _maxConcurrentOperationCount.withLock {\n                let old = $0; $0 = newValue; return old\n            }\n            if newValue > oldValue {\n                Task { @ImagePipelineActor in drain() }\n            }\n        }\n    }\n\n    nonisolated private let _maxConcurrentOperationCount: Mutex<Int>\n    nonisolated private let _isSuspended = Mutex(value: false)\n\n    /// Events emitted by the queue for observation (testing only).\n    enum Event {\n        case enqueued(TaskQueue.Operation)\n        case finished\n        case cancelled(TaskQueue.Operation)\n        case priorityChanged(TaskQueue.Operation)\n    }\n\n    /// Test hook.\n    var onEvent: ((Event) -> Void)?\n\n    /// Initializes the queue.\n    nonisolated public init(maxConcurrentOperationCount: Int = ProcessInfo.processInfo.processorCount) {\n        self._maxConcurrentOperationCount = Mutex(value: maxConcurrentOperationCount)\n    }\n\n    /// Adds work to the queue. The closure runs `@ImagePipelineActor`. The\n    /// concurrency slot is freed when the closure returns.\n    ///\n    /// If the work needs to be performed in a background, the caller needs to\n    /// ensure that happens.\n    @discardableResult\n    func add(_ work: @ImagePipelineActor @Sendable @escaping () async throws -> Void) -> TaskQueue.Operation {\n        let operation = TaskQueue.Operation(queue: self)\n        operation.work = work\n        enqueue(operation)\n        return operation\n    }\n\n    // MARK: - Private\n\n    private func enqueue(_ operation: TaskQueue.Operation) {\n        operation.node = buckets[operation.priority.rawValue].append(operation)\n        pendingCount += 1\n        onEvent?(.enqueued(operation))\n        drain()\n    }\n\n    private func drain() {\n        while !isSuspended && runningCount < maxConcurrentOperationCount && pendingCount > 0 {\n            guard let operation = dequeueHighestPriority() else { break }\n            execute(operation)\n        }\n    }\n\n    private func dequeueHighestPriority() -> TaskQueue.Operation? {\n        for i in stride(from: buckets.count - 1, through: 0, by: -1) {\n            if let node = buckets[i].first {\n                buckets[i].remove(node)\n                node.value.node = nil\n                pendingCount -= 1\n                return node.value\n            }\n        }\n        return nil\n    }\n\n    private func execute(_ operation: TaskQueue.Operation) {\n        runningCount += 1\n        let work = operation.work\n        operation.work = nil\n        operation.task = Task { @ImagePipelineActor [weak self] in\n            try? await work?()\n            self?.operationFinished()\n        }\n    }\n\n    fileprivate func operationFinished() {\n        runningCount -= 1\n        drain()\n        onEvent?(.finished)\n    }\n\n    fileprivate func operationPriorityChanged(_ operation: TaskQueue.Operation, from oldPriority: TaskPriority) {\n        guard let node = operation.node else { return }\n        buckets[oldPriority.rawValue].remove(node)\n        if operation.priority < oldPriority {\n            buckets[operation.priority.rawValue].prepend(node)\n        } else {\n            buckets[operation.priority.rawValue].append(node)\n        }\n        onEvent?(.priorityChanged(operation))\n    }\n\n    fileprivate func operationCancelled(_ operation: TaskQueue.Operation) {\n        guard let node = operation.node else { return }\n        buckets[operation.priority.rawValue].remove(node)\n        operation.node = nil\n        pendingCount -= 1\n        onEvent?(.cancelled(operation))\n    }\n\n    /// A handle to a unit of work enqueued in a ``TaskQueue``.\n    ///\n    /// Use the handle to adjust ``priority`` or ``cancel()`` the operation.\n    /// Priority changes move the operation between the queue's internal buckets;\n    /// cancellation removes it from the queue and cancels the underlying `Task`.\n    @ImagePipelineActor\n    final class Operation: Sendable {\n        /// The scheduling priority. Changing this while the operation is pending\n        /// moves it to the corresponding priority bucket. Changes to a running\n        /// or cancelled operation update the stored value but have no scheduling\n        /// effect.\n        var priority: TaskPriority = .normal {\n            didSet {\n                guard oldValue != priority else { return }\n                queue?.operationPriorityChanged(self, from: oldValue)\n                onPriorityChanged?(priority)\n            }\n        }\n\n        fileprivate var work: (@ImagePipelineActor @Sendable () async throws -> Void)?\n        private(set) var isCancelled = false\n        fileprivate var task: Task<Void, Never>?\n        fileprivate weak var node: LinkedList<TaskQueue.Operation>.Node?\n        private weak let queue: TaskQueue?\n\n        // Test hooks.\n        var onCancelled: (() -> Void)?\n        var onPriorityChanged: ((TaskPriority) -> Void)?\n\n        init(queue: TaskQueue? = nil) {\n            self.queue = queue\n        }\n\n        func cancel() {\n            guard !isCancelled else { return }\n            isCancelled = true\n            work = nil\n            task?.cancel()\n            queue?.operationCancelled(self)\n            onCancelled?()\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Loading/DataLoader.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Provides basic networking using `URLSession`.\npublic final class DataLoader: DataLoading, @unchecked Sendable {\n    public let session: URLSession\n    private let impl: _DataLoader\n\n    /// Determines whether to deliver a partial response body in increments. By\n    /// default, `false`.\n    public var prefersIncrementalDelivery = false\n\n    /// The delegate that gets called for the callbacks handled by the data loader.\n    /// You can use it for observing the session events and modifying some of the\n    /// task behavior, e.g. handling authentication challenges.\n    ///\n    /// For example, you can use it to log network requests using [Pulse](https://github.com/kean/Pulse)\n    /// which is optimized to work with images.\n    ///\n    /// ```swift\n    /// (ImagePipeline.shared.configuration.dataLoader as? DataLoader)?.delegate = URLSessionProxyDelegate()\n    /// ```\n    ///\n    /// - note: The delegate is retained.\n    public var delegate: URLSessionDelegate? {\n        didSet { impl.delegate = delegate }\n    }\n\n    deinit {\n        session.invalidateAndCancel()\n    }\n\n    /// Initializes ``DataLoader`` with the given configuration.\n    ///\n    /// - parameters:\n    ///   - configuration: `URLSessionConfiguration.default` with `URLCache` with\n    ///   0 MB memory capacity and 150 MB disk capacity by default.\n    ///   - validate: Validates the response. By default, check if the status\n    ///   code is in the acceptable range (`200..<300`).\n    public init(configuration: URLSessionConfiguration = DataLoader.defaultConfiguration,\n                validate: @Sendable @escaping (URLResponse) -> Swift.Error? = DataLoader.validate) {\n        self.impl = _DataLoader(validate: validate)\n        let queue = OperationQueue()\n        queue.maxConcurrentOperationCount = 1\n        self.session = URLSession(configuration: configuration, delegate: impl, delegateQueue: queue)\n        self.session.sessionDescription = \"Nuke URLSession\"\n    }\n\n    /// Returns a default configuration which has a `sharedUrlCache` set\n    /// as a `urlCache`.\n    public static var defaultConfiguration: URLSessionConfiguration {\n        let conf = URLSessionConfiguration.default\n        conf.urlCache = DataLoader.sharedUrlCache\n        return conf\n    }\n\n    /// Validates `HTTP` responses by checking that the status code is 2xx. If\n    /// it's not returns ``DataLoader/Error/statusCodeUnacceptable(_:)``.\n    @Sendable public static func validate(response: URLResponse) -> Swift.Error? {\n        guard let response = response as? HTTPURLResponse else {\n            return nil\n        }\n        return (200..<300).contains(response.statusCode) ? nil : Error.statusCodeUnacceptable(response.statusCode)\n    }\n\n#if !os(macOS) && !targetEnvironment(macCatalyst)\n    private static let cachePath = \"com.github.kean.Nuke.Cache\"\n#else\n    private static let cachePath: String = {\n        let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)\n        if let cachePath = cachePaths.first, let identifier = Bundle.main.bundleIdentifier {\n            return cachePath.appending(\"/\" + identifier)\n        }\n\n        return \"\"\n    }()\n#endif\n\n    /// Shared URL cache used by a default ``DataLoader``. The cache is\n    /// initialized with 0 MB memory capacity and 150 MB disk capacity.\n    public static let sharedUrlCache: URLCache = {\n        let diskCapacity = 150 * 1048576 // 150 MB\n#if targetEnvironment(macCatalyst)\n        return URLCache(memoryCapacity: 0, diskCapacity: diskCapacity, directory: URL(fileURLWithPath: cachePath))\n#else\n        return URLCache(memoryCapacity: 0, diskCapacity: diskCapacity, diskPath: cachePath)\n#endif\n    }()\n\n    public func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, any Swift.Error>, URLResponse) {\n        let task = session.dataTask(with: request)\n        task.prefersIncrementalDelivery = prefersIncrementalDelivery\n        return try await impl.loadData(with: task, session: session)\n    }\n\n    /// Errors produced by ``DataLoader``.\n    public enum Error: Swift.Error, CustomStringConvertible {\n        /// Validation failed.\n        case statusCodeUnacceptable(Int)\n\n        public var description: String {\n            switch self {\n            case let .statusCodeUnacceptable(code):\n                return \"Response status code was unacceptable: \\(code.description)\"\n            }\n        }\n    }\n}\n\n// Actual data loader implementation. Hide NSObject inheritance, hide\n// URLSessionDataDelegate conformance, and break retain cycle between URLSession\n// and URLSessionDataDelegate.\nprivate final class _DataLoader: NSObject, URLSessionDataDelegate, @unchecked Sendable {\n    let validate: @Sendable (URLResponse) -> Swift.Error?\n    private var handlers = [URLSessionTask: _Handler]()\n    var delegate: URLSessionDelegate?\n\n    init(validate: @Sendable @escaping (URLResponse) -> Swift.Error?) {\n        self.validate = validate\n    }\n\n    /// Loads data with the given request.\n    func loadData(with task: URLSessionDataTask, session: URLSession) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse) {\n        let handler = _Handler()\n        session.delegateQueue.addOperation { // `URLSession` is configured to use this same queue\n            self.handlers[task] = handler\n        }\n        return try await withTaskCancellationHandler {\n            try await withUnsafeThrowingContinuation { continuation in\n                handler.responseContinuation = continuation\n                task.resume()\n            }\n        } onCancel: {\n            task.cancel()\n        }\n    }\n\n    // MARK: URLSessionDelegate\n\n#if !os(macOS) && !targetEnvironment(macCatalyst)\n    func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {\n        if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) {\n            (delegate as? URLSessionTaskDelegate)?.urlSession?(session, didCreateTask: task)\n        } else {\n            // Doesn't exist on earlier versions\n        }\n    }\n#endif\n\n    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {\n        (delegate as? URLSessionDataDelegate)?.urlSession?(session, dataTask: dataTask, didReceive: response, completionHandler: { _ in })\n\n        guard let handler = handlers[dataTask] else {\n            completionHandler(.cancel)\n            return\n        }\n        if let error = validate(response) {\n            handler.responseContinuation?.resume(throwing: error)\n            handler.responseContinuation = nil\n            completionHandler(.cancel)\n            return\n        }\n        let stream = AsyncThrowingStream<Data, Error> { continuation in\n            handler.streamContinuation = continuation\n            continuation.onTermination = { @Sendable reason in\n                if case .cancelled = reason {\n                    dataTask.cancel()\n                }\n            }\n        }\n        handler.responseContinuation?.resume(returning: (stream, response))\n        handler.responseContinuation = nil\n        completionHandler(.allow)\n    }\n\n    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {\n        (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, didCompleteWithError: error)\n        assert(task is URLSessionDataTask)\n        guard let handler = handlers[task] else {\n            return\n        }\n        handlers[task] = nil\n        if let streamContinuation = handler.streamContinuation {\n            // Response was already delivered; finish the stream.\n            if let error {\n                streamContinuation.finish(throwing: error)\n            } else {\n                streamContinuation.finish()\n            }\n        } else if let responseContinuation = handler.responseContinuation {\n            // Error before response (DNS failure, etc.)\n            responseContinuation.resume(throwing: error ?? URLError(.unknown))\n            handler.responseContinuation = nil\n        }\n    }\n\n    func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {\n        (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, didFinishCollecting: metrics)\n    }\n\n    func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @Sendable @escaping (URLRequest?) -> Void) {\n        (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler) ?? completionHandler(request)\n    }\n\n    func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {\n        (delegate as? URLSessionTaskDelegate)?.urlSession?(session, taskIsWaitingForConnectivity: task)\n    }\n\n    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @Sendable @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {\n        (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, didReceive: challenge, completionHandler: completionHandler) ??\n        completionHandler(.performDefaultHandling, nil)\n    }\n\n    func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @Sendable @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {\n        (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, willBeginDelayedRequest: request, completionHandler: completionHandler) ??\n        completionHandler(.continueLoading, nil)\n    }\n\n    // MARK: URLSessionDataDelegate\n\n    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {\n        (delegate as? URLSessionDataDelegate)?.urlSession?(session, dataTask: dataTask, didReceive: data)\n        handlers[dataTask]?.streamContinuation?.yield(data)\n    }\n\n    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @Sendable @escaping (CachedURLResponse?) -> Void) {\n        (delegate as? URLSessionDataDelegate)?.urlSession?(session, dataTask: dataTask, willCacheResponse: proposedResponse, completionHandler: completionHandler) ??\n        completionHandler(proposedResponse)\n    }\n\n    // MARK: Internal\n\n    private final class _Handler: @unchecked Sendable {\n        var responseContinuation: UnsafeContinuation<(AsyncThrowingStream<Data, Error>, URLResponse), Error>?\n        var streamContinuation: AsyncThrowingStream<Data, Error>.Continuation?\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Loading/DataLoading.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Fetches original image data.\npublic protocol DataLoading: Sendable {\n    /// Loads data for the given request.\n    ///\n    /// - Returns: A tuple of an `AsyncThrowingStream` delivering data chunks\n    ///   and the initial `URLResponse`.\n    func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse)\n}\n"
  },
  {
    "path": "Sources/Nuke/Pipeline/Deprecated.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// - warning: Renamed to ``ImagePipeline/Delegate``.\n@available(*, deprecated, renamed: \"ImagePipeline.Delegate\")\npublic typealias ImagePipelineDelegate = ImagePipeline.Delegate\n\nextension ImageRequest {\n    /// - warning: Renamed to ``imageID``. The new property uses idiomatic Swift naming (uppercase \"ID\") and is writable.\n    @available(*, deprecated, renamed: \"imageID\")\n    public var imageId: String? {\n        get { imageID }\n        set { imageID = newValue }\n    }\n}\n\nextension ImageRequest.UserInfoKey {\n    /// - warning: Use ``ImageRequest/imageID`` instead.\n    @available(*, deprecated, message: \"Use the imageID property on ImageRequest instead\")\n    public static let imageIdKey: ImageRequest.UserInfoKey = \"github.com/kean/nuke/imageId\"\n\n    /// - warning: Use ``ImageRequest/scale`` instead.\n    @available(*, deprecated, message: \"Use the scale property on ImageRequest instead\")\n    public static let scaleKey: ImageRequest.UserInfoKey = \"github.com/kean/nuke/scale\"\n\n    /// - warning: Use ``ImageRequest/thumbnail`` instead.\n    @available(*, deprecated, message: \"Use the thumbnail property on ImageRequest instead\")\n    public static let thumbnailKey: ImageRequest.UserInfoKey = \"github.com/kean/nuke/thumbnail\"\n}\n\nextension ImagePipeline {\n    // MARK: - Loading Images (Closures)\n\n    /// - warning: Soft-deprecated in Nuke 12.9.\n    @discardableResult nonisolated public func loadImage(with url: URL, completion: @escaping @MainActor @Sendable (_ result: Result<ImageResponse, Error>) -> Void) -> ImageTask {\n        _loadImage(with: ImageRequest(url: url), progress: nil, completion: completion)\n    }\n\n    /// - warning: Soft-deprecated in Nuke 12.9.\n    @discardableResult nonisolated public func loadImage(with request: ImageRequest, completion: @escaping @MainActor @Sendable (_ result: Result<ImageResponse, Error>) -> Void) -> ImageTask {\n        _loadImage(with: request, progress: nil, completion: completion)\n    }\n\n    /// - warning: Soft-deprecated in Nuke 12.9.\n    @discardableResult nonisolated public func loadImage(with request: ImageRequest, progress: (@MainActor @Sendable (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?, completion: @escaping @MainActor @Sendable (_ result: Result<ImageResponse, Error>) -> Void) -> ImageTask {\n        _loadImage(with: request, progress: {\n            progress?($0, $1.completed, $1.total)\n        }, completion: completion)\n    }\n\n    nonisolated func _loadImage(\n        with request: ImageRequest,\n        isDataTask: Bool = false,\n        progress: (@MainActor @Sendable (ImageResponse?, ImageTask.Progress) -> Void)?,\n        completion: @escaping @MainActor @Sendable (Result<ImageResponse, Error>) -> Void\n    ) -> ImageTask {\n        makeStartedImageTask(with: request, isDataTask: isDataTask) { event, task in\n            let work: @MainActor @Sendable () -> Void = {\n                // The callback-based API guarantees that after cancellation no\n                // events are called on the callback queue.\n                guard task.state != .cancelled else { return }\n                switch event {\n                case .started: break\n                case .progress(let value): progress?(nil, value)\n                case .preview(let response): progress?(response, task.currentProgress)\n                case .finished(let result):\n                    completion(result)\n                }\n            }\n            DispatchQueue.main.async(execute: work)\n        }\n    }\n\n    // MARK: - Loading Data (Closures)\n\n    /// - warning: Soft-deprecated in Nuke 12.9.\n    @discardableResult nonisolated public func loadData(with request: ImageRequest, completion: @escaping @MainActor @Sendable (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask {\n        _loadImage(with: request, isDataTask: true, progress: nil) { result in\n            let result = result.map { response in\n                (data: response.container.data ?? Data(), response: response.urlResponse)\n            }\n            completion(result)\n        }\n    }\n\n    /// - warning: Soft-deprecated in Nuke 12.9.\n    @discardableResult nonisolated public func loadData(with request: ImageRequest, progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)?, completion: @escaping @MainActor @Sendable (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask {\n        _loadImage(with: request, isDataTask: true) { _, progress in\n            progressHandler?(progress.completed, progress.total)\n        } completion: { result in\n            let result = result.map { response in\n                (data: response.container.data ?? Data(), response: response.urlResponse)\n            }\n            completion(result)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Pipeline/ImagePipeline+Cache.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\nextension ImagePipeline {\n    /// Provides a set of convenience APIs for managing the pipeline cache layers,\n    /// including ``ImageCaching`` (memory cache) and ``DataCaching`` (disk cache).\n    ///\n    /// - important: This class doesn't work with a `URLCache`. For more info,\n    /// see <doc:caching>.\n    public struct Cache: Sendable {\n        let pipeline: ImagePipeline\n        private var configuration: ImagePipeline.Configuration { pipeline.configuration }\n    }\n}\n\nextension ImagePipeline.Cache {\n    // MARK: Subscript (Memory Cache)\n\n    /// Returns an image from the memory cache for the given URL.\n    public subscript(url: URL) -> ImageContainer? {\n        get { self[ImageRequest(url: url)] }\n        nonmutating set { self[ImageRequest(url: url)] = newValue }\n    }\n\n    /// Returns an image from the memory cache for the given request.\n    public subscript(request: ImageRequest) -> ImageContainer? {\n        get {\n            cachedImageFromMemoryCache(for: request)\n        }\n        nonmutating set {\n            if let image = newValue {\n                storeCachedImageInMemoryCache(image, for: request)\n            } else {\n                removeCachedImageFromMemoryCache(for: request)\n            }\n        }\n    }\n\n    // MARK: Cached Images\n\n    /// Returns a cached image from any of the caches.\n    ///\n    /// - note: Respects request options such as its cache policy.\n    ///\n    /// - parameters:\n    ///   - request: The request. Make sure to remove the processors if you want\n    ///   to retrieve an original image (if it's stored).\n    ///   - caches: `[.all]`, by default.\n    public func cachedImage(for request: ImageRequest, caches: Caches = [.all]) -> ImageContainer? {\n        if caches.contains(.memory) {\n            if let image = cachedImageFromMemoryCache(for: request) {\n                return image\n            }\n        }\n        if caches.contains(.disk) {\n            if let data = cachedData(for: request),\n               let image = decodeImageData(data, for: request) {\n                return image\n            }\n        }\n        return nil\n    }\n\n    /// Stores the image in all caches. To store image in the disk cache, it\n    /// will be encoded (see ``ImageEncoding``)\n    ///\n    /// - note: Respects request cache options.\n    ///\n    /// - note: Default ``DataCache`` stores data asynchronously, so it's safe\n    /// to call this method even from the main thread.\n    ///\n    /// - note: Image previews are not stored.\n    ///\n    /// - parameters:\n    ///   - request: The request. Make sure to remove the processors if you want\n    ///   to retrieve an original image (if it's stored).\n    ///   - caches: `[.all]`, by default.\n    public func storeCachedImage(_ image: ImageContainer, for request: ImageRequest, caches: Caches = [.all]) {\n        if caches.contains(.memory) {\n            storeCachedImageInMemoryCache(image, for: request)\n        }\n        if caches.contains(.disk) {\n            if let data = encodeImage(image, for: request) {\n                storeCachedData(data, for: request)\n            }\n        }\n    }\n\n    /// Removes the image from all caches.\n    public func removeCachedImage(for request: ImageRequest, caches: Caches = [.all]) {\n        if caches.contains(.memory) {\n            removeCachedImageFromMemoryCache(for: request)\n        }\n        if caches.contains(.disk) {\n            removeCachedData(for: request)\n        }\n    }\n\n    /// Returns `true` if any of the caches contain the image.\n    public func containsCachedImage(for request: ImageRequest, caches: Caches = [.all]) -> Bool {\n        if caches.contains(.memory) && cachedImageFromMemoryCache(for: request) != nil {\n            return true\n        }\n        if caches.contains(.disk), let dataCache = dataCache(for: request) {\n            let key = makeDataCacheKey(for: request)\n            return dataCache.containsData(for: key)\n        }\n        return false\n    }\n\n    private func cachedImageFromMemoryCache(for request: ImageRequest) -> ImageContainer? {\n        guard !request.options.contains(.disableMemoryCacheReads) else {\n            return nil\n        }\n        guard let imageCache = imageCache(for: request) else {\n            return nil\n        }\n        return imageCache[makeImageCacheKey(for: request)]\n    }\n\n    private func storeCachedImageInMemoryCache(_ image: ImageContainer, for request: ImageRequest) {\n        guard !request.options.contains(.disableMemoryCacheWrites) else {\n            return\n        }\n        guard !image.isPreview || configuration.isStoringPreviewsInMemoryCache else {\n            return\n        }\n        guard let imageCache = imageCache(for: request) else {\n            return\n        }\n        imageCache[makeImageCacheKey(for: request)] = image\n    }\n\n    private func removeCachedImageFromMemoryCache(for request: ImageRequest) {\n        guard let imageCache = imageCache(for: request) else {\n            return\n        }\n        imageCache[makeImageCacheKey(for: request)] = nil\n    }\n\n    // MARK: Cached Data\n\n    /// Returns cached data for the given request.\n    public func cachedData(for request: ImageRequest) -> Data? {\n        guard !request.options.contains(.disableDiskCacheReads) else {\n            return nil\n        }\n        guard let dataCache = dataCache(for: request) else {\n            return nil\n        }\n        let key = makeDataCacheKey(for: request)\n        return dataCache.cachedData(for: key)\n    }\n\n    /// Stores data for the given request.\n    ///\n    /// - note: Default ``DataCache`` stores data asynchronously, so it's safe\n    /// to call this method even from the main thread.\n    public func storeCachedData(_ data: Data, for request: ImageRequest) {\n        guard let dataCache = dataCache(for: request),\n              !request.options.contains(.disableDiskCacheWrites) else {\n            return\n        }\n        let key = makeDataCacheKey(for: request)\n        dataCache.storeData(data, for: key)\n    }\n\n    /// Returns `true` if the data cache contains data for the given image.\n    public func containsData(for request: ImageRequest) -> Bool {\n        guard let dataCache = dataCache(for: request) else {\n            return false\n        }\n        return dataCache.containsData(for: makeDataCacheKey(for: request))\n    }\n\n    /// Removes cached data for the given request.\n    public func removeCachedData(for request: ImageRequest) {\n        guard let dataCache = dataCache(for: request) else {\n            return\n        }\n        let key = makeDataCacheKey(for: request)\n        dataCache.removeData(for: key)\n    }\n\n    // MARK: Keys\n\n    /// Returns image cache (memory cache) key for the given request.\n    public func makeImageCacheKey(for request: ImageRequest) -> ImageCacheKey {\n        if let customKey = pipeline.delegate.cacheKey(for: request, pipeline: pipeline) {\n            return ImageCacheKey(key: customKey)\n        }\n        return ImageCacheKey(request: request) // Use the default key\n    }\n\n    /// Returns data cache (disk cache) key for the given request.\n    public func makeDataCacheKey(for request: ImageRequest) -> String {\n        if let customKey = pipeline.delegate.cacheKey(for: request, pipeline: pipeline) {\n            return customKey\n        }\n        return \"\\(request.imageID ?? \"\")\\(request.thumbnail?.identifier ?? \"\")\\(ImageProcessors.Composition(request.processors).identifier)\"\n    }\n\n    // MARK: Misc\n\n    /// Removes both images and data from all cache layers.\n    ///\n    /// - important: It clears only caches set in the pipeline configuration. If\n    /// you implement ``ImagePipeline/Delegate`` that uses different caches for\n    /// different requests, this won't remove images from them.\n    public func removeAll(caches: Caches = [.all]) {\n        if caches.contains(.memory) {\n            configuration.imageCache?.removeAll()\n        }\n        if caches.contains(.disk) {\n            configuration.dataCache?.removeAll()\n        }\n    }\n\n    // MARK: Private\n\n    private func decodeImageData(_ data: Data, for request: ImageRequest) -> ImageContainer? {\n        let context = ImageDecodingContext(request: request, data: data, cacheType: .disk)\n        guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else {\n            return nil\n        }\n        return (try? decoder.decode(context))?.container\n    }\n\n    private func encodeImage(_ image: ImageContainer, for request: ImageRequest) -> Data? {\n        let context = ImageEncodingContext(request: request, image: image.image, urlResponse: nil)\n        let encoder = pipeline.delegate.imageEncoder(for: context, pipeline: pipeline)\n        return encoder.encode(image, context: context)\n    }\n\n    private func imageCache(for request: ImageRequest) -> (any ImageCaching)? {\n        pipeline.delegate.imageCache(for: request, pipeline: pipeline)\n    }\n\n    private func dataCache(for request: ImageRequest) -> (any DataCaching)? {\n        pipeline.delegate.dataCache(for: request, pipeline: pipeline)\n    }\n\n    // MARK: Options\n\n    /// Describes a set of cache layers to use.\n    public struct Caches: OptionSet {\n        public let rawValue: Int\n        public init(rawValue: Int) {\n            self.rawValue = rawValue\n        }\n\n        public static let memory = Caches(rawValue: 1 << 0)\n        public static let disk = Caches(rawValue: 1 << 1)\n        public static let all: Caches = [.memory, .disk]\n    }\n}\n\nextension ImagePipeline.Cache.Caches: Sendable {}\n"
  },
  {
    "path": "Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport ImageIO\n\nextension ImagePipeline {\n    /// The pipeline configuration.\n    public struct Configuration: Sendable {\n        // MARK: - Dependencies\n\n        /// Data loader used by the pipeline.\n        public var dataLoader: any DataLoading\n\n        /// Data cache used by the pipeline.\n        public var dataCache: (any DataCaching)?\n\n        /// Image cache used by the pipeline.\n        public var imageCache: (any ImageCaching)? {\n            // This exists simply to ensure we don't init ImageCache.shared if the\n            // user provides their own instance.\n            get { isCustomImageCacheProvided ? customImageCache : ImageCache.shared }\n            set {\n                customImageCache = newValue\n                isCustomImageCacheProvided = true\n            }\n        }\n        private var customImageCache: (any ImageCaching)?\n\n        /// Default implementation uses shared ``ImageDecoderRegistry`` to create\n        /// a decoder that matches the context.\n        public var makeImageDecoder: @Sendable (ImageDecodingContext) -> (any ImageDecoding)? = {\n            ImageDecoderRegistry.shared.decoder(for: $0)\n        }\n\n        /// Returns `ImageEncoders.Default()` by default.\n        public var makeImageEncoder: @Sendable (ImageEncodingContext) -> any ImageEncoding = { _ in\n            ImageEncoders.Default()\n        }\n\n        // MARK: - Options\n\n        /// Decompresses the loaded images. By default, enabled on all platforms\n        /// except for `macOS`.\n        ///\n        /// Decompressing compressed image formats (such as JPEG) can significantly\n        /// improve drawing performance as it allows a bitmap representation to be\n        /// created in a background rather than on the main thread.\n        public var isDecompressionEnabled: Bool {\n            get { _isDecompressionEnabled }\n            set { _isDecompressionEnabled = newValue }\n        }\n\n        /// Set this to `true` to use native `preparingForDisplay()` method for\n        /// decompression on iOS and tvOS 15.0 and later. Disabled by default.\n        /// If disabled, CoreGraphics-based decompression is used.\n        public var isUsingPrepareForDisplay: Bool = false\n\n#if os(macOS)\n        var _isDecompressionEnabled = false\n#else\n        var _isDecompressionEnabled = true\n#endif\n\n        /// Determines what images are stored in the disk cache (``DataCaching``).\n        /// ``ImagePipeline/DataCachePolicy/storeOriginalData`` by default.\n        public var dataCachePolicy = ImagePipeline.DataCachePolicy.storeOriginalData\n\n        /// Enables task coalescing. When enabled, the pipeline avoids duplicated\n        /// work when loading images. A task is only cancelled when all requests\n        /// associated with it are cancelled. The pipeline also automatically\n        /// manages the priority of the deduplicated work. `true` by default.\n        ///\n        /// For example, given these two requests:\n        ///\n        /// ```swift\n        /// let url = URL(string: \"http://example.com/image\")\n        /// pipeline.loadImage(with: ImageRequest(url: url, processors: [\n        ///     .resize(size: CGSize(width: 44, height: 44)),\n        ///     .gaussianBlur(radius: 8)\n        /// ]))\n        /// pipeline.loadImage(with: ImageRequest(url: url, processors: [\n        ///     .resize(size: CGSize(width: 44, height: 44))\n        /// ]))\n        /// ```\n        ///\n        /// Nuke loads the image data once, resizes once, and applies the blur\n        /// once — no duplicated work at any stage.\n        public var isTaskCoalescingEnabled = true\n\n        /// Enables the rate limiter. When enabled, the pipeline throttles requests\n        /// to prevent thrashing the underlying systems (e.g. `URLSession`). The\n        /// rate limiter only activates when requests are started and cancelled at\n        /// a high rate, such as during fast scrolling. `true` by default.\n        public var isRateLimiterEnabled = true\n\n        /// Enables progressive decoding. When enabled, the pipeline produces a\n        /// new image preview each time it receives a new chunk of data. Whether\n        /// a preview is produced depends on the decoder — ``ImageDecoders/Default``\n        /// supports progressive JPEG. `false` by default.\n        public var isProgressiveDecodingEnabled = false\n\n        /// The minimum interval between progressive decoding attempts, in\n        /// seconds. When data arrives faster than this interval, intermediate\n        /// chunks are skipped. `0.5` by default.\n        public var progressiveDecodingInterval: TimeInterval = 0.5\n\n        /// Stores progressively generated previews in the memory cache. All\n        /// previews have ``ImageContainer/isPreview`` set to `true`. `true` by\n        /// default.\n        public var isStoringPreviewsInMemoryCache = true\n\n        /// If the data task is terminated (either because of a failure or a\n        /// cancellation) and the image was partially loaded, the next load will\n        /// resume where it left off. Supports both validators (`ETag`,\n        /// `Last-Modified`). Resumable downloads are enabled by default.\n        public var isResumableDataEnabled = true\n\n        /// If enabled, the pipeline will load the local resources (`file` and\n        /// `data` schemes) inline without using the data loader. By default, `true`.\n        public var isLocalResourcesSupportEnabled = true\n\n        /// The maximum decoded image size in bytes allowed before automatic\n        /// downscaling. Images whose decoded bitmap would exceed this limit are\n        /// decoded at a reduced resolution. `nil` disables the check. The\n        /// default value is calculated based on the device's physical memory.\n        public var maximumDecodedImageSize: Int? = {\n            let physicalMemory = ProcessInfo.processInfo.physicalMemory\n            let ratio = physicalMemory <= (536_870_912 /* 512 MB */) ? 0.02 : 0.04\n            let limit = min(67_108_864 /* 64 MB */, physicalMemory / UInt64(1 / ratio))\n            return Int(limit)\n        }()\n\n        /// The maximum response data size in bytes allowed before the download\n        /// is automatically cancelled. Downloads that exceed this limit fail\n        /// with ``ImagePipeline/Error/dataDownloadExceededMaximumSize``. `nil`\n        /// disables the check. The default value is 10% of physical memory,\n        /// capped at 200 MB.\n        public var maximumResponseDataSize: Int? = {\n            let physicalMemory = ProcessInfo.processInfo.physicalMemory\n            let limit = min(209_715_200 /* 200 MB */, physicalMemory / 10)\n            return Int(limit)\n        }()\n\n        // MARK: - Options (Shared)\n\n        /// Enables `os_signpost` logging for measuring performance. When enabled,\n        /// all performance metrics are visible in the Instruments app. `false`\n        /// by default.\n        ///\n        /// For more information, see the [Logging](https://developer.apple.com/documentation/os/logging)\n        /// documentation and [WWDC 2018 Session 405](https://developer.apple.com/videos/play/wwdc2018/405/).\n        public static var isSignpostLoggingEnabled: Bool {\n            get { _isSignpostLoggingEnabled.value }\n            set { _isSignpostLoggingEnabled.value = newValue }\n        }\n\n        private static let _isSignpostLoggingEnabled = Mutex(value: false)\n\n        private var isCustomImageCacheProvided = false\n\n        // MARK: - Task Queues\n\n        /// Data loading queue. Default maximum concurrent task count is 6.\n        public var dataLoadingQueue = TaskQueue(maxConcurrentOperationCount: 6)\n\n        /// Image decoding queue. Default maximum concurrent task count is 1.\n        public var imageDecodingQueue = TaskQueue(maxConcurrentOperationCount: 1)\n\n        /// Image encoding queue. Default maximum concurrent task count is 1.\n        public var imageEncodingQueue = TaskQueue(maxConcurrentOperationCount: 1)\n\n        /// Image processing queue. Default maximum concurrent task count is 2.\n        public var imageProcessingQueue = TaskQueue(maxConcurrentOperationCount: 2)\n\n        /// Image decompressing queue. Default maximum concurrent task count is 2.\n        public var imageDecompressingQueue = TaskQueue(maxConcurrentOperationCount: 2)\n\n        // MARK: - Initializer\n\n        /// Instantiates a pipeline configuration.\n        ///\n        /// - parameter dataLoader: `DataLoader()` by default.\n        public init(dataLoader: any DataLoading = DataLoader()) {\n            self.dataLoader = dataLoader\n        }\n\n        // MARK: - Predefined Configurations\n\n        /// A configuration with an HTTP disk cache (`URLCache`) with a size limit\n        /// of 150 MB. This is a default configuration.\n        ///\n        /// Also uses ``ImageCache/shared`` for in-memory caching with the size\n        /// that adjusts based on the amount of device memory.\n        public static var withURLCache: Configuration { Configuration() }\n\n        /// A configuration with an aggressive disk cache (``DataCache``) with a\n        /// size limit of 150 MB. An HTTP cache (`URLCache`) is disabled.\n        ///\n        /// Also uses ``ImageCache/shared`` for in-memory caching with the size\n        /// that adjusts based on the amount of device memory.\n        public static var withDataCache: Configuration {\n            withDataCache()\n        }\n\n        /// A configuration with an aggressive disk cache (``DataCache``) with a\n        /// size limit of 150 MB by default. An HTTP cache (`URLCache`) is disabled.\n        ///\n        /// Also uses ``ImageCache/shared`` for in-memory caching with the size\n        /// that adjusts based on the amount of device memory.\n        ///\n        /// - parameters:\n        ///   - name: Data cache name.\n        ///   - sizeLimit: Size limit, by default 150 MB.\n        public static func withDataCache(\n            name: String = \"com.github.kean.Nuke.DataCache\",\n            sizeLimit: Int? = nil\n        ) -> Configuration {\n            let dataLoader: DataLoader = {\n                let config = URLSessionConfiguration.default\n                config.urlCache = nil\n                return DataLoader(configuration: config)\n            }()\n\n            var config = Configuration()\n            config.dataLoader = dataLoader\n\n            let dataCache = try? DataCache(name: name)\n            if let sizeLimit {\n                dataCache?.sizeLimit = sizeLimit\n            }\n            config.dataCache = dataCache\n\n            return config\n        }\n    }\n\n    /// Determines what images are stored in the disk cache.\n    @frozen public enum DataCachePolicy: Sendable {\n        /// Store original image data for requests with no processors. Store\n        /// _only_ processed images for requests with processors.\n        ///\n        /// - note: Store only processed images for local resources (file:// or\n        /// data:// URL scheme).\n        ///\n        /// - important: With this policy, the pipeline's ``ImagePipeline/loadData(with:completion:)-6cwk3``\n        /// method will not store the images in the disk cache for requests with\n        /// any processors applied – this method only loads data and doesn't\n        /// decode images.\n        case automatic\n\n        /// Store only original image data.\n        ///\n        /// - note: If the resource is local (file:// or data:// URL scheme),\n        /// data isn't stored.\n        case storeOriginalData\n\n        /// Encode and store images.\n        ///\n        /// - note: This is useful if you want to store images in a format\n        /// different than provided by a server, e.g. decompressed. In other\n        /// scenarios, consider using ``automatic`` policy instead.\n        ///\n        /// - important: With this policy, the pipeline's ``ImagePipeline/loadData(with:completion:)-6cwk3``\n        /// method will not store the images in the disk cache – this method only\n        /// loads data and doesn't decode images.\n        case storeEncodedImages\n\n        /// Stores both processed images and the original image data.\n        ///\n        /// - note: If the resource is local (has file:// or data:// scheme),\n        /// only the processed images are stored.\n        case storeAll\n    }\n\n    /// Determines how progressive (partial) image previews are generated during\n    /// downloads.\n    @frozen public enum PreviewPolicy: Sendable, Equatable {\n        /// Use Image I/O incremental decoding to produce progressive previews.\n        case incremental\n        /// Extract the embedded EXIF thumbnail if available, then stop.\n        case thumbnail\n        /// No previews are generated for partially downloaded data.\n        case disabled\n\n        /// Returns the default policy for the given data: `.incremental` for\n        /// progressive JPEGs and GIFs, `.disabled` for everything else.\n        public static func `default`(for data: Data) -> PreviewPolicy {\n            let type = AssetType(data)\n            if type == .gif {\n                return .incremental\n            }\n            if type == .jpeg && _isProgressiveJPEG(data) {\n                return .incremental\n            }\n            return .disabled\n        }\n\n        private static func _isProgressiveJPEG(_ data: Data) -> Bool {\n            guard let source = CGImageSourceCreateWithData(data as CFData, nil),\n                  let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],\n                  let jfif = properties[kCGImagePropertyJFIFDictionary] as? [CFString: Any],\n                  let isProgressive = jfif[kCGImagePropertyJFIFIsProgressive] as? Bool else {\n                return false\n            }\n            return isProgressive\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\nextension ImagePipeline {\n    /// A delegate that allows you to customize the pipeline dynamically on a per-request basis.\n    ///\n    /// - important: The delegate methods are performed on the pipeline queue in the\n    /// background.\n    public protocol Delegate: AnyObject, Sendable {\n        // MARK: Misc\n\n        /// Returns image decoder for the given context.\n        func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)?\n\n        /// Returns image encoder for the given context.\n        func imageEncoder(for context: ImageEncodingContext, pipeline: ImagePipeline) -> any ImageEncoding\n\n        /// Returns the preview policy for progressive decoding of the given request.\n        func previewPolicy(for context: ImageDecodingContext, pipeline: ImagePipeline) -> ImagePipeline.PreviewPolicy\n\n        // MARK: Data Loading\n\n        /// Returns data loader for the given request.\n        func dataLoader(for request: ImageRequest, pipeline: ImagePipeline) -> any DataLoading\n\n        /// Intercepts the URL request just before data loading begins, allowing\n        /// you to modify or replace it.\n        ///\n        /// Use this hook to inject authentication tokens, sign requests, or\n        /// perform any other async pre-flight work. Throw to cancel the request\n        /// with a meaningful error — for example, when a token refresh fails.\n        ///\n        /// The default implementation returns `urlRequest` unchanged.\n        ///\n        /// - important: Not called for requests using a custom data fetch closure\n        ///   or for local file resources.\n        /// - parameters:\n        ///   - request: The image request being loaded.\n        ///   - urlRequest: The URL request that is about to be sent.\n        ///   - pipeline: The pipeline performing the request.\n        /// - returns: The URL request to use for loading. Return `urlRequest`\n        ///   unchanged to proceed without modification.\n        /// - throws: If an error is thrown, the image request fails with\n        ///   ``ImagePipeline/Error/dataLoadingFailed(error:)`` wrapping the error.\n        func willLoadData(for request: ImageRequest, urlRequest: URLRequest, pipeline: ImagePipeline) async throws -> URLRequest\n\n        // MARK: Caching\n\n        /// Returns in-memory image cache for the given request. Return `nil` to prevent cache reads and writes.\n        func imageCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any ImageCaching)?\n\n        /// Returns disk cache for the given request. Return `nil` to prevent cache\n        /// reads and writes.\n        func dataCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any DataCaching)?\n\n        /// Returns a cache key identifying the image produced for the given request\n        /// (including image processors). The key is used for both in-memory and\n        /// on-disk caches.\n        ///\n        /// Return `nil` to use a default key.\n        func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String?\n\n        /// Gets called when the pipeline is about to save data for the given request.\n        /// The implementation must call the completion closure passing `non-nil` data\n        /// to enable caching or `nil` to prevent it.\n        ///\n        /// This method is called only if the request parameters and data caching policy\n        /// of the pipeline already allow caching.\n        ///\n        /// - parameters:\n        ///   - data: Either the original data or the encoded image in case of storing\n        ///   a processed or re-encoded image.\n        ///   - image: Non-nil in case storing an encoded image.\n        ///   - request: The request for which image is being stored.\n        ///   - completion: The implementation must call the completion closure\n        ///   passing `non-nil` data to enable caching or `nil` to prevent it. You can\n        ///   safely call it synchronously. The callback gets called on the background\n        ///   thread.\n        func willCache(data: Data, image: ImageContainer?, for request: ImageRequest, pipeline: ImagePipeline, completion: @escaping (Data?) -> Void)\n\n        // MARK: Decompression\n\n        /// Returns `true` if the pipeline should decompress the given response.\n        ///\n        /// Called on a background queue managed by the pipeline.\n        func shouldDecompress(response: ImageResponse, for request: ImageRequest, pipeline: ImagePipeline) -> Bool\n\n        /// Decompresses the given image response.\n        ///\n        /// Called on a background queue managed by the pipeline.\n        func decompress(response: ImageResponse, request: ImageRequest, pipeline: ImagePipeline) -> ImageResponse\n\n        // MARK: ImageTask\n\n        /// Gets called when the task is created. Unlike other methods, it is called\n        /// immediately on the caller's queue.\n        func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline)\n\n        /// Gets called when the task receives an event.\n        func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline)\n    }\n}\n\nextension ImagePipeline.Delegate {\n    public func imageCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any ImageCaching)? {\n        pipeline.configuration.imageCache\n    }\n\n    public func dataLoader(for request: ImageRequest, pipeline: ImagePipeline) -> any DataLoading {\n        pipeline.configuration.dataLoader\n    }\n\n    public func willLoadData(\n        for request: ImageRequest,\n        urlRequest: URLRequest,\n        pipeline: ImagePipeline\n    ) async throws -> URLRequest {\n        urlRequest\n    }\n\n    public func dataCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any DataCaching)? {\n        pipeline.configuration.dataCache\n    }\n\n    public func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)? {\n        pipeline.configuration.makeImageDecoder(context)\n    }\n\n    public func imageEncoder(for context: ImageEncodingContext, pipeline: ImagePipeline) -> any ImageEncoding {\n        pipeline.configuration.makeImageEncoder(context)\n    }\n\n    public func previewPolicy(for context: ImageDecodingContext, pipeline: ImagePipeline) -> ImagePipeline.PreviewPolicy {\n        ImagePipeline.PreviewPolicy.default(for: context.data)\n    }\n\n    public func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? {\n        nil\n    }\n\n    public func willCache(data: Data, image: ImageContainer?, for request: ImageRequest, pipeline: ImagePipeline, completion: @escaping (Data?) -> Void) {\n        completion(data)\n    }\n\n    public func shouldDecompress(response: ImageResponse, for request: ImageRequest, pipeline: ImagePipeline) -> Bool {\n        pipeline.configuration.isDecompressionEnabled\n    }\n\n    public func decompress(response: ImageResponse, request: ImageRequest, pipeline: ImagePipeline) -> ImageResponse {\n        var response = response\n        response.container.image = ImageDecompression.decompress(image: response.image, isUsingPrepareForDisplay: pipeline.configuration.isUsingPrepareForDisplay)\n        return response\n    }\n\n    public func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {}\n\n    public func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) {}\n}\n\nfinal class ImagePipelineDefaultDelegate: ImagePipeline.Delegate {}\n"
  },
  {
    "path": "Sources/Nuke/Pipeline/ImagePipeline+Error.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\nextension ImagePipeline {\n    /// Represents all possible image pipeline errors.\n    public enum Error: Swift.Error, CustomStringConvertible, @unchecked Sendable {\n        /// Returned if data is not cached and ``ImageRequest/Options-swift.struct/returnCacheDataDontLoad`` option is specified.\n        case dataMissingInCache\n        /// Data loader failed to load image data with a wrapped error.\n        case dataLoadingFailed(error: Swift.Error)\n        /// Data loader returned empty data.\n        case dataIsEmpty\n        /// No decoder registered for the given data.\n        ///\n        /// This error can only be thrown if the pipeline has custom decoders.\n        /// By default, the pipeline uses ``ImageDecoders/Default`` as a catch-all.\n        case decoderNotRegistered(context: ImageDecodingContext)\n        /// Decoder failed to produce a final image.\n        case decodingFailed(decoder: any ImageDecoding, context: ImageDecodingContext, error: Swift.Error)\n        /// Processor failed to produce a final image.\n        case processingFailed(processor: any ImageProcessing, context: ImageProcessingContext, error: Swift.Error)\n        /// Load image method was called with no image request or no URL.\n        case imageRequestMissing\n        /// Image pipeline is invalidated and no requests can be made.\n        case pipelineInvalidated\n        /// The downloaded data exceeded ``ImagePipeline/Configuration/maximumResponseDataSize``.\n        case dataDownloadExceededMaximumSize\n        /// The image task was cancelled.\n        case cancelled\n    }\n}\n\nextension ImagePipeline.Error {\n    /// Returns underlying data loading error.\n    public var dataLoadingError: Swift.Error? {\n        switch self {\n        case .dataLoadingFailed(let error):\n            return error\n        default:\n            return nil\n        }\n    }\n\n    public var description: String {\n        switch self {\n        case .dataMissingInCache:\n            return \"Failed to load data from cache and download is disabled.\"\n        case let .dataLoadingFailed(error):\n            return \"Failed to load image data. Underlying error: \\(error).\"\n        case .dataIsEmpty:\n            return \"Data loader returned empty data.\"\n        case .decoderNotRegistered:\n            return \"No decoders registered for the downloaded data.\"\n        case let .decodingFailed(decoder, _, error):\n            let underlying = error is ImageDecodingError ? \"\" : \" Underlying error: \\(error).\"\n            return \"Failed to decode image data using decoder \\(decoder).\\(underlying)\"\n        case let .processingFailed(processor, _, error):\n            let underlying = error is ImageProcessingError ? \"\" : \" Underlying error: \\(error).\"\n            return \"Failed to process the image using processor \\(processor).\\(underlying)\"\n        case .imageRequestMissing:\n            return \"Load image method was called with no image request or no URL.\"\n        case .pipelineInvalidated:\n            return \"Image pipeline is invalidated and no requests can be made.\"\n        case .dataDownloadExceededMaximumSize:\n            return \"The downloaded data exceeded the maximum allowed size.\"\n        case .cancelled:\n            return \"The image task was cancelled.\"\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Pipeline/ImagePipeline.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Combine\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\n/// Downloads, decodes, processes, and caches images.\n///\n/// The pipeline is the central component of Nuke. It orchestrates a graph of\n/// tasks - fetching data, decoding, processing, and decompressing images -\n/// while automatically coalescing duplicate work, respecting priorities, and\n/// managing multiple cache layers.\n///\n/// ```swift\n/// let image = try await ImagePipeline.shared.image(for: url)\n/// ```\n///\n/// Use ``ImagePipeline/Configuration-swift.struct`` to customize behavior, or\n/// ``ImagePipeline/shared`` to use the default pipeline.\n@ImagePipelineActor\npublic final class ImagePipeline: Sendable {\n    /// Returns the shared image pipeline.\n    nonisolated public static var shared: ImagePipeline {\n        get { _shared.value }\n        set { _shared.value = newValue }\n    }\n\n    private nonisolated static let _shared = Mutex(value: ImagePipeline(configuration: .withURLCache))\n\n    /// The pipeline configuration.\n    nonisolated public let configuration: Configuration\n\n    /// Provides access to the underlying caching subsystems.\n    nonisolated public var cache: ImagePipeline.Cache { .init(pipeline: self) }\n\n    let delegate: any ImagePipeline.Delegate\n\n    private var tasks = [ImageTask: TaskSubscription]()\n\n    private let tasksLoadData: TaskPool<TaskLoadImageKey, ImageResponse, Error>\n    private let tasksLoadImage: TaskPool<TaskLoadImageKey, ImageResponse, Error>\n    private let tasksFetchOriginalImage: TaskPool<TaskFetchOriginalImageKey, ImageResponse, Error>\n    private let tasksFetchOriginalData: TaskPool<TaskFetchOriginalDataKey, (Data, URLResponse?), Error>\n\n    private var isInvalidated = false\n\n    private nonisolated var nextTaskId: UInt64 {\n        _nextTaskId.withLock { value in\n            value += 1\n            return value\n        }\n    }\n    private nonisolated let _nextTaskId = Mutex<UInt64>(value: 0)\n\n    let rateLimiter: RateLimiter?\n    nonisolated let id = UUID()\n    nonisolated(unsafe) var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes\n\n    nonisolated deinit {\n        let id = self.id\n        Task { @ImagePipelineActor in ResumableDataStorage.shared.unregister(id) }\n    }\n\n    /// Initializes the instance with the given configuration.\n    ///\n    /// - parameters:\n    ///   - configuration: The pipeline configuration.\n    ///   - delegate: Provides more ways to customize the pipeline behavior on per-request basis.\n    nonisolated public init(\n        configuration: Configuration = Configuration(),\n        delegate: (any ImagePipeline.Delegate)? = nil\n    ) {\n        self.configuration = configuration\n        self.rateLimiter = configuration.isRateLimiterEnabled ? RateLimiter() : nil\n        self.delegate = delegate ?? ImagePipelineDefaultDelegate()\n        (configuration.dataLoader as? DataLoader)?.prefersIncrementalDelivery = configuration.isProgressiveDecodingEnabled\n\n        let isCoalescingEnabled = configuration.isTaskCoalescingEnabled\n        self.tasksLoadData = TaskPool(isCoalescingEnabled)\n        self.tasksLoadImage = TaskPool(isCoalescingEnabled)\n        self.tasksFetchOriginalImage = TaskPool(isCoalescingEnabled)\n        self.tasksFetchOriginalData = TaskPool(isCoalescingEnabled)\n\n        let id = self.id\n        Task { @ImagePipelineActor in ResumableDataStorage.shared.register(id) }\n    }\n\n    /// A convenience way to initialize the pipeline with a closure.\n    ///\n    /// Example usage:\n    ///\n    /// ```swift\n    /// ImagePipeline {\n    ///     $0.dataCache = try? DataCache(name: \"com.myapp.datacache\")\n    ///     $0.dataCachePolicy = .automatic\n    /// }\n    /// ```\n    ///\n    /// - parameters:\n    ///   - configuration: The pipeline configuration.\n    ///   - delegate: Provides more ways to customize the pipeline behavior on per-request basis.\n    nonisolated public convenience init(delegate: (any ImagePipeline.Delegate)? = nil, _ configure: (inout ImagePipeline.Configuration) -> Void) {\n        var configuration = ImagePipeline.Configuration()\n        configure(&configuration)\n        self.init(configuration: configuration, delegate: delegate)\n    }\n\n    /// Invalidates the pipeline and cancels all outstanding tasks. Any new\n    /// requests will immediately fail with ``ImagePipeline/Error/pipelineInvalidated`` error.\n    nonisolated public func invalidate() {\n        Task { @ImagePipelineActor in\n            guard !self.isInvalidated else { return }\n            self.isInvalidated = true\n            self.tasks.keys.forEach(self.imageTaskCancelCalled)\n        }\n    }\n\n    // MARK: - Loading Images (Async/Await)\n\n    /// Creates a task with the given URL.\n    ///\n    /// The task starts executing the moment it is created.\n    nonisolated public func imageTask(with url: URL) -> ImageTask {\n        makeStartedImageTask(with: ImageRequest(url: url))\n    }\n\n    /// Creates a task with the given request.\n    ///\n    /// The task starts executing the moment it is created.\n    nonisolated public func imageTask(with request: ImageRequest) -> ImageTask {\n        makeStartedImageTask(with: request)\n    }\n\n    /// Returns an image for the given URL.\n    nonisolated public func image(for url: URL) async throws(ImagePipeline.Error) -> PlatformImage {\n        try await image(for: ImageRequest(url: url))\n    }\n\n    /// Returns an image for the given request.\n    nonisolated public func image(for request: ImageRequest) async throws(ImagePipeline.Error) -> PlatformImage {\n        try await imageTask(with: request).image\n    }\n\n    // MARK: - Loading Data (Async/Await)\n\n    /// Returns image data for the given request.\n    ///\n    /// - parameter request: An image request.\n    nonisolated public func data(for request: ImageRequest) async throws(ImagePipeline.Error) -> (Data, URLResponse?) {\n        let task = makeStartedImageTask(with: request, isDataTask: true)\n        let response = try await task.response\n        return (response.container.data ?? Data(), response.urlResponse)\n    }\n\n    // MARK: - Loading Images (Combine)\n\n    /// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added.\n    nonisolated public func imagePublisher(with url: URL) -> AnyPublisher<ImageResponse, ImagePipeline.Error> {\n        imagePublisher(with: ImageRequest(url: url))\n    }\n\n    /// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added.\n    nonisolated public func imagePublisher(with request: ImageRequest) -> AnyPublisher<ImageResponse, ImagePipeline.Error> {\n        ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher()\n    }\n\n    // MARK: - ImageTask (Internal)\n\n    nonisolated func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask {\n        let task = ImageTask(taskId: nextTaskId, request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent)\n        // Important to call it before `imageTaskStartCalled`\n        if !isDataTask {\n            delegate.imageTaskCreated(task, pipeline: self)\n        }\n        task._task = Task { @ImagePipelineActor in\n            try await withUnsafeThrowingContinuation { continuation in\n                task._continuation = continuation\n                self.startImageTask(task, isDataTask: isDataTask)\n            }\n        }\n        return task\n    }\n\n    // By this time, the task has `continuation` set and is fully wired.\n    private func startImageTask(_ task: ImageTask, isDataTask: Bool) {\n        guard task._state != .cancelled else {\n            // The task gets started asynchronously in a `Task` and cancellation\n            // can happen before the pipeline reached `startImageTask`. In that\n            // case, the `cancel` method do no send the task event.\n            return task._dispatch(.finished(.failure(.cancelled)))\n        }\n        guard !isInvalidated else {\n            return task._process(.error(.pipelineInvalidated))\n        }\n        let worker = isDataTask ? makeTaskLoadData(for: task.request) : makeTaskLoadImage(for: task.request)\n        tasks[task] = worker.subscribe(priority: task.priority.taskPriority, subscriber: task) { [weak task] in\n            task?._process($0)\n        }\n        if !isDataTask {\n            delegate.imageTask(task, didReceiveEvent: .started, pipeline: self)\n        }\n        onTaskStarted?(task)\n    }\n\n    // MARK: - Image Task Events\n\n    func imageTaskCancelCalled(_ task: ImageTask) {\n        tasks.removeValue(forKey: task)?.unsubscribe()\n        task._cancel()\n    }\n\n    func imageTaskUpdatePriorityCalled(_ task: ImageTask, priority: ImageRequest.Priority) {\n        tasks[task]?.setPriority(priority.taskPriority)\n    }\n\n    func imageTask(_ task: ImageTask, didProcessEvent event: ImageTask.Event, isDataTask: Bool) {\n        switch event {\n        case .finished:\n            tasks[task] = nil\n        default: break\n        }\n\n        if !isDataTask {\n            delegate.imageTask(task, didReceiveEvent: event, pipeline: self)\n        }\n    }\n\n    // MARK: - Task Factory (Private)\n\n    // When you request an image or image data, the pipeline creates a graph of tasks\n    // (some tasks are added to the graph on demand).\n    //\n    // `loadImage()` call is represented by TaskLoadImage:\n    //\n    // TaskLoadImage -> TaskFetchOriginalImage -> TaskFetchOriginalData\n    //\n    // `loadData()` call is represented by TaskLoadData:\n    //\n    // TaskLoadData -> TaskFetchOriginalData\n    //\n    //\n    // Each task represents a resource or a piece of work required to produce the\n    // final result. The pipeline reduces the amount of duplicated work by coalescing\n    // the tasks that represent the same work. For example, if you all `loadImage()`\n    // and `loadData()` with the same request, only on `TaskFetchOriginalImageData`\n    // is created. The work is split between tasks to minimize any duplicated work.\n\n    func makeTaskLoadImage(for request: ImageRequest) -> AsyncTask<ImageResponse, Error>.Publisher {\n        tasksLoadImage.publisherForKey(TaskLoadImageKey(request)) {\n            TaskLoadImage(self, request)\n        }\n    }\n\n    func makeTaskLoadData(for request: ImageRequest) -> AsyncTask<ImageResponse, Error>.Publisher {\n        tasksLoadData.publisherForKey(TaskLoadImageKey(request)) {\n            TaskLoadData(self, request)\n        }\n    }\n\n    func makeTaskFetchOriginalImage(for request: ImageRequest) -> AsyncTask<ImageResponse, Error>.Publisher {\n        tasksFetchOriginalImage.publisherForKey(TaskFetchOriginalImageKey(request)) {\n            TaskFetchOriginalImage(self, request)\n        }\n    }\n\n    func makeTaskFetchOriginalData(for request: ImageRequest) -> AsyncTask<(Data, URLResponse?), Error>.Publisher {\n        tasksFetchOriginalData.publisherForKey(TaskFetchOriginalDataKey(request)) {\n            TaskFetchOriginalData(self, request)\n        }\n    }\n\n}\n"
  },
  {
    "path": "Sources/Nuke/Pipeline/ImagePipelineActor.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// A global actor that serializes access to ``ImagePipeline`` and its\n/// internal task graph. All pipeline coordination — starting, cancelling,\n/// and coalescing image tasks, updating priorities, and delivering\n/// delegate callbacks — runs on this actor.\n///\n/// Heavy work such as decoding, processing, and decompression is *not*\n/// performed on this actor.\n@globalActor public actor ImagePipelineActor {\n    public static let shared = ImagePipelineActor()\n}\n"
  },
  {
    "path": "Sources/Nuke/Prefetching/ImagePrefetcher.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Prefetches and caches images to eliminate delays when requesting the same\n/// images later.\n///\n/// The prefetcher cancels all of the outstanding tasks when deallocated.\n///\n/// All ``ImagePrefetcher`` methods are thread-safe and are optimized to be used\n/// even from the main thread during scrolling.\n@ImagePipelineActor\npublic final class ImagePrefetcher: Sendable {\n    /// Pauses the prefetching.\n    ///\n    /// - note: When you pause, the prefetcher will finish outstanding tasks\n    /// (by default, there are only 2 at a time), and pause the rest.\n    nonisolated public var isPaused: Bool {\n        get { queue.isSuspended }\n        set { queue.isSuspended = newValue }\n    }\n\n    /// The priority of the requests. By default, ``ImageRequest/Priority-swift.enum/low``.\n    ///\n    /// Changing the priority also changes the priority of all of the outstanding\n    /// tasks managed by the prefetcher.\n    nonisolated public var priority: ImageRequest.Priority {\n        get { _priority.value }\n        set {\n            guard _priority.value != newValue else { return }\n            _priority.value = newValue\n            Task { @ImagePipelineActor in self.didUpdatePriority(to: newValue) }\n        }\n    }\n    private nonisolated let _priority = Mutex(value: ImageRequest.Priority.low)\n\n    /// Prefetching destination.\n    @frozen public enum Destination: Sendable {\n        /// Prefetches the image and stores it in both the memory and the disk\n        /// cache (make sure to enable it).\n        case memoryCache\n\n        /// Prefetches the image data and stores it in disk caches. It does not\n        /// require decoding the image data and therefore requires less CPU.\n        ///\n        /// - important: This option is incompatible with ``ImagePipeline/DataCachePolicy/automatic``\n        /// (for requests with processors) and ``ImagePipeline/DataCachePolicy/storeEncodedImages``.\n        case diskCache\n    }\n\n    /// The closure that gets called when the prefetching completes for all the\n    /// scheduled requests. The closure is always called on completion,\n    /// regardless of whether the requests succeed or some fail.\n    nonisolated(unsafe) public var didComplete: (@MainActor @Sendable () -> Void)?\n\n    private let pipeline: ImagePipeline\n    private let destination: Destination\n    private var tasks = [TaskLoadImageKey: PrefetchTask]()\n    let queue: TaskQueue // internal for testing\n\n    /// Initializes the ``ImagePrefetcher`` instance.\n    ///\n    /// - parameters:\n    ///   - pipeline: The pipeline used for loading images.\n    ///   - destination: By default load images in all cache layers.\n    ///   - maxConcurrentRequestCount: 2 by default.\n    nonisolated public init(\n        pipeline: ImagePipeline = ImagePipeline.shared,\n        destination: Destination = .memoryCache,\n        maxConcurrentRequestCount: Int = 2\n    ) {\n        self.pipeline = pipeline\n        self.destination = destination\n        self.queue = TaskQueue(maxConcurrentOperationCount: maxConcurrentRequestCount)\n    }\n\n    nonisolated deinit {\n        let tasks = self.tasks.values\n        Task { @ImagePipelineActor in\n            for task in tasks {\n                task.cancel()\n            }\n        }\n    }\n\n    /// Starts prefetching images for the given URL.\n    ///\n    /// See also ``startPrefetching(with:)-718dg`` that works with ``ImageRequest``.\n    nonisolated public func startPrefetching(with urls: [URL]) {\n        startPrefetching(with: urls.map { ImageRequest(url: $0) })\n    }\n\n    /// Starts prefetching images for the given requests.\n    ///\n    /// When you need to display the same image later, use the ``ImagePipeline``\n    /// or the view extensions to load it as usual. The pipeline will take care\n    /// of coalescing the requests to avoid any duplicate work.\n    ///\n    /// The priority of the requests is set to the priority of the prefetcher\n    /// (`.low` by default).\n    ///\n    /// See also ``startPrefetching(with:)-1jef2`` that works with `URL`.\n    nonisolated public func startPrefetching(with requests: [ImageRequest]) {\n        Task { @ImagePipelineActor in\n            self._startPrefetching(with: requests)\n        }\n    }\n\n    private func _startPrefetching(with requests: [ImageRequest]) {\n        let currentPriority = _priority.value\n        for request in requests {\n            var request = request\n            if currentPriority != request.priority {\n                request.priority = currentPriority\n            }\n            _startPrefetching(with: request)\n        }\n        sendCompletionIfNeeded()\n    }\n\n    private func _startPrefetching(with request: ImageRequest) {\n        guard pipeline.cache[request] == nil else {\n            return\n        }\n        let key = TaskLoadImageKey(request)\n        guard tasks[key] == nil else {\n            return\n        }\n        let task = PrefetchTask(request: request, key: key)\n        let pipeline = self.pipeline\n        let isDataTask = destination == .diskCache\n        let operation = queue.add { [weak self] in\n            let imageTask = pipeline.makeStartedImageTask(with: task.request, isDataTask: isDataTask)\n            task.imageTask = imageTask\n            _ = try? await imageTask.response\n            self?._remove(task)\n        }\n        operation.priority = request.priority.taskPriority\n        task.operation = operation\n        tasks[key] = task\n    }\n\n    private func _remove(_ task: PrefetchTask) {\n        guard tasks[task.key] === task else { return }\n        tasks[task.key] = nil\n        sendCompletionIfNeeded()\n    }\n\n    private func sendCompletionIfNeeded() {\n        guard tasks.isEmpty, let callback = didComplete else {\n            return\n        }\n        DispatchQueue.main.async(execute: callback)\n    }\n\n    /// Stops prefetching images for the given URLs and cancels outstanding\n    /// requests.\n    ///\n    /// See also ``stopPrefetching(with:)-8cdam`` that works with ``ImageRequest``.\n    nonisolated public func stopPrefetching(with urls: [URL]) {\n        stopPrefetching(with: urls.map { ImageRequest(url: $0) })\n    }\n\n    /// Stops prefetching images for the given requests and cancels outstanding\n    /// requests.\n    ///\n    /// You don't need to balance the number of `start` and `stop` requests.\n    /// If you have multiple screens with prefetching, create multiple instances\n    /// of ``ImagePrefetcher``.\n    ///\n    /// See also ``stopPrefetching(with:)-2tcyq`` that works with `URL`.\n    nonisolated public func stopPrefetching(with requests: [ImageRequest]) {\n        Task { @ImagePipelineActor in\n            for request in requests {\n                self._stopPrefetching(with: request)\n            }\n        }\n    }\n\n    private func _stopPrefetching(with request: ImageRequest) {\n        if let task = tasks.removeValue(forKey: TaskLoadImageKey(request)) {\n            task.cancel()\n        }\n    }\n\n    /// Stops all prefetching tasks.\n    nonisolated public func stopPrefetching() {\n        Task { @ImagePipelineActor in\n            self.tasks.values.forEach { $0.cancel() }\n            self.tasks.removeAll()\n        }\n    }\n\n    private func didUpdatePriority(to priority: ImageRequest.Priority) {\n        let taskPriority = priority.taskPriority\n        for task in tasks.values {\n            task.imageTask?.priority = priority\n            task.operation?.priority = taskPriority\n        }\n    }\n\n    @ImagePipelineActor\n    private final class PrefetchTask: Sendable {\n        let key: TaskLoadImageKey\n        let request: ImageRequest\n        weak var imageTask: ImageTask?\n        weak var operation: TaskQueue.Operation?\n\n        init(request: ImageRequest, key: TaskLoadImageKey) {\n            self.request = request\n            self.key = key\n        }\n\n        // When task is cancelled, it is removed from the prefetcher and can\n        // never get cancelled twice.\n        func cancel() {\n            operation?.cancel()\n            imageTask?._cancelTask()\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageDecompression.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\nenum ImageDecompression {\n    static func isDecompressionNeeded(for response: ImageResponse) -> Bool {\n        isDecompressionNeeded(for: response.image) ?? false\n    }\n\n    static func decompress(image: PlatformImage, isUsingPrepareForDisplay: Bool = false) -> PlatformImage {\n        image.decompressed(isUsingPrepareForDisplay: isUsingPrepareForDisplay) ?? image\n    }\n\n    // MARK: Managing Decompression State\n\n    // Safe because it's never mutated.\n    nonisolated(unsafe) static let isDecompressionNeededAK = malloc(1)!\n\n    static func setDecompressionNeeded(_ isDecompressionNeeded: Bool, for image: PlatformImage) {\n        objc_setAssociatedObject(image, isDecompressionNeededAK, isDecompressionNeeded, .OBJC_ASSOCIATION_RETAIN)\n    }\n\n    static func isDecompressionNeeded(for image: PlatformImage) -> Bool? {\n        objc_getAssociatedObject(image, isDecompressionNeededAK) as? Bool\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessing.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\n/// Transforms an image as part of the pipeline processing step.\n///\n/// For basic processing needs, implement the following method:\n///\n/// ```swift\n/// func process(image: PlatformImage) -> PlatformImage?\n/// ```\n///\n/// If your processor needs to manipulate image metadata (``ImageContainer``), or\n/// get access to more information via the context (``ImageProcessingContext``),\n/// there is an additional method that allows you to do that:\n///\n/// ```swift\n/// func process(image container: ImageContainer, context: ImageProcessingContext) -> ImageContainer?\n/// ```\n///\n/// You must implement either one of those methods.\npublic protocol ImageProcessing: Sendable {\n    /// Returns a processed image. By default, returns `nil`.\n    ///\n    /// - note: Gets called on a background queue managed by the pipeline.\n    func process(_ image: PlatformImage) -> PlatformImage?\n\n    /// Optional method. Returns a processed image. By default, this calls the\n    /// basic `process(image:)` method.\n    ///\n    /// - note: Gets called on a background queue managed by the pipeline.\n    func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer\n\n    /// Returns a string that uniquely identifies the processor.\n    ///\n    /// Consider using the reverse DNS notation.\n    var identifier: String { get }\n\n    /// Returns a unique processor identifier.\n    ///\n    /// The default implementation simply returns `var identifier: String` but\n    /// can be overridden as a performance optimization - creating and comparing\n    /// strings is _expensive_ so you can opt-in to return something which is\n    /// fast to create and to compare. See ``ImageProcessors/Resize`` for an example.\n    ///\n    /// - note: A common approach is to make your processor `Hashable` and return `self`\n    /// as a hashable identifier.\n    var hashableIdentifier: AnyHashable { get }\n}\n\nextension ImageProcessing {\n    /// The default implementation simply calls the basic\n    /// `process(_ image: PlatformImage) -> PlatformImage?` method.\n    public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer {\n        guard let output = process(container.image) else {\n            throw ImageProcessingError.unknown\n        }\n        var container = container\n        container.image = output\n        return container\n    }\n\n    /// The default implementation simply returns `var identifier: String`.\n    public var hashableIdentifier: AnyHashable { identifier }\n}\n\nextension ImageProcessing where Self: Hashable {\n    public var hashableIdentifier: AnyHashable { self }\n}\n\n/// Context passed to an ``ImageProcessing`` implementation.\n///\n/// Provides access to the originating request, the current response, and\n/// whether the response is the final (fully downloaded) image or a progressive\n/// preview.\npublic struct ImageProcessingContext: Sendable {\n    public var request: ImageRequest\n    public var response: ImageResponse\n    public var isCompleted: Bool\n\n    public init(request: ImageRequest, response: ImageResponse, isCompleted: Bool) {\n        self.request = request\n        self.response = response\n        self.isCompleted = isCompleted\n    }\n}\n\n/// An error thrown by an ``ImageProcessing`` implementation.\npublic enum ImageProcessingError: Error, CustomStringConvertible, Sendable {\n    /// The processor failed for an unspecified reason.\n    case unknown\n\n    public var description: String { \"Unknown\" }\n}\n\nfunc == (lhs: [any ImageProcessing], rhs: [any ImageProcessing]) -> Bool {\n    guard lhs.count == rhs.count else {\n        return false\n    }\n    // Lazily creates `hashableIdentifiers` because for some processors the\n    // identifiers might be expensive to compute.\n    return zip(lhs, rhs).allSatisfy {\n        $0.hashableIdentifier == $1.hashableIdentifier\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessingOptions.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\n/// A namespace with shared image processing options.\npublic enum ImageProcessingOptions: Sendable {\n\n    /// A unit for size and radius values used in image processors.\n    @frozen public enum Unit: CustomStringConvertible, Sendable {\n        /// Points, automatically scaled to the screen's pixel density.\n        case points\n        /// Pixels, used as-is without any scaling.\n        case pixels\n\n        public var description: String {\n            switch self {\n            case .points: return \"points\"\n            case .pixels: return \"pixels\"\n            }\n        }\n    }\n\n    /// Draws a border.\n    ///\n    /// - important: To make sure that the border looks the way you expect,\n    /// make sure that the images you display exactly match the size of the\n    /// views in which they get displayed. If you can't guarantee that, please\n    /// consider adding border to a view layer. This should be your primary\n    /// option regardless.\n    public struct Border: Hashable, CustomStringConvertible, Sendable {\n        public let width: CGFloat\n\n#if canImport(UIKit)\n        public let color: UIColor\n\n        /// - parameters:\n        ///   - color: Border color.\n        ///   - width: Border width.\n        ///   - unit: Unit of the width.\n        public init(color: UIColor, width: CGFloat = 1, unit: Unit = .points) {\n            self.color = color\n            self.width = width.converted(to: unit)\n        }\n#else\n        public let color: NSColor\n\n        /// - parameters:\n        ///   - color: Border color.\n        ///   - width: Border width.\n        ///   - unit: Unit of the width.\n        public init(color: NSColor, width: CGFloat = 1, unit: Unit = .points) {\n            self.color = color\n            self.width = width.converted(to: unit)\n        }\n#endif\n\n        public var description: String {\n            \"Border(color: \\(color.hex), width: \\(width) pixels)\"\n        }\n    }\n\n    /// An option for how to resize the image.\n    @frozen public enum ContentMode: CustomStringConvertible, Sendable {\n        /// Scales the image so that it completely fills the target area.\n        /// Maintains the aspect ratio of the original image.\n        case aspectFill\n\n        /// Scales the image so that it fits the target size. Maintains the\n        /// aspect ratio of the original image.\n        case aspectFit\n\n        public var description: String {\n            switch self {\n            case .aspectFill: return \".aspectFill\"\n            case .aspectFit: return \".aspectFit\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessors+Anonymous.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageProcessors {\n    /// Processes an image using a specified closure.\n    public struct Anonymous: ImageProcessing, CustomStringConvertible {\n        public let identifier: String\n        private let closure: @Sendable (PlatformImage) -> PlatformImage?\n\n        public init(id: String, _ closure: @Sendable @escaping (PlatformImage) -> PlatformImage?) {\n            self.identifier = id\n            self.closure = closure\n        }\n\n        public func process(_ image: PlatformImage) -> PlatformImage? {\n            closure(image)\n        }\n\n        public var description: String {\n            \"AnonymousProcessor(identifier: \\(identifier)\"\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessors+Circle.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageProcessors {\n\n    /// Rounds the corners of an image into a circle. If the image is not a square,\n    /// crops it to a square first.\n    public struct Circle: ImageProcessing, Hashable, CustomStringConvertible {\n        private let border: ImageProcessingOptions.Border?\n\n        /// - parameter border: `nil` by default.\n        public init(border: ImageProcessingOptions.Border? = nil) {\n            self.border = border\n        }\n\n        public func process(_ image: PlatformImage) -> PlatformImage? {\n            image.processed.byDrawingInCircle(border: border)\n        }\n\n        public var identifier: String {\n            let suffix = border.map { \"?border=\\($0)\" }\n            return \"com.github.kean/nuke/circle\" + (suffix ?? \"\")\n        }\n\n        public var description: String {\n            \"Circle(border: \\(border?.description ?? \"nil\"))\"\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessors+Composition.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageProcessors {\n    /// Composes multiple processors.\n    public struct Composition: ImageProcessing, Hashable, CustomStringConvertible {\n        let processors: [any ImageProcessing]\n\n        /// Composes multiple processors.\n        public init(_ processors: [any ImageProcessing]) {\n            // note: multiple compositions are not flatten by default.\n            self.processors = processors\n        }\n\n        /// Processes the given image by applying each processor in an order in\n        /// which they were added. If one of the processors fails to produce\n        /// an image the processing stops and `nil` is returned.\n        public func process(_ image: PlatformImage) -> PlatformImage? {\n            processors.reduce(image) { image, processor in\n                autoreleasepool {\n                    image.flatMap(processor.process)\n                }\n            }\n        }\n\n        /// Processes the given image by applying each processor in an order in\n        /// which they were added. If one of the processors fails to produce\n        /// an image the processing stops and an error is thrown.\n        public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer {\n            try processors.reduce(container) { container, processor in\n                try autoreleasepool {\n                    try processor.process(container, context: context)\n                }\n            }\n        }\n\n        /// Returns combined identifier of all the underlying processors.\n        public var identifier: String {\n            processors.map({ $0.identifier }).joined()\n        }\n\n        /// Creates a combined hash of all the given processors.\n        public func hash(into hasher: inout Hasher) {\n            for processor in processors {\n                hasher.combine(processor.hashableIdentifier)\n            }\n        }\n\n        /// Compares all the underlying processors for equality.\n        public static func == (lhs: Composition, rhs: Composition) -> Bool {\n            lhs.processors == rhs.processors\n        }\n\n        public var description: String {\n            \"Composition(processors: \\(processors))\"\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessors+CoreImage.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\nimport Foundation\nimport CoreImage\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageProcessors {\n\n    /// Applies Core Image filter (`CIFilter`) to the image.\n    ///\n    /// # Performance Considerations.\n    ///\n    /// Prefer chaining multiple `CIFilter` objects using `Core Image` facilities\n    /// instead of using multiple instances of `ImageProcessors.CoreImageFilter`.\n    ///\n    /// # References\n    ///\n    /// - [Core Image Programming Guide](https://developer.apple.com/library/ios/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html)\n    /// - [Core Image Filter Reference](https://developer.apple.com/library/prerelease/ios/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html)\n    public struct CoreImageFilter: ImageProcessing, CustomStringConvertible, @unchecked Sendable {\n        let filter: Filter\n        public let identifier: String\n\n        enum Filter {\n            case named(String, parameters: [String: Any])\n            case custom(CIFilter)\n        }\n\n        /// Initializes the processor with a name of the `CIFilter` and its parameters.\n        ///\n        /// - parameter identifier: Uniquely identifies the processor.\n        public init(name: String, parameters: [String: Any], identifier: String) {\n            self.filter = .named(name, parameters: parameters)\n            self.identifier = identifier\n        }\n\n        /// Initializes the processor with a name of the `CIFilter`.\n        public init(name: String) {\n            self.filter = .named(name, parameters: [:])\n            self.identifier = \"com.github.kean/nuke/core_image?name=\\(name))\"\n        }\n\n        /// Initializes the processor with the given `CIFilter`.\n        ///\n        /// - parameter identifier: Uniquely identifies the processor.\n        public init(_ filter: CIFilter, identifier: String) {\n            self.filter = .custom(filter)\n            self.identifier = identifier\n        }\n\n        public func process(_ image: PlatformImage) -> PlatformImage? {\n            try? _process(image)\n        }\n\n        public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer {\n            try container.map(_process)\n        }\n\n        private func _process(_ image: PlatformImage) throws -> PlatformImage {\n            switch filter {\n            case let .named(name, parameters):\n                return try CoreImageFilter.applyFilter(named: name, parameters: parameters, to: image)\n            case .custom(let filter):\n                return try CoreImageFilter.apply(filter: filter, to: image)\n            }\n        }\n\n        // MARK: - Apply Filter\n\n        /// A default context shared between all Core Image filters. The context\n        /// has `.priorityRequestLow` option set to `true`.\n        public static var context: CIContext {\n            get { _context.value }\n            set { _context.value = newValue }\n        }\n\n        private static let _context = Mutex(value: CIContext(options: [.priorityRequestLow: true]))\n\n        static func applyFilter(named name: String, parameters: [String: Any] = [:], to image: PlatformImage) throws -> PlatformImage {\n            guard let filter = CIFilter(name: name, parameters: parameters) else {\n                throw Error.failedToCreateFilter(name: name, parameters: parameters)\n            }\n            return try CoreImageFilter.apply(filter: filter, to: image)\n        }\n\n        /// Applies filter to the given image.\n        public static func apply(filter: CIFilter, to image: PlatformImage) throws -> PlatformImage {\n            func getCIImage() throws -> CoreImage.CIImage {\n                if let image = image.ciImage {\n                    return image\n                }\n                if let image = image.cgImage {\n                    return CoreImage.CIImage(cgImage: image)\n                }\n                throw Error.inputImageIsEmpty(inputImage: image)\n            }\n            filter.setValue(try getCIImage(), forKey: kCIInputImageKey)\n            guard let outputImage = filter.outputImage else {\n                throw Error.failedToApplyFilter(filter: filter)\n            }\n            guard let imageRef = context.createCGImage(outputImage, from: outputImage.extent) else {\n                throw Error.failedToCreateOutputCGImage(image: outputImage)\n            }\n            return PlatformImage.make(cgImage: imageRef, source: image)\n        }\n\n        public var description: String {\n            switch filter {\n            case let .named(name, parameters):\n                return \"CoreImageFilter(name: \\(name), parameters: \\(parameters))\"\n            case .custom(let filter):\n                return \"CoreImageFilter(filter: \\(filter))\"\n            }\n        }\n\n        public enum Error: Swift.Error, CustomStringConvertible, @unchecked Sendable {\n            case failedToCreateFilter(name: String, parameters: [String: Any])\n            case inputImageIsEmpty(inputImage: PlatformImage)\n            case failedToApplyFilter(filter: CIFilter)\n            case failedToCreateOutputCGImage(image: CIImage)\n\n            public var description: String {\n                switch self {\n                case let .failedToCreateFilter(name, parameters):\n                    return \"Failed to create filter named \\(name) with parameters: \\(parameters)\"\n                case let .inputImageIsEmpty(inputImage):\n                    return \"Failed to create input CIImage for \\(inputImage)\"\n                case let .failedToApplyFilter(filter):\n                    return \"Failed to apply filter: \\(filter.name)\"\n                case let .failedToCreateOutputCGImage(image):\n                    return \"Failed to create output image for extent: \\(image.extent) from \\(image)\"\n                }\n            }\n        }\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessors+GaussianBlur.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\nimport Foundation\nimport Accelerate\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageProcessors {\n    /// Blurs an image using a simulated Gaussian blur.\n    ///\n    /// Uses the Accelerate framework (`vImageBoxConvolve`) with edge extension\n    /// to avoid gray border artifacts.\n    public struct GaussianBlur: ImageProcessing, Hashable, CustomStringConvertible {\n        private let radius: Int\n\n        /// Initializes the receiver with a blur radius.\n        ///\n        /// - parameter radius: `8` by default.\n        public init(radius: Int = 8) {\n            self.radius = radius\n        }\n\n        /// Applies a Gaussian blur to the image.\n        public func process(_ image: PlatformImage) -> PlatformImage? {\n            guard let cgImage = image.cgImage else { return nil }\n            guard let output = cgImage.blurred(radius: radius) else { return nil }\n            return PlatformImage.make(cgImage: output, source: image)\n        }\n\n        public var identifier: String {\n            \"com.github.kean/nuke/gaussian_blur?radius=\\(radius)\"\n        }\n\n        public var description: String {\n            \"GaussianBlur(radius: \\(radius))\"\n        }\n    }\n}\n\nprivate extension CGImage {\n    /// Applies a Gaussian blur approximation using three box-blur passes (SVG spec).\n    func blurred(radius: Int) -> CGImage? {\n        let inputRadius = max(Double(radius), 2.0)\n        let pi2 = 2.0 * Double.pi\n        var kernelSize = UInt32(floor(inputRadius * 3.0 * sqrt(pi2) / 4.0 + 0.5))\n        if kernelSize % 2 == 0 { kernelSize += 1 }\n\n        let size = self.size\n        guard let inputCtx = CGContext.make(self, size: size, alphaInfo: .premultipliedLast),\n              let outputCtx = CGContext.make(self, size: size, alphaInfo: .premultipliedLast) else {\n            return nil\n        }\n        inputCtx.draw(self, in: CGRect(origin: .zero, size: size))\n\n        var inBuffer = vImage_Buffer(data: inputCtx.data, height: vImagePixelCount(inputCtx.height), width: vImagePixelCount(inputCtx.width), rowBytes: inputCtx.bytesPerRow)\n        var outBuffer = vImage_Buffer(data: outputCtx.data, height: vImagePixelCount(outputCtx.height), width: vImagePixelCount(outputCtx.width), rowBytes: outputCtx.bytesPerRow)\n\n        // Three box-blur passes approximate a Gaussian blur. kvImageEdgeExtend\n        // extends edge pixels to prevent border artifacts (see #308).\n        let flags = vImage_Flags(kvImageEdgeExtend)\n        vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, nil, 0, 0, kernelSize, kernelSize, nil, flags)\n        vImageBoxConvolve_ARGB8888(&outBuffer, &inBuffer, nil, 0, 0, kernelSize, kernelSize, nil, flags)\n        vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, nil, 0, 0, kernelSize, kernelSize, nil, flags)\n\n        return outputCtx.makeImage()\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessors+Resize.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport CoreGraphics\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageProcessors {\n    /// Scales an image to a specified size.\n    public struct Resize: ImageProcessing, Hashable, CustomStringConvertible {\n        private let size: ImageTargetSize\n        private let contentMode: ImageProcessingOptions.ContentMode\n        private let crop: Bool\n        private let upscale: Bool\n\n\n        /// Initializes the processor with the given size.\n        ///\n        /// - parameters:\n        ///   - size: The target size.\n        ///   - unit: Unit of the target size.\n        ///   - contentMode: A target content mode.\n        ///   - crop: If `true`, crops the image to exactly match the target size.\n        ///   Has no effect when `contentMode` is `.aspectFill`.\n        ///   - upscale: By default, upscaling is not allowed.\n        public init(size: CGSize, unit: ImageProcessingOptions.Unit = .points, contentMode: ImageProcessingOptions.ContentMode = .aspectFill, crop: Bool = false, upscale: Bool = false) {\n            self.size = ImageTargetSize(size: size, unit: unit)\n            self.contentMode = contentMode\n            self.crop = crop\n            self.upscale = upscale\n        }\n\n        /// Scales an image to the given width preserving aspect ratio.\n        ///\n        /// - parameters:\n        ///   - width: The target width.\n        ///   - unit: Unit of the target size.\n        ///   - upscale: `false` by default.\n        public init(width: CGFloat, unit: ImageProcessingOptions.Unit = .points, upscale: Bool = false) {\n            self.init(size: CGSize(width: width, height: 9999), unit: unit, contentMode: .aspectFit, crop: false, upscale: upscale)\n        }\n\n        /// Scales an image to the given height preserving aspect ratio.\n        ///\n        /// - parameters:\n        ///   - height: The target height.\n        ///   - unit: Unit of the target size.\n        ///   - upscale: By default, upscaling is not allowed.\n        public init(height: CGFloat, unit: ImageProcessingOptions.Unit = .points, upscale: Bool = false) {\n            self.init(size: CGSize(width: 9999, height: height), unit: unit, contentMode: .aspectFit, crop: false, upscale: upscale)\n        }\n\n        public func process(_ image: PlatformImage) -> PlatformImage? {\n            if crop && contentMode == .aspectFill {\n                return image.processed.byResizingAndCropping(to: size.cgSize)\n            }\n            return image.processed.byResizing(to: size.cgSize, contentMode: contentMode, upscale: upscale)\n        }\n\n        public var identifier: String {\n            \"com.github.kean/nuke/resize?s=\\(size.cgSize),cm=\\(contentMode),crop=\\(crop),upscale=\\(upscale)\"\n        }\n\n        public var description: String {\n            \"Resize(size: \\(size.cgSize) pixels, contentMode: \\(contentMode), crop: \\(crop), upscale: \\(upscale))\"\n        }\n    }\n}\n\n// Adds Hashable without making changes to public CGSize API. It uses `Float`\n// to reduce memory size.\nstruct ImageTargetSize: Hashable {\n    let width: Float\n    let height: Float\n\n    var cgSize: CGSize { CGSize(width: Double(width), height: Double(height)) }\n\n    init(maxPixelSize: Float) {\n        (width, height) = (maxPixelSize, 0)\n    }\n\n    /// Creates the size in pixels by scaling to the input size to the screen scale\n    /// if needed.\n    init(size: CGSize, unit: ImageProcessingOptions.Unit) {\n        switch unit {\n        case .pixels:\n            (width, height) = (Float(size.width), Float(size.height))\n        case .points:\n            let scaled = size.scaled(by: Screen.scale)\n            (width, height) = (Float(scaled.width), Float(scaled.height))\n        }\n    }\n\n    func hash(into hasher: inout Hasher) {\n        hasher.combine(width)\n        hasher.combine(height)\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessors+RoundedCorners.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport CoreGraphics\n\n#if !os(macOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nextension ImageProcessors {\n    /// Rounds the corners of an image to the specified radius.\n    ///\n    /// - important: In order for the corners to be displayed correctly, the image must exactly match the size\n    /// of the image view in which it will be displayed. See ``ImageProcessors/Resize`` for more info.\n    public struct RoundedCorners: ImageProcessing, Hashable, CustomStringConvertible {\n        private let radius: CGFloat\n        private let border: ImageProcessingOptions.Border?\n\n        /// Initializes the processor with the given radius.\n        ///\n        /// - parameters:\n        ///   - radius: The radius of the corners.\n        ///   - unit: Unit of the radius.\n        ///   - border: An optional border drawn around the image.\n        public init(radius: CGFloat, unit: ImageProcessingOptions.Unit = .points, border: ImageProcessingOptions.Border? = nil) {\n            self.radius = radius.converted(to: unit)\n            self.border = border\n        }\n\n        public func process(_ image: PlatformImage) -> PlatformImage? {\n            image.processed.byAddingRoundedCorners(radius: radius, border: border)\n        }\n\n        public var identifier: String {\n            let suffix = border.map { \",border=\\($0)\" }\n            return \"com.github.kean/nuke/rounded_corners?radius=\\(radius)\" + (suffix ?? \"\")\n        }\n\n        public var description: String {\n            \"RoundedCorners(radius: \\(radius) pixels, border: \\(border?.description ?? \"nil\"))\"\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Processing/ImageProcessors.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\n/// A namespace for all processors that implement ``ImageProcessing`` protocol.\npublic enum ImageProcessors {}\n\nextension ImageProcessing where Self == ImageProcessors.Resize {\n    /// Scales an image to a specified size.\n    ///\n    /// - parameters:\n    ///   - size: The target size.\n    ///   - unit: Unit of the target size. By default, `.points`.\n    ///   - contentMode: Target content mode.\n    ///   - crop: If `true` will crop the image to match the target size. Does\n    ///   nothing with content mode .aspectFill. `false` by default.\n    ///   - upscale: Upscaling is not allowed by default.\n    public static func resize(size: CGSize, unit: ImageProcessingOptions.Unit = .points, contentMode: ImageProcessingOptions.ContentMode = .aspectFill, crop: Bool = false, upscale: Bool = false) -> ImageProcessors.Resize {\n        ImageProcessors.Resize(size: size, unit: unit, contentMode: contentMode, crop: crop, upscale: upscale)\n    }\n\n    /// Scales an image to the given width preserving aspect ratio.\n    ///\n    /// - parameters:\n    ///   - width: The target width.\n    ///   - unit: Unit of the target size. By default, `.points`.\n    ///   - upscale: `false` by default.\n    public static func resize(width: CGFloat, unit: ImageProcessingOptions.Unit = .points, upscale: Bool = false) -> ImageProcessors.Resize {\n        ImageProcessors.Resize(width: width, unit: unit, upscale: upscale)\n    }\n\n    /// Scales an image to the given height preserving aspect ratio.\n    ///\n    /// - parameters:\n    ///   - height: The target height.\n    ///   - unit: Unit of the target size. By default, `.points`.\n    ///   - upscale: `false` by default.\n    public static func resize(height: CGFloat, unit: ImageProcessingOptions.Unit = .points, upscale: Bool = false) -> ImageProcessors.Resize {\n        ImageProcessors.Resize(height: height, unit: unit, upscale: upscale)\n    }\n}\n\nextension ImageProcessing where Self == ImageProcessors.Circle {\n    /// Rounds the corners of an image into a circle. If the image is not a square,\n    /// crops it to a square first.\n    ///\n    /// - parameter border: `nil` by default.\n    public static func circle(border: ImageProcessingOptions.Border? = nil) -> ImageProcessors.Circle {\n        ImageProcessors.Circle(border: border)\n    }\n}\n\nextension ImageProcessing where Self == ImageProcessors.RoundedCorners {\n    /// Rounds the corners of an image to the specified radius.\n    ///\n    /// - parameters:\n    ///   - radius: The radius of the corners.\n    ///   - unit: Unit of the radius.\n    ///   - border: An optional border drawn around the image.\n    ///\n    /// - important: In order for the corners to be displayed correctly, the image must exactly match the size\n    /// of the image view in which it will be displayed. See ``ImageProcessors/Resize`` for more info.\n    public static func roundedCorners(radius: CGFloat, unit: ImageProcessingOptions.Unit = .points, border: ImageProcessingOptions.Border? = nil) -> ImageProcessors.RoundedCorners {\n        ImageProcessors.RoundedCorners(radius: radius, unit: unit, border: border)\n    }\n}\n\nextension ImageProcessing where Self == ImageProcessors.Anonymous {\n    /// Creates a custom processor with a given closure.\n    ///\n    /// - parameters:\n    ///   - id: Uniquely identifies the operation performed by the processor.\n    ///   - closure: A closure that transforms the images.\n    public static func process(id: String, _ closure: @Sendable @escaping (PlatformImage) -> PlatformImage?) -> ImageProcessors.Anonymous {\n        ImageProcessors.Anonymous(id: id, closure)\n    }\n}\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\nextension ImageProcessing where Self == ImageProcessors.CoreImageFilter {\n    /// Applies Core Image filter – `CIFilter` – to the image.\n    ///\n    /// - parameter identifier: Uniquely identifies the processor.\n    public static func coreImageFilter(name: String, parameters: [String: Any], identifier: String) -> ImageProcessors.CoreImageFilter {\n        ImageProcessors.CoreImageFilter(name: name, parameters: parameters, identifier: identifier)\n    }\n\n    /// Applies Core Image filter – `CIFilter` – to the image.\n    ///\n    public static func coreImageFilter(name: String) -> ImageProcessors.CoreImageFilter {\n        ImageProcessors.CoreImageFilter(name: name)\n    }\n\n    public static func coreImageFilter(_ filter: CIFilter, identifier: String) -> ImageProcessors.CoreImageFilter {\n        ImageProcessors.CoreImageFilter(filter, identifier: identifier)\n    }\n}\n\nextension ImageProcessing where Self == ImageProcessors.GaussianBlur {\n    /// Blurs an image using `CIGaussianBlur` filter.\n    ///\n    /// - parameter radius: `8` by default.\n    public static func gaussianBlur(radius: Int = 8) -> ImageProcessors.GaussianBlur {\n        ImageProcessors.GaussianBlur(radius: radius)\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/Nuke/Tasks/AsyncPipelineTask.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n// Each task holds a strong reference to the pipeline. This is by design. The\n// user does not need to hold a strong reference to the pipeline.\nclass AsyncPipelineTask<Value: Sendable>: AsyncTask<Value, ImagePipeline.Error> {\n    let pipeline: ImagePipeline\n    // A canonical request representing the unit work performed by the task.\n    let request: ImageRequest\n\n    init(_ pipeline: ImagePipeline, _ request: ImageRequest) {\n        self.pipeline = pipeline\n        self.request = request\n    }\n}\n\n// Returns all image tasks subscribed to the current pipeline task.\n// A suboptimal approach just to make the new DiskCachPolicy.automatic work.\n@ImagePipelineActor\nprotocol ImageTaskSubscribers {\n    var imageTasks: [ImageTask] { get }\n}\n\nextension ImageTask: ImageTaskSubscribers {\n    var imageTasks: [ImageTask] {\n        [self]\n    }\n}\n\nextension AsyncPipelineTask: ImageTaskSubscribers {\n    var imageTasks: [ImageTask] {\n        subscribers.flatMap { subscribers -> [ImageTask] in\n            (subscribers as? ImageTaskSubscribers)?.imageTasks ?? []\n        }\n    }\n}\n\nextension AsyncPipelineTask {\n    /// Decodes the data on the dedicated queue and calls the completion\n    /// on the pipeline's internal queue.\n    func decode(_ context: ImageDecodingContext, decoder: any ImageDecoding, _ completion: @escaping @ImagePipelineActor (Result<ImageResponse, ImagePipeline.Error>) -> Void) {\n        @Sendable func decode() -> Result<ImageResponse, ImagePipeline.Error> {\n            signpost(context.isCompleted ? \"DecodeImageData\" : \"DecodeProgressiveImageData\") {\n                Result { try decoder.decode(context) }\n                    .mapError { .decodingFailed(decoder: decoder, context: context, error: $0) }\n            }\n        }\n        guard decoder.isAsynchronous else {\n            return completion(decode())\n        }\n        operation = pipeline.configuration.imageDecodingQueue.add {\n            completion(await performInBackground(decode))\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Tasks/AsyncTask.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Represents a task with support for multiple observers, cancellation,\n/// progress reporting, dependencies – everything that `ImagePipeline` needs.\n///\n/// A `AsyncTask` can have zero or more subscriptions (`TaskSubscription`) which can\n/// be used to later unsubscribe or change the priority of the subscription.\n///\n/// The task has built-in support for operations (`Foundation.Operation`) – it\n/// automatically cancels them, updates the priority, etc. Most steps in the\n/// image pipeline are represented using Operation to take advantage of these features.\n@ImagePipelineActor\nclass AsyncTask<Value: Sendable, Error: Sendable>: AsyncTaskSubscriptionDelegate {\n\n    private struct Subscription {\n        let closure: (Event) -> Void\n        weak var subscriber: AnyObject?\n        var priority: TaskPriority\n    }\n\n    // In most situations, especially for intermediate tasks, the almost almost\n    // only one subscription.\n    private var inlineSubscription: Subscription?\n    private var subscriptions: [TaskSubscriptionKey: Subscription]? // Create lazily\n    private var nextSubscriptionKey = 0\n\n    var subscribers: [AnyObject] {\n        var output = [AnyObject?]()\n        output.append(inlineSubscription?.subscriber)\n        subscriptions?.values.forEach { output.append($0.subscriber) }\n        return output.compactMap { $0 }\n    }\n\n    /// Returns `true` if the task was either cancelled, or was completed.\n    private(set) var isDisposed = false\n    private var isStarted = false\n\n    /// Gets called when the task is either cancelled, or was completed.\n    var onDisposed: (@ImagePipelineActor @Sendable() -> Void)?\n\n    var onCancelled: (@ImagePipelineActor @Sendable () -> Void)?\n\n    var priority: TaskPriority = .normal {\n        didSet {\n            guard oldValue != priority else { return }\n            operation?.priority = priority\n            dependency?.setPriority(priority)\n        }\n    }\n\n    /// A task might have a dependency. The task automatically unsubscribes\n    /// from the dependency when it gets cancelled, and also updates the\n    /// priority of the subscription to the dependency when its own\n    /// priority is updated.\n    var dependency: TaskSubscription? {\n        didSet {\n            dependency?.setPriority(priority)\n        }\n    }\n\n    var operation: TaskQueue.Operation? {\n        didSet {\n            guard priority != .normal else { return }\n            operation?.priority = priority\n        }\n    }\n\n    /// Publishes the results of the task.\n    var publisher: Publisher { Publisher(task: self) }\n\n    /// Override this to start image task. Only gets called once.\n    func start() {}\n\n    // MARK: - Managing Observers\n\n    /// - note: Returns `nil` if the task was disposed.\n    private func subscribe(priority: TaskPriority = .normal, subscriber: AnyObject, _ closure: @escaping (Event) -> Void) -> TaskSubscription? {\n        guard !isDisposed else { return nil }\n\n        let subscriptionKey = nextSubscriptionKey\n        nextSubscriptionKey += 1\n        let subscription = TaskSubscription(task: self, key: subscriptionKey)\n\n        if subscriptionKey == 0 {\n            inlineSubscription = Subscription(closure: closure, subscriber: subscriber, priority: priority)\n        } else {\n            if subscriptions == nil { subscriptions = [:] }\n            subscriptions![subscriptionKey] = Subscription(closure: closure, subscriber: subscriber, priority: priority)\n        }\n\n        updatePriority(suggestedPriority: priority)\n\n        if !isStarted {\n            isStarted = true\n            start()\n        }\n\n        // The task may have been completed synchronously by `starter`.\n        guard !isDisposed else { return nil }\n        return subscription\n    }\n\n    // MARK: - TaskSubscriptionDelegate\n\n    fileprivate func setPriority(_ priority: TaskPriority, for key: TaskSubscriptionKey) {\n        guard !isDisposed else { return }\n\n        if key == 0 {\n            inlineSubscription?.priority = priority\n        } else {\n            subscriptions![key]?.priority = priority\n        }\n        updatePriority(suggestedPriority: priority)\n    }\n\n    fileprivate func unsubsribe(key: TaskSubscriptionKey) {\n        if key == 0 {\n            guard inlineSubscription != nil else { return }\n            inlineSubscription = nil\n        } else {\n            guard subscriptions!.removeValue(forKey: key) != nil else { return }\n        }\n\n        guard !isDisposed else { return }\n\n        if inlineSubscription == nil && subscriptions?.isEmpty ?? true {\n            terminate(reason: .cancelled)\n        } else {\n            updatePriority(suggestedPriority: nil)\n        }\n    }\n\n    // MARK: - Sending Events\n\n    func send(value: Value, isCompleted: Bool = false) {\n        send(event: .value(value, isCompleted: isCompleted))\n    }\n\n    func send(error: Error) {\n        send(event: .error(error))\n    }\n\n    func send(progress: TaskProgress) {\n        send(event: .progress(progress))\n    }\n\n    private func send(event: Event) {\n        guard !isDisposed else { return }\n\n        switch event {\n        case let .value(_, isCompleted):\n            if isCompleted {\n                terminate(reason: .finished)\n            }\n        case .progress:\n            break // Simply send the event\n        case .error:\n            terminate(reason: .finished)\n        }\n\n        inlineSubscription?.closure(event)\n        if let subscriptions {\n            for subscription in subscriptions.values {\n                subscription.closure(event)\n            }\n        }\n    }\n\n    // MARK: - Termination\n\n    private enum TerminationReason {\n        case finished, cancelled\n    }\n\n    private func terminate(reason: TerminationReason) {\n        guard !isDisposed else { return }\n        isDisposed = true\n\n        if reason == .cancelled {\n            operation?.cancel()\n            dependency?.unsubscribe()\n            onCancelled?()\n        }\n        onDisposed?()\n    }\n\n    // MARK: - Priority\n\n    private func updatePriority(suggestedPriority: TaskPriority?) {\n        if let suggestedPriority, suggestedPriority >= priority {\n            // No need to recompute, won't go higher than that\n            priority = suggestedPriority\n            return\n        }\n\n        var newPriority = inlineSubscription?.priority\n        // Same as subscriptions.map { $0?.priority }.max() but without allocating\n        // any memory for redundant arrays\n        if let subscriptions {\n            for subscription in subscriptions.values {\n                if newPriority == nil {\n                    newPriority = subscription.priority\n                } else if subscription.priority > newPriority! {\n                    newPriority = subscription.priority\n                }\n            }\n        }\n        self.priority = newPriority ?? .normal\n    }\n}\n\n// MARK: - AsyncTask (Publisher)\n\nextension AsyncTask {\n    /// Publishes the results of the task.\n    @ImagePipelineActor struct Publisher {\n        fileprivate let task: AsyncTask\n\n        /// Attaches the subscriber to the task.\n        /// - note: Returns `nil` if the task is already disposed.\n        func subscribe(priority: TaskPriority = .normal, subscriber: AnyObject, _ closure: @escaping (Event) -> Void) -> TaskSubscription? {\n            task.subscribe(priority: priority, subscriber: subscriber, closure)\n        }\n\n        /// Attaches the subscriber to the task. Automatically forwards progress\n        /// and error events to the given task.\n        /// - note: Returns `nil` if the task is already disposed.\n        func subscribe<NewValue>(_ task: AsyncTask<NewValue, Error>, onValue: @escaping (Value, Bool) -> Void) -> TaskSubscription? {\n            subscribe(priority: task.priority, subscriber: task) { [weak task] event in\n                guard let task else { return }\n                switch event {\n                case let .value(value, isCompleted):\n                    onValue(value, isCompleted)\n                case let .progress(progress):\n                    task.send(progress: progress)\n                case let .error(error):\n                    task.send(error: error)\n                }\n            }\n        }\n    }\n}\n\ntypealias TaskProgress = ImageTask.Progress // Using typealias for simplicity\n\nenum TaskPriority: Int, Comparable, CaseIterable {\n    case veryLow = 0, low, normal, high, veryHigh\n\n    static func < (lhs: TaskPriority, rhs: TaskPriority) -> Bool {\n        lhs.rawValue < rhs.rawValue\n    }\n}\n\n// MARK: - AsyncTask.Event {\nextension AsyncTask {\n    enum Event {\n        case value(Value, isCompleted: Bool)\n        case progress(TaskProgress)\n        case error(Error)\n    }\n}\n\nextension AsyncTask.Event: Equatable where Value: Equatable, Error: Equatable {}\n\n// MARK: - TaskSubscription\n\n/// Represents a subscription to a task. The observer must retain a strong\n/// reference to a subscription.\n@ImagePipelineActor\nstruct TaskSubscription: Sendable {\n    private let task: any AsyncTaskSubscriptionDelegate\n    private let key: TaskSubscriptionKey\n\n    fileprivate init(task: any AsyncTaskSubscriptionDelegate, key: TaskSubscriptionKey) {\n        self.task = task\n        self.key = key\n    }\n\n    /// Removes the subscription from the task. The observer won't receive any\n    /// more events from the task.\n    ///\n    /// If there are no more subscriptions attached to the task, the task gets\n    /// cancelled along with its dependencies. The cancelled task is\n    /// marked as disposed.\n    func unsubscribe() {\n        task.unsubsribe(key: key)\n    }\n\n    /// Updates the priority of the subscription. The priority of the task is\n    /// calculated as the maximum priority out of all of its subscription. When\n    /// the priority of the task is updated, the priority of a dependency also is.\n    ///\n    /// - note: The priority also automatically gets updated when the subscription\n    /// is removed from the task.\n    func setPriority(_ priority: TaskPriority) {\n        task.setPriority(priority, for: key)\n    }\n}\n\n@ImagePipelineActor\nprivate protocol AsyncTaskSubscriptionDelegate: AnyObject, Sendable {\n    func unsubsribe(key: TaskSubscriptionKey)\n    func setPriority(_ priority: TaskPriority, for observer: TaskSubscriptionKey)\n}\n\nprivate typealias TaskSubscriptionKey = Int\n\n// MARK: - TaskPool\n\n/// Contains the tasks which haven't completed yet.\n@ImagePipelineActor\nfinal class TaskPool<Key: Hashable, Value: Sendable, Error: Sendable> {\n    private let isCoalescingEnabled: Bool\n    private var map = [Key: AsyncTask<Value, Error>]()\n\n    nonisolated init(_ isCoalescingEnabled: Bool) {\n        self.isCoalescingEnabled = isCoalescingEnabled\n    }\n\n    /// Creates a task with the given key. If there is an outstanding task with\n    /// the given key in the pool, the existing task is returned. Tasks are\n    /// automatically removed from the pool when they are disposed.\n    func publisherForKey(_ key: @autoclosure () -> Key, _ make: () -> AsyncTask<Value, Error>) -> AsyncTask<Value, Error>.Publisher {\n        guard isCoalescingEnabled else {\n            return make().publisher\n        }\n        let key = key()\n        if let task = map[key] {\n            return task.publisher\n        }\n        let task = make()\n        map[key] = task\n        task.onDisposed = { [weak self] in\n            self?.map[key] = nil\n        }\n        return task.publisher\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Tasks/TaskFetchOriginalData.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Fetches original image from the data loader (`DataLoading`) and stores it\n/// in the disk cache (`DataCaching`).\nfinal class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unchecked Sendable {\n    private var urlResponse: URLResponse?\n    private var resumableData: ResumableData?\n    private var resumedDataCount: Int64 = 0\n    private var data = Data()\n\n    override func start() {\n        if case .data(let closure) = request.resource {\n            loadAsyncData(closure)\n            return\n        }\n\n        guard let urlRequest = request.urlRequest, let url = urlRequest.url else {\n            // A malformed URL prevented a URL request from being initiated.\n            send(error: .dataLoadingFailed(error: URLError(.badURL)))\n            return\n        }\n\n        if url.isLocalResource && pipeline.configuration.isLocalResourcesSupportEnabled {\n            do {\n                let data = try Data(contentsOf: url)\n                send(value: (data, nil), isCompleted: true)\n            } catch {\n                send(error: .dataLoadingFailed(error: error))\n            }\n            return\n        }\n\n        if let rateLimiter = pipeline.rateLimiter {\n            // Rate limiter is synchronized on pipeline's queue. Delayed work is\n            // executed asynchronously also on the same queue.\n            rateLimiter.execute { [weak self] in\n                guard let self, !self.isDisposed else {\n                    return false\n                }\n                self.loadData(urlRequest: urlRequest)\n                return true\n            }\n        } else { // Start loading immediately.\n            loadData(urlRequest: urlRequest)\n        }\n    }\n\n    private func loadData(urlRequest: URLRequest) {\n        if request.options.contains(.skipDataLoadingQueue) {\n            Task { @ImagePipelineActor in\n                await self.performDataLoad(urlRequest: urlRequest)\n            }\n        } else {\n            // Wrap data request in an operation to limit the maximum number of\n            // concurrent data tasks.\n            operation = pipeline.configuration.dataLoadingQueue.add { [weak self] in\n                guard let self else { return }\n                await self.performDataLoad(urlRequest: urlRequest)\n            }\n        }\n    }\n\n    private func performDataLoad(urlRequest: URLRequest) async {\n        guard !isDisposed else { return }\n\n        // Read and remove resumable data from cache (we're going to insert it\n        // back in the cache if the request fails to complete again).\n        var urlRequest = urlRequest\n        if pipeline.configuration.isResumableDataEnabled,\n           let resumableData = ResumableDataStorage.shared.removeResumableData(for: request, pipeline: pipeline) {\n            // Update headers to add \"Range\" and \"If-Range\" headers\n            resumableData.resume(request: &urlRequest)\n            // Save resumable data to be used later (before using it, the pipeline\n            // verifies that the server returns \"206 Partial Content\")\n            self.resumableData = resumableData\n        }\n\n        signpost(self, \"LoadImageData\", .begin, \"URL: \\(urlRequest.url?.absoluteString ?? \"\"), resumable data: \\(Formatter.bytes(resumableData?.data.count ?? 0))\")\n\n        onCancelled = { [weak self] in\n            guard let self else { return }\n            signpost(self, \"LoadImageData\", .end, \"Cancelled\")\n            self.tryToSaveResumableData()\n        }\n\n        let dataLoader = pipeline.delegate.dataLoader(for: request, pipeline: pipeline)\n\n        do {\n            urlRequest = try await pipeline.delegate.willLoadData(for: request, urlRequest: urlRequest, pipeline: pipeline)\n            let (stream, response) = try await dataLoader.loadData(with: urlRequest)\n\n            guard !isDisposed else { return }\n\n            try dataTask(didReceiveResponse: response)\n\n            for try await chunk in stream {\n                guard !isDisposed else { return }\n                try dataTask(didReceiveData: chunk, response: response)\n            }\n\n            signpost(self, \"LoadImageData\", .end, \"Finished with size \\(Formatter.bytes(self.data.count))\")\n            dataTaskDidFinish()\n        } catch {\n            signpost(self, \"LoadImageData\", .end, \"Failed\")\n            if let error = error as? ImagePipeline.Error {\n                dataTaskDidFinish(error: error)\n            } else {\n                dataTaskDidFinish(error: .dataLoadingFailed(error: error))\n            }\n        }\n    }\n\n    /// Processes the initial response. Returns `false` if the size limit is\n    /// exceeded early (based on expected content length).\n    private func dataTask(didReceiveResponse response: URLResponse) throws(ImagePipeline.Error) {\n        // See if the server confirmed that the resumable data can be used\n        if let resumableData, ResumableData.isResumedResponse(response) {\n            data = resumableData.data\n            resumedDataCount = Int64(resumableData.data.count)\n            let expectedSize = response.expectedContentLength + resumedDataCount\n            if expectedSize > 0, expectedSize <= Int.max {\n                data.reserveCapacity(Int(expectedSize))\n            }\n            signpost(self, \"LoadImageData\", .event, \"Resumed with data \\(Formatter.bytes(resumedDataCount))\")\n        }\n        resumableData = nil // Get rid of resumable data\n\n        // Check the expected size early to avoid a large `reserveCapacity`\n        // allocation when the server reports a content length above the limit.\n        if let maximumResponseDataSize = pipeline.configuration.maximumResponseDataSize {\n            let expectedSize = response.expectedContentLength + resumedDataCount\n            if expectedSize > 0, expectedSize > maximumResponseDataSize {\n                throw .dataDownloadExceededMaximumSize\n            }\n        }\n    }\n\n    /// Processes a data chunk. Returns `false` when the size limit is exceeded.\n    private func dataTask(didReceiveData chunk: Data, response: URLResponse) throws(ImagePipeline.Error) {\n        // Append data and save response\n        if data.isEmpty {\n            data = chunk\n            if response.expectedContentLength > chunk.count, response.expectedContentLength <= Int.max {\n                data.reserveCapacity(Int(response.expectedContentLength))\n            }\n        } else {\n            data.append(chunk)\n        }\n        urlResponse = response\n\n        if let maximumResponseDataSize = pipeline.configuration.maximumResponseDataSize, data.count > maximumResponseDataSize {\n            throw .dataDownloadExceededMaximumSize\n        }\n\n        let progress = TaskProgress(completed: Int64(data.count), total: response.expectedContentLength + resumedDataCount)\n        send(progress: progress)\n\n        // If the image hasn't been fully loaded yet, give decoder a chance\n        // to decode the data chunk. In case `expectedContentLength` is `0`,\n        // progressive decoding doesn't run.\n        guard data.count < response.expectedContentLength else { return }\n        send(value: (data, response))\n    }\n\n    private func dataTaskDidFinish(error: ImagePipeline.Error? = nil) {\n        guard !isDisposed else { return }\n\n        if let error {\n            tryToSaveResumableData()\n            send(error: error)\n            return\n        }\n\n        // Sanity check, should never happen in practice\n        guard !data.isEmpty else {\n            send(error: .dataIsEmpty)\n            return\n        }\n\n        // Store in data cache\n        storeDataInCacheIfNeeded(data)\n\n        send(value: (data, urlResponse), isCompleted: true)\n    }\n\n    // MARK: Async Data Loading\n\n    private func loadAsyncData(_ fetch: @Sendable @escaping () async throws -> Data) {\n        if request.options.contains(.skipDataLoadingQueue) {\n            Task { await self.performAsyncDataLoad(fetch) }\n        } else {\n            operation = pipeline.configuration.dataLoadingQueue.add { [weak self] in\n                await self?.performAsyncDataLoad(fetch)\n            }\n        }\n    }\n\n    private func performAsyncDataLoad(_ fetch: @Sendable @escaping () async throws -> Data) async {\n        guard !isDisposed else { return }\n        do {\n            let data = try await fetch()\n            asyncDataDidFinish(data)\n        } catch {\n            send(error: .dataLoadingFailed(error: error))\n        }\n    }\n\n    private func asyncDataDidFinish(_ data: Data) {\n        guard !data.isEmpty else {\n            send(error: .dataIsEmpty)\n            return\n        }\n        storeDataInCacheIfNeeded(data)\n        send(value: (data, nil), isCompleted: true)\n    }\n\n    private func asyncDataDidFail(_ error: Error) {\n        send(error: .dataLoadingFailed(error: error))\n    }\n\n    private func tryToSaveResumableData() {\n        // Try to save resumable data in case the task was cancelled\n        // (`URLError.cancelled`) or failed to complete with other error.\n        if pipeline.configuration.isResumableDataEnabled,\n           let response = urlResponse, !data.isEmpty,\n           let resumableData = ResumableData(response: response, data: data) {\n            ResumableDataStorage.shared.storeResumableData(resumableData, for: request, pipeline: pipeline)\n        }\n    }\n}\n\nextension AsyncPipelineTask where Value == (Data, URLResponse?) {\n    func storeDataInCacheIfNeeded(_ data: Data) {\n        let request = makeSanitizedRequest()\n        guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), shouldStoreDataInDiskCache() else {\n            return\n        }\n        let key = pipeline.cache.makeDataCacheKey(for: request)\n        pipeline.delegate.willCache(data: data, image: nil, for: request, pipeline: pipeline) {\n            guard let data = $0 else { return }\n            // Important! Storing directly ignoring `ImageRequest.Options`.\n            dataCache.storeData(data, for: key)\n        }\n    }\n\n    /// Returns a request that doesn't contain any information non-related\n    /// to data loading.\n    private func makeSanitizedRequest() -> ImageRequest {\n        var request = request\n        request.processors = []\n        request.thumbnail = nil\n        return request\n    }\n\n    private func shouldStoreDataInDiskCache() -> Bool {\n        let imageTasks = imageTasks\n        guard imageTasks.contains(where: { !$0.request.options.contains(.disableDiskCacheWrites) }) else {\n            return false\n        }\n        guard !(request.url?.isLocalResource ?? false) else {\n            return false\n        }\n        switch pipeline.configuration.dataCachePolicy {\n        case .automatic:\n            return imageTasks.contains { $0.request.processors.isEmpty }\n        case .storeOriginalData:\n            return true\n        case .storeEncodedImages:\n            return false\n        case .storeAll:\n            return true\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Tasks/TaskFetchOriginalImage.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Receives data from ``TaskLoadImageData`` and decodes it as it arrives.\nfinal class TaskFetchOriginalImage: AsyncPipelineTask<ImageResponse>, @unchecked Sendable {\n    private var decoder: (any ImageDecoding)?\n    private var lastPreviewTime: CFAbsoluteTime?\n\n    override func start() {\n        if case .image(let fetch) = request.resource {\n            loadAsyncImage(fetch)\n            return\n        }\n        dependency = pipeline.makeTaskFetchOriginalData(for: request).subscribe(self) { [weak self] in\n            self?.didReceiveData($0.0, urlResponse: $0.1, isCompleted: $1)\n        }\n    }\n\n    /// Receiving data from `TaskFetchOriginalData`.\n    private func didReceiveData(_ data: Data, urlResponse: URLResponse?, isCompleted: Bool) {\n        guard isCompleted || pipeline.configuration.isProgressiveDecodingEnabled else {\n            return\n        }\n\n        if !isCompleted && operation != nil {\n            return // Back pressure - already decoding another progressive data chunk\n        }\n\n        if !isCompleted, let last = lastPreviewTime {\n            let interval = pipeline.configuration.progressiveDecodingInterval\n            if interval > 0 && CFAbsoluteTimeGetCurrent() - last < interval {\n                return\n            }\n        }\n\n        if isCompleted {\n            operation?.cancel() // Cancel any potential pending progressive decoding tasks\n        }\n\n        var decodingContext = ImageDecodingContext(request: request, data: data, isCompleted: isCompleted, urlResponse: urlResponse)\n        decodingContext.maximumDecodedImageSize = pipeline.configuration.maximumDecodedImageSize\n        if !isCompleted {\n            decodingContext.previewPolicy = pipeline.delegate.previewPolicy(for: decodingContext, pipeline: pipeline)\n        }\n        let context = decodingContext\n        guard let decoder = getDecoder(for: context) else {\n            if isCompleted {\n                send(error: .decoderNotRegistered(context: context))\n            } else {\n                // Try again when more data is downloaded.\n            }\n            return\n        }\n\n        decode(context, decoder: decoder) { [weak self] in\n            self?.didFinishDecoding(context: context, result: $0)\n        }\n    }\n\n    private func didFinishDecoding(context: ImageDecodingContext, result: Result<ImageResponse, ImagePipeline.Error>) {\n        operation = nil\n\n        switch result {\n        case .success(let response):\n            if !context.isCompleted {\n                lastPreviewTime = CFAbsoluteTimeGetCurrent()\n            }\n            send(value: response, isCompleted: context.isCompleted)\n        case .failure(let error):\n            if context.isCompleted {\n                send(error: error)\n            }\n        }\n    }\n\n    // Lazily creates decoding for task\n    private func getDecoder(for context: ImageDecodingContext) -> (any ImageDecoding)? {\n        // Return the existing processor in case it has already been created.\n        if let decoder {\n            return decoder\n        }\n        let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline)\n        self.decoder = decoder\n        return decoder\n    }\n\n    // MARK: Async Image Loading\n\n    private func loadAsyncImage(_ fetch: @Sendable @escaping () async throws -> ImageContainer) {\n        operation = pipeline.configuration.dataLoadingQueue.add { [weak self] in\n            await self?.performAsyncImageLoad(fetch)\n        }\n    }\n\n    private func performAsyncImageLoad(_ fetch: @Sendable @escaping () async throws -> ImageContainer) async {\n        guard !isDisposed else { return }\n        do {\n            let container = try await fetch()\n            send(value: ImageResponse(container: container, request: request), isCompleted: true)\n        } catch {\n            send(error: .dataLoadingFailed(error: error))\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Tasks/TaskLoadData.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Wrapper for tasks created by `loadData` calls.\nfinal class TaskLoadData: AsyncPipelineTask<ImageResponse>, @unchecked Sendable {\n    override func start() {\n        if let data = pipeline.cache.cachedData(for: request) {\n            let container = ImageContainer(image: .init(), data: data)\n            let response = ImageResponse(container: container, request: request)\n            self.send(value: response, isCompleted: true)\n        } else {\n            self.loadData()\n        }\n    }\n\n    private func loadData() {\n        guard !request.options.contains(.returnCacheDataDontLoad) else {\n            return send(error: .dataMissingInCache)\n        }\n        let request = request.withProcessors([])\n        dependency = pipeline.makeTaskFetchOriginalData(for: request).subscribe(self) { [weak self] in\n            self?.didReceiveData($0.0, urlResponse: $0.1, isCompleted: $1)\n        }\n    }\n\n    private func didReceiveData(_ data: Data, urlResponse: URLResponse?, isCompleted: Bool) {\n        let container = ImageContainer(image: .init(), data: data)\n        let response = ImageResponse(container: container, request: request, urlResponse: urlResponse)\n        if isCompleted {\n            send(value: response, isCompleted: isCompleted)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Nuke/Tasks/TaskLoadImage.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n/// Wrapper for tasks created by `loadImage` calls.\n///\n/// Performs all the quick cache lookups and also manages image processing.\n/// The coalescing for image processing is implemented on demand (extends the\n/// scenarios in which coalescing can kick in).\nfinal class TaskLoadImage: AsyncPipelineTask<ImageResponse>, @unchecked Sendable {\n    override func start() {\n        if let container = pipeline.cache[request] {\n            let response = ImageResponse(container: container, request: request, cacheType: .memory)\n            send(value: response, isCompleted: !container.isPreview)\n            if !container.isPreview {\n                return // The final image is loaded\n            }\n        }\n        if let data = pipeline.cache.cachedData(for: request) {\n            decodeCachedData(data)\n        } else if request.thumbnail != nil, request.processors.isEmpty,\n                  let data = pipeline.cache.cachedData(for: request.withoutThumbnail()) {\n            decodeCachedData(data)\n        } else {\n            fetchImage()\n        }\n    }\n\n    private func decodeCachedData(_ data: Data) {\n        let context = ImageDecodingContext(request: request, data: data, cacheType: .disk)\n        guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else {\n            return didFinishDecoding(with: nil)\n        }\n        decode(context, decoder: decoder) { [weak self] in\n            self?.didFinishDecoding(with: try? $0.get())\n        }\n    }\n\n    private func didFinishDecoding(with response: ImageResponse?) {\n        if let response {\n            didReceiveImageResponse(response, isCompleted: true)\n        } else {\n            fetchImage()\n        }\n    }\n\n    // MARK: Fetch Image\n\n    private func fetchImage() {\n        guard !request.options.contains(.returnCacheDataDontLoad) else {\n            return send(error: .dataMissingInCache)\n        }\n        if let processor = request.processors.last {\n            let request = request.withProcessors(request.processors.dropLast())\n            dependency = pipeline.makeTaskLoadImage(for: request).subscribe(self) { [weak self] in\n                self?.process($0, isCompleted: $1, processor: processor)\n            }\n        } else {\n            dependency = pipeline.makeTaskFetchOriginalImage(for: request).subscribe(self) { [weak self] in\n                self?.didReceiveImageResponse($0, isCompleted: $1)\n            }\n        }\n    }\n\n    // MARK: Processing\n\n    private func process(_ response: ImageResponse, isCompleted: Bool, processor: any ImageProcessing) {\n        guard !isDisposed else { return }\n        if isCompleted {\n            operation?.cancel() // Cancel any potential pending progressive\n        } else if operation != nil {\n            return // Back pressure - already processing another progressive image\n        }\n        let context = ImageProcessingContext(request: request, response: response, isCompleted: isCompleted)\n        operation = pipeline.configuration.imageProcessingQueue.add { [weak self] in\n            guard let self else { return }\n            let result = await performInBackground {\n                signpost(isCompleted ? \"ProcessImage\" : \"ProcessProgressiveImage\") {\n                    Result {\n                        var response = response\n                        response.container = try processor.process(response.container, context: context)\n                        return response\n                    }.mapError { error in\n                        ImagePipeline.Error.processingFailed(processor: processor, context: context, error: error)\n                    }\n                }\n            }\n            self.operation = nil\n            self.didFinishProcessing(result: result, isCompleted: isCompleted)\n        }\n    }\n\n    private func didFinishProcessing(result: Result<ImageResponse, ImagePipeline.Error>, isCompleted: Bool) {\n        switch result {\n        case .success(let response):\n            didReceiveImageResponse(response, isCompleted: isCompleted)\n        case .failure(let error):\n            if isCompleted {\n                send(error: error)\n            }\n        }\n    }\n\n    // MARK: Decompression\n\n    private func didReceiveImageResponse(_ response: ImageResponse, isCompleted: Bool) {\n        guard isDecompressionNeeded(for: response) else {\n            return didReceiveDecompressedImage(response, isCompleted: isCompleted)\n        }\n        guard !isDisposed else { return }\n        if isCompleted {\n            operation?.cancel() // Cancel any potential pending progressive decompression tasks\n        } else if operation != nil {\n            return  // Back-pressure: receiving progressive scans too fast\n        }\n        operation = pipeline.configuration.imageDecompressingQueue.add { [weak self] in\n            guard let self else { return }\n            let response = await performInBackground {\n                signpost(isCompleted ? \"DecompressImage\" : \"DecompressProgressiveImage\") {\n                    self.pipeline.delegate.decompress(response: response, request: self.request, pipeline: self.pipeline)\n                }\n            }\n            self.operation = nil\n            self.didReceiveDecompressedImage(response, isCompleted: isCompleted)\n        }\n    }\n\n    private func isDecompressionNeeded(for response: ImageResponse) -> Bool {\n        ImageDecompression.isDecompressionNeeded(for: response) &&\n        !request.options.contains(.skipDecompression) &&\n        hasDirectSubscribers &&\n        pipeline.delegate.shouldDecompress(response: response, for: request, pipeline: pipeline)\n    }\n\n    private func didReceiveDecompressedImage(_ response: ImageResponse, isCompleted: Bool) {\n        storeImageInCaches(response)\n        send(value: response, isCompleted: isCompleted)\n    }\n\n    // MARK: Caching\n\n    private func storeImageInCaches(_ response: ImageResponse) {\n        guard hasDirectSubscribers else {\n            return\n        }\n        pipeline.cache[request] = response.container\n        if shouldStoreResponseInDataCache(response) {\n            storeImageInDataCache(response)\n        }\n    }\n\n    private func storeImageInDataCache(_ response: ImageResponse) {\n        guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline) else {\n            return\n        }\n        let context = ImageEncodingContext(request: request, image: response.image, urlResponse: response.urlResponse)\n        let encoder = pipeline.delegate.imageEncoder(for: context, pipeline: pipeline)\n        let key = pipeline.cache.makeDataCacheKey(for: request)\n        pipeline.configuration.imageEncodingQueue.add { [weak pipeline, request] in\n            guard let pipeline else { return }\n            let data = await performInBackground {\n                signpost(\"EncodeImage\") {\n                    encoder.encode(response.container, context: context)\n                }\n            }\n            guard let data, !data.isEmpty else { return }\n            pipeline.delegate.willCache(data: data, image: response.container, for: request, pipeline: pipeline) {\n                guard let data = $0, !data.isEmpty else { return }\n                // Important! Storing directly ignoring `ImageRequest.Options`.\n                dataCache.storeData(data, for: key) // This is instant, writes are async\n            }\n        }\n    }\n\n    private func shouldStoreResponseInDataCache(_ response: ImageResponse) -> Bool {\n        guard !response.container.isPreview,\n              !(response.cacheType == .disk) else {\n            return false\n        }\n        let isProcessed = !request.processors.isEmpty || request.thumbnail != nil\n        switch pipeline.configuration.dataCachePolicy {\n        case .automatic:\n            return isProcessed\n        case .storeOriginalData:\n            return false\n        case .storeEncodedImages:\n            return true\n        case .storeAll:\n            return isProcessed\n        }\n    }\n\n    /// Returns `true` if the task has at least one image task that was directly\n    /// subscribed to it, which means that the request was initiated by the\n    /// user and not the framework.\n    private var hasDirectSubscribers: Bool {\n        subscribers.contains { $0 is ImageTask }\n    }\n}\n"
  },
  {
    "path": "Sources/NukeExtensions/ImageLoadingOptions.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\n#if !os(macOS)\nimport UIKit.UIImage\nimport UIKit.UIColor\n#else\nimport AppKit.NSImage\n#endif\n\n/// A set of options that control how the image is loaded and displayed.\npublic struct ImageLoadingOptions {\n    /// Shared options.\n    @MainActor public static var shared = ImageLoadingOptions()\n\n    /// Placeholder to be displayed when the image is loading. `nil` by default.\n    public var placeholder: PlatformImage?\n\n    /// Image to be displayed when the request fails. `nil` by default.\n    public var failureImage: PlatformImage?\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n    /// The image transition animation performed when displaying a loaded image.\n    /// Only runs when the image was not found in memory cache. `nil` by default.\n    public var transition: Transition?\n\n    /// The image transition animation performed when displaying a failure image.\n    /// `nil` by default.\n    public var failureImageTransition: Transition?\n\n    /// If true, the requested image will always appear with transition, even\n    /// when loaded from cache.\n    public var alwaysTransition = false\n\n    func transition(for response: ResponseType) -> Transition? {\n        switch response {\n        case .success: return transition\n        case .failure: return failureImageTransition\n        case .placeholder: return nil\n        }\n    }\n\n#endif\n\n    /// If true, every time you request a new image for a view, the view will be\n    /// automatically prepared for reuse: image will be set to `nil`, and animations\n    /// will be removed. `true` by default.\n    public var isPrepareForReuseEnabled = true\n\n    /// If `true`, every progressively generated preview produced by the pipeline\n    /// is going to be displayed. `true` by default.\n    ///\n    /// - note: To enable progressive decoding, see the `isProgressiveDecodingEnabled`\n    /// option in `ImagePipeline.Configuration`.\n    public var isProgressiveRenderingEnabled = true\n\n    /// Custom pipeline to be used. `nil` by default.\n    public var pipeline: ImagePipeline?\n\n    /// Image processors to be applied unless the processors are provided in the\n    /// request. `[]` by default.\n    public var processors: [any ImageProcessing] = []\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n\n    /// Content modes to be used for each image type (placeholder, success,\n    /// failure). `nil` by default (don't change content mode).\n    public var contentModes: ContentModes?\n\n    /// Custom content modes to be used for each image type (placeholder, success,\n    /// failure).\n    public struct ContentModes {\n        /// Content mode to be used for the loaded image.\n        public var success: UIView.ContentMode\n        /// Content mode to be used when displaying a `failureImage`.\n        public var failure: UIView.ContentMode\n        /// Content mode to be used when displaying a `placeholder`.\n        public var placeholder: UIView.ContentMode\n\n        /// - parameters:\n        ///   - success: A content mode to be used with a loaded image.\n        ///   - failure: A content mode to be used with a `failureImage`.\n        ///   - placeholder: A content mode to be used with a `placeholder`.\n        public init(success: UIView.ContentMode, failure: UIView.ContentMode, placeholder: UIView.ContentMode) {\n            self.success = success; self.failure = failure; self.placeholder = placeholder\n        }\n    }\n\n    func contentMode(for response: ResponseType) -> UIView.ContentMode? {\n        switch response {\n        case .success: return contentModes?.success\n        case .placeholder: return contentModes?.placeholder\n        case .failure: return contentModes?.failure\n        }\n    }\n\n    /// Tint colors to be used for each image type (placeholder, success,\n    /// failure). `nil` by default (don't change tint color or rendering mode).\n    public var tintColors: TintColors?\n\n    /// Custom tint color to be used for each image type (placeholder, success,\n    /// failure).\n    public struct TintColors {\n        /// Tint color to be used for the loaded image.\n        public var success: UIColor?\n        /// Tint color to be used when displaying a `failureImage`.\n        public var failure: UIColor?\n        /// Tint color to be used when displaying a `placeholder`.\n        public var placeholder: UIColor?\n\n        /// - parameters:\n        ///   - success: A tint color to be used with a loaded image.\n        ///   - failure: A tint color to be used with a `failureImage`.\n        ///   - placeholder: A tint color to be used with a `placeholder`.\n        public init(success: UIColor?, failure: UIColor?, placeholder: UIColor?) {\n            self.success = success; self.failure = failure; self.placeholder = placeholder\n        }\n    }\n\n    func tintColor(for response: ResponseType) -> UIColor? {\n        switch response {\n        case .success: return tintColors?.success\n        case .placeholder: return tintColors?.placeholder\n        case .failure: return tintColors?.failure\n        }\n    }\n\n#endif\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n\n    /// - parameters:\n    ///   - placeholder: Placeholder to be displayed when the image is loading.\n    ///   - transition: The image transition animation performed when\n    ///   displaying a loaded image. Only runs when the image was not found in\n    ///   memory cache.\n    ///   - failureImage: Image to be displayed when the request fails.\n    ///   - failureImageTransition: The image transition animation\n    ///   performed when displaying a failure image.\n    ///   - contentModes: Content modes to be used for each image type\n    ///   (placeholder, success, failure).\n    public init(placeholder: UIImage? = nil, transition: Transition? = nil, failureImage: UIImage? = nil, failureImageTransition: Transition? = nil, contentModes: ContentModes? = nil, tintColors: TintColors? = nil) {\n        self.placeholder = placeholder\n        self.transition = transition\n        self.failureImage = failureImage\n        self.failureImageTransition = failureImageTransition\n        self.contentModes = contentModes\n        self.tintColors = tintColors\n    }\n\n#elseif os(macOS)\n\n    public init(placeholder: NSImage? = nil, transition: Transition? = nil, failureImage: NSImage? = nil, failureImageTransition: Transition? = nil) {\n        self.placeholder = placeholder\n        self.transition = transition\n        self.failureImage = failureImage\n        self.failureImageTransition = failureImageTransition\n    }\n\n#elseif os(watchOS)\n\n    public init(placeholder: UIImage? = nil, failureImage: UIImage? = nil) {\n        self.placeholder = placeholder\n        self.failureImage = failureImage\n    }\n\n#endif\n\n    /// An animated image transition.\n    public struct Transition {\n        var style: Style\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n        enum Style { // internal representation\n            case fadeIn(parameters: Parameters)\n            case custom((ImageDisplayingView, UIImage) -> Void)\n        }\n\n        struct Parameters { // internal representation\n            let duration: TimeInterval\n            let options: UIView.AnimationOptions\n        }\n\n        /// Fade-in transition (cross-fade in case the image view is already\n        /// displaying an image).\n        public static func fadeIn(duration: TimeInterval, options: UIView.AnimationOptions = .allowUserInteraction) -> Transition {\n            Transition(style: .fadeIn(parameters: Parameters(duration: duration, options: options)))\n        }\n\n        /// Custom transition. Only runs when the image was not found in memory cache.\n        public static func custom(_ closure: @escaping (ImageDisplayingView, UIImage) -> Void) -> Transition {\n            Transition(style: .custom(closure))\n        }\n#elseif os(macOS)\n        enum Style { // internal representation\n            case fadeIn(parameters: Parameters)\n            case custom((ImageDisplayingView, NSImage) -> Void)\n        }\n\n        struct Parameters { // internal representation\n            let duration: TimeInterval\n        }\n\n        /// Fade-in transition.\n        public static func fadeIn(duration: TimeInterval) -> Transition {\n            Transition(style: .fadeIn(parameters: Parameters(duration: duration)))\n        }\n\n        /// Custom transition. Only runs when the image was not found in memory cache.\n        public static func custom(_ closure: @escaping (ImageDisplayingView, NSImage) -> Void) -> Transition {\n            Transition(style: .custom(closure))\n        }\n#else\n        enum Style {}\n#endif\n    }\n\n    public init() {}\n\n    enum ResponseType {\n        case success, failure, placeholder\n    }\n}\n"
  },
  {
    "path": "Sources/NukeExtensions/ImageViewExtensions.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\n#if !os(macOS)\nimport UIKit.UIImage\nimport UIKit.UIColor\n#else\nimport AppKit.NSImage\n#endif\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n/// Displays images. Add the conformance to this protocol to your views to make\n/// them compatible with Nuke image loading extensions.\n///\n/// The protocol is defined as `@objc` to make it possible to override its\n/// methods in extensions (e.g. you can override `nuke_display(image:data:)` in\n/// a `UIImageView` subclass like `Gifu.ImageView`).\n///\n/// The protocol and its methods have prefixes to make sure they don't clash\n/// with other similar methods and protocols in the Objective-C runtime.\n@MainActor\n@objc public protocol Nuke_ImageDisplaying {\n    /// Display a given image.\n    @objc func nuke_display(image: PlatformImage?, data: Data?)\n\n#if os(macOS)\n    @objc var layer: CALayer? { get }\n#endif\n}\n\nextension Nuke_ImageDisplaying {\n    func display(_ container: ImageContainer) {\n        nuke_display(image: container.image, data: container.data)\n    }\n}\n\n#if os(macOS)\nextension Nuke_ImageDisplaying {\n    public var layer: CALayer? { nil }\n}\n#endif\n\n#if os(iOS) || os(tvOS) || os(visionOS)\nimport UIKit\n/// A `UIView` that implements the `ImageDisplaying` protocol.\npublic typealias ImageDisplayingView = UIView & Nuke_ImageDisplaying\n\nextension UIImageView: Nuke_ImageDisplaying {\n    /// Displays an image.\n    open func nuke_display(image: UIImage?, data: Data? = nil) {\n        self.image = image\n    }\n}\n#elseif os(macOS)\nimport Cocoa\n/// An `NSObject` that implements the `ImageDisplaying` protocol.\n/// Can support `NSView` and `NSCell`. The latter can return nil for layer.\npublic typealias ImageDisplayingView = NSObject & Nuke_ImageDisplaying\n\nextension NSImageView: Nuke_ImageDisplaying {\n    /// Displays an image.\n    open func nuke_display(image: NSImage?, data: Data? = nil) {\n        self.image = image\n    }\n}\n#endif\n\n#if os(tvOS)\nimport TVUIKit\n\nextension TVPosterView: Nuke_ImageDisplaying {\n    /// Displays an image.\n    open func nuke_display(image: UIImage?, data: Data? = nil) {\n        self.image = image\n    }\n}\n#endif\n\n// MARK: - ImageView Extensions\n\n/// Loads an image with the given request and displays it in the view.\n///\n/// See the complete method signature for more information.\n@MainActor\n@discardableResult public func loadImage(\n    with url: URL?,\n    options: ImageLoadingOptions? = nil,\n    into view: ImageDisplayingView,\n    completion: @escaping @MainActor @Sendable (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void\n) -> ImageTask? {\n    loadImage(with: url, options: options, into: view, progress: nil, completion: completion)\n}\n\n/// Loads an image with the given request and displays it in the view.\n///\n/// Before loading a new image, the view is prepared for reuse by canceling any\n/// outstanding requests and removing a previously displayed image.\n///\n/// If the image is stored in the memory cache, it is displayed immediately with\n/// no animations. If not, the image is loaded using an image pipeline. When the\n/// image is loading, the `placeholder` is displayed. When the request\n/// completes the loaded image is displayed (or `failureImage` in case of an error)\n/// with the selected animation.\n///\n/// - parameters:\n///   - request: The image request. If `nil`, it's handled as a failure scenario.\n///   - options: `ImageLoadingOptions.shared` by default.\n///   - view: Nuke keeps a weak reference to the view. If the view is deallocated\n///   the associated request automatically gets canceled.\n///   - progress: A closure to be called periodically on the main thread\n///   when the progress is updated.\n///   - completion: A closure to be called on the main thread when the\n///   request is finished. Gets called synchronously if the response was found in\n///   the memory cache.\n///\n/// - returns: An image task or `nil` if the image was found in the memory cache.\n@MainActor\n@discardableResult public func loadImage(\n    with url: URL?,\n    options: ImageLoadingOptions? = nil,\n    into view: ImageDisplayingView,\n    progress: (@MainActor @Sendable (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil,\n    completion: (@MainActor @Sendable (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil\n) -> ImageTask? {\n    let controller = ImageViewController.controller(for: view)\n    return controller.loadImage(with: url.map({ ImageRequest(url: $0) }), options: options ?? .shared, progress: progress, completion: completion)\n}\n\n/// Loads an image with the given request and displays it in the view.\n///\n/// See the complete method signature for more information.\n@MainActor\n@discardableResult public func loadImage(\n    with request: ImageRequest?,\n    options: ImageLoadingOptions? = nil,\n    into view: ImageDisplayingView,\n    completion: @escaping @MainActor @Sendable (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void\n) -> ImageTask? {\n    loadImage(with: request, options: options ?? .shared, into: view, progress: nil, completion: completion)\n}\n\n/// Loads an image with the given request and displays it in the view.\n///\n/// Before loading a new image, the view is prepared for reuse by canceling any\n/// outstanding requests and removing a previously displayed image.\n///\n/// If the image is stored in the memory cache, it is displayed immediately with\n/// no animations. If not, the image is loaded using an image pipeline. When the\n/// image is loading, the `placeholder` is displayed. When the request\n/// completes the loaded image is displayed (or `failureImage` in case of an error)\n/// with the selected animation.\n///\n/// - parameters:\n///   - request: The image request. If `nil`, it's handled as a failure scenario.\n///   - options: `ImageLoadingOptions.shared` by default.\n///   - view: Nuke keeps a weak reference to the view. If the view is deallocated\n///   the associated request automatically gets canceled.\n///   - progress: A closure to be called periodically on the main thread\n///   when the progress is updated.\n///   - completion: A closure to be called on the main thread when the\n///   request is finished. Gets called synchronously if the response was found in\n///   the memory cache.\n///\n/// - returns: An image task or `nil` if the image was found in the memory cache.\n@MainActor\n@discardableResult public func loadImage(\n    with request: ImageRequest?,\n    options: ImageLoadingOptions? = nil,\n    into view: ImageDisplayingView,\n    progress: (@MainActor @Sendable (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil,\n    completion: (@MainActor @Sendable (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil\n) -> ImageTask? {\n    let controller = ImageViewController.controller(for: view)\n    return controller.loadImage(with: request, options: options ?? .shared, progress: progress, completion: completion)\n}\n\n/// Cancels an outstanding request associated with the view.\n@MainActor\npublic func cancelRequest(for view: ImageDisplayingView) {\n    ImageViewController.controller(for: view).cancelOutstandingTask()\n}\n\n// MARK: - ImageViewController\n\n/// Manages image requests on behalf of an image view.\n///\n/// - note: With a few modifications this might become public at some point,\n/// however as it stands today `ImageViewController` is just a helper class,\n/// making it public wouldn't expose any additional functionality to the users.\n@MainActor\nprivate final class ImageViewController {\n    private weak var imageView: ImageDisplayingView?\n    private var task: ImageTask?\n    private var options: ImageLoadingOptions\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n    // Image view used for cross-fade transition between images with different\n    // content modes.\n    private lazy var transitionImageView = UIImageView()\n#endif\n\n    // Automatically cancel the request when the view is deallocated.\n    deinit {\n        task?.cancel()\n    }\n\n    init(view: /* weak */ ImageDisplayingView) {\n        self.imageView = view\n        self.options = .shared\n    }\n\n    // MARK: - Associating Controller\n\n    // Safe because it's never mutated.\n    nonisolated(unsafe) static let controllerAK = malloc(1)!\n\n    // Lazily create a controller for a given view and associate it with a view.\n    static func controller(for view: ImageDisplayingView) -> ImageViewController {\n        if let controller = objc_getAssociatedObject(view, controllerAK) as? ImageViewController {\n            return controller\n        }\n        let controller = ImageViewController(view: view)\n        objc_setAssociatedObject(view, controllerAK, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)\n        return controller\n    }\n\n    // MARK: - Loading Images\n\n    func loadImage(\n        with request: ImageRequest?,\n        options: ImageLoadingOptions,\n        progress: (@MainActor @Sendable (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil,\n        completion: (@MainActor @Sendable (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil\n    ) -> ImageTask? {\n        cancelOutstandingTask()\n\n        guard let imageView else {\n            return nil\n        }\n\n        self.options = options\n\n        if options.isPrepareForReuseEnabled { // enabled by default\n#if os(iOS) || os(tvOS) || os(visionOS)\n            imageView.layer.removeAllAnimations()\n#elseif os(macOS)\n            let layer = (imageView as? NSView)?.layer ?? imageView.layer\n            layer?.removeAllAnimations()\n#endif\n        }\n\n        // Handle a scenario where request is `nil` (in the same way as a failure)\n        guard var request else {\n            if options.isPrepareForReuseEnabled {\n                imageView.nuke_display(image: nil, data: nil)\n            }\n            let result: Result<ImageResponse, ImagePipeline.Error> = .failure(.imageRequestMissing)\n            handle(result: result, isFromMemory: true)\n            completion?(result)\n            return nil\n        }\n\n        let pipeline = options.pipeline ?? ImagePipeline.shared\n        if !options.processors.isEmpty && request.processors.isEmpty {\n            request.processors = options.processors\n        }\n\n        // Quick synchronous memory cache lookup.\n        if let image = pipeline.cache[request] {\n            display(image, true, .success)\n            if !image.isPreview { // Final image was downloaded\n                completion?(.success(ImageResponse(container: image, request: request, cacheType: .memory)))\n                return nil // No task to perform\n            }\n        }\n\n        // Display a placeholder.\n        if let placeholder = options.placeholder {\n            display(ImageContainer(image: placeholder), true, .placeholder)\n        } else if options.isPrepareForReuseEnabled {\n            imageView.nuke_display(image: nil, data: nil) // Remove previously displayed images (if any)\n        }\n\n        task = pipeline.loadImage(with: request, progress: { [weak self] response, completedCount, totalCount in\n            if let response, options.isProgressiveRenderingEnabled {\n                self?.handle(partialImage: response)\n            }\n            progress?(response, completedCount, totalCount)\n        }, completion: { [weak self] result in\n            self?.handle(result: result, isFromMemory: false)\n            completion?(result)\n        })\n        return task\n    }\n\n    func cancelOutstandingTask() {\n        task?.cancel() // The pipeline guarantees no callbacks to be deliver after cancellation\n        task = nil\n    }\n\n    // MARK: - Handling Responses\n\n    private func handle(result: Result<ImageResponse, ImagePipeline.Error>, isFromMemory: Bool) {\n        switch result {\n        case let .success(response):\n            display(response.container, isFromMemory, .success)\n        case .failure:\n            if let failureImage = options.failureImage {\n                display(ImageContainer(image: failureImage), isFromMemory, .failure)\n            }\n        }\n        self.task = nil\n    }\n\n    private func handle(partialImage response: ImageResponse) {\n        display(response.container, false, .success)\n    }\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n    private func display(_ image: ImageContainer, _ isFromMemory: Bool, _ response: ImageLoadingOptions.ResponseType) {\n        guard let imageView else {\n            return\n        }\n\n        var image = image\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n        if let tintColor = options.tintColor(for: response) {\n            image.image = image.image.withRenderingMode(.alwaysTemplate)\n            imageView.tintColor = tintColor\n        }\n#endif\n\n        if !isFromMemory || options.alwaysTransition, let transition = options.transition(for: response) {\n            switch transition.style {\n            case let .fadeIn(params):\n                runFadeInTransition(image: image, params: params, response: response)\n            case let .custom(closure):\n                // The user is responsible for both displaying an image and performing\n                // animations.\n                closure(imageView, image.image)\n            }\n        } else {\n            imageView.display(image)\n        }\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n        if let contentMode = options.contentMode(for: response) {\n            imageView.contentMode = contentMode\n        }\n#endif\n    }\n\n#elseif os(watchOS)\n\n    private func display(_ image: ImageContainer, _ isFromMemory: Bool, _ response: ImageLoadingOptions.ResponseType) {\n        imageView?.display(image)\n    }\n\n#endif\n}\n\n// MARK: - ImageViewController (Transitions)\n\nextension ImageViewController {\n#if os(iOS) || os(tvOS) || os(visionOS)\n\n    private func runFadeInTransition(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters, response: ImageLoadingOptions.ResponseType) {\n        guard let imageView else {\n            return\n        }\n\n        // Special case where it animates between content modes, only works\n        // on imageView subclasses.\n        if let contentMode = options.contentMode(for: response), imageView.contentMode != contentMode, let imageView = imageView as? UIImageView, imageView.image != nil {\n            runCrossDissolveWithContentMode(imageView: imageView, image: image, params: params)\n        } else {\n            runSimpleFadeIn(image: image, params: params)\n        }\n    }\n\n    private func runSimpleFadeIn(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters) {\n        guard let imageView else {\n            return\n        }\n\n        UIView.transition(\n            with: imageView,\n            duration: params.duration,\n            options: params.options.union(.transitionCrossDissolve),\n            animations: {\n                imageView.nuke_display(image: image.image, data: image.data)\n            },\n            completion: nil\n        )\n    }\n\n    /// Performs cross-dissolve animation alongside transition to a new content\n    /// mode. This isn't natively supported feature and it requires a second\n    /// image view. There might be better ways to implement it.\n    private func runCrossDissolveWithContentMode(imageView: UIImageView, image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters) {\n        // Lazily create a transition view.\n        let transitionView = self.transitionImageView\n\n        // Create a transition view which mimics current view's contents.\n        transitionView.image = imageView.image\n        transitionView.contentMode = imageView.contentMode\n        transitionView.frame = imageView.frame\n        transitionView.tintColor = imageView.tintColor\n        transitionView.tintAdjustmentMode = imageView.tintAdjustmentMode\n        if #available(iOS 17.0, tvOS 17.0, *) {\n            transitionView.preferredImageDynamicRange = imageView.preferredImageDynamicRange\n        }\n        transitionView.preferredSymbolConfiguration = imageView.preferredSymbolConfiguration\n        transitionView.isHidden = imageView.isHidden\n        transitionView.clipsToBounds = imageView.clipsToBounds\n        transitionView.layer.cornerRadius = imageView.layer.cornerRadius\n        transitionView.layer.cornerCurve = imageView.layer.cornerCurve\n        transitionView.layer.maskedCorners = imageView.layer.maskedCorners\n        imageView.superview?.insertSubview(transitionView, aboveSubview: imageView)\n\n        // \"Manual\" cross-fade.\n        transitionView.alpha = 1\n        imageView.alpha = 0\n        imageView.display(image) // Display new image in current view\n\n        UIView.animate(\n            withDuration: params.duration,\n            delay: 0,\n            options: params.options,\n            animations: {\n                transitionView.alpha = 0\n                imageView.alpha = 1\n            },\n            completion: { [weak transitionView] isCompleted in\n                if isCompleted, let transitionView {\n                    transitionView.removeFromSuperview()\n                    transitionView.image = nil\n                }\n            }\n        )\n    }\n\n#elseif os(macOS)\n\n    private func runFadeInTransition(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters, response: ImageLoadingOptions.ResponseType) {\n        let animation = CABasicAnimation(keyPath: \"opacity\")\n        animation.duration = params.duration\n        animation.fromValue = 0\n        animation.toValue = 1\n        imageView?.layer?.add(animation, forKey: \"imageTransition\")\n\n        imageView?.display(image)\n    }\n\n#endif\n}\n\n#endif\n"
  },
  {
    "path": "Sources/NukeUI/FetchImage.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport SwiftUI\nimport Combine\nimport Nuke\n\n/// An observable object that simplifies image loading in SwiftUI.\n@MainActor\npublic final class FetchImage: ObservableObject, Identifiable {\n    /// Returns the current fetch result.\n    @Published public private(set) var result: Result<ImageResponse, Error>?\n\n    /// Returns the fetched image.\n    public var image: Image? {\n#if os(macOS)\n        imageContainer.map { Image(nsImage: $0.image) }\n#else\n        imageContainer.map { Image(uiImage: $0.image) }\n#endif\n    }\n\n    /// Returns the fetched image.\n    ///\n    /// - note: In case the pipeline has the `isProgressiveDecodingEnabled` option enabled\n    /// and the image being downloaded supports progressive decoding, the `image`\n    /// might be updated multiple times during the download.\n    @Published public private(set) var imageContainer: ImageContainer?\n\n    /// Returns `true` if the image is being loaded.\n    @Published public private(set) var isLoading = false\n\n    /// Animations to be used when displaying the loaded images. By default, `nil`.\n    ///\n    /// - note: Animation isn't used when the image is available in the memory cache.\n    public var transaction = Transaction(animation: nil)\n\n    /// The progress of the current image download.\n    public var progress: Progress {\n        if _progress == nil {\n            _progress = Progress()\n        }\n        return _progress!\n    }\n\n    private var _progress: Progress?\n\n    /// The download progress.\n    public final class Progress: ObservableObject {\n        /// The number of bytes that the task has received.\n        @Published public internal(set) var completed: Int64 = 0\n\n        /// A best-guess upper bound on the number of bytes of the resource.\n        @Published public internal(set) var total: Int64 = 0\n\n        /// Returns the fraction of the completion.\n        public var fraction: Float {\n            guard total > 0 else { return 0 }\n            return min(1, Float(completed) / Float(total))\n        }\n    }\n\n    /// Overrides the priority of the current and future requests. When `nil`\n    /// (the default), the request's own priority is used. Can be updated while\n    /// a task is already running.\n    public var priority: ImageRequest.Priority? {\n        didSet { priority.map { imageTask?.priority = $0 } }\n    }\n\n    /// A pipeline used for performing image requests.\n    public var pipeline: ImagePipeline = .shared\n\n    /// Image processors to be applied unless the processors are provided in the\n    /// request. `[]` by default.\n    public var processors: [any ImageProcessing] = []\n\n    /// Gets called when the request is started.\n    public var onStart: (@MainActor @Sendable (ImageTask) -> Void)?\n\n    /// Gets called when the current request is completed.\n    public var onCompletion: (@MainActor @Sendable (Result<ImageResponse, Error>) -> Void)?\n\n    private var imageTask: ImageTask?\n    private var lastResponse: ImageResponse?\n    private var cancellable: AnyCancellable?\n\n    deinit {\n        imageTask?.cancel()\n    }\n\n    /// Initializes the image. To load an image, use one of the `load()` methods.\n    public init() {}\n\n    // MARK: Loading Images\n\n    /// Loads an image with the given request.\n    public func load(_ url: URL?) {\n        load(url.map { ImageRequest(url: $0) })\n    }\n\n    /// Loads an image with the given request.\n    public func load(_ request: ImageRequest?) {\n        assert(Thread.isMainThread, \"Must be called from the main thread\")\n\n        reset()\n\n        guard var request else {\n            handle(result: .failure(ImagePipeline.Error.imageRequestMissing))\n            return\n        }\n\n        if !processors.isEmpty && request.processors.isEmpty {\n            request.processors = processors\n        }\n        if let priority {\n            request.priority = priority\n        }\n\n        // Quick synchronous memory cache lookup\n        if let image = pipeline.cache[request] {\n            if image.isPreview {\n                imageContainer = image // Display progressive image\n            } else {\n                let response = ImageResponse(container: image, request: request, cacheType: .memory)\n                handle(result: .success(response))\n                return\n            }\n        }\n\n        isLoading = true\n\n        let task = pipeline.loadImage(\n            with: request,\n            progress: { [weak self] response, completed, total in\n                guard let self else { return }\n                if let response {\n                    withTransaction(self.transaction) {\n                        self.handle(preview: response)\n                    }\n                } else {\n                    self._progress?.completed = completed\n                    self._progress?.total = total\n                }\n            },\n            completion: { [weak self] result in\n                guard let self else { return }\n                withTransaction(self.transaction) {\n                    self.handle(result: result.mapError { $0 })\n                }\n            }\n        )\n        imageTask = task\n        onStart?(task)\n    }\n\n    private func handle(preview: ImageResponse) {\n        // Display progressively decoded image\n        self.imageContainer = preview.container\n    }\n\n    private func handle(result: Result<ImageResponse, Error>) {\n        isLoading = false\n        imageTask = nil\n\n        if case .success(let response) = result {\n            self.imageContainer = response.container\n        }\n        self.result = result\n        self.onCompletion?(result)\n    }\n\n    // MARK: Load (Async/Await)\n\n    /// Loads and displays an image using the given async function.\n    ///\n    /// - parameter action: Fetches the image.\n    public func load(_ action: @escaping () async throws -> ImageResponse) {\n        reset()\n        isLoading = true\n\n        let task = Task {\n            do {\n                let response = try await action()\n                withTransaction(transaction) {\n                    handle(result: .success(response))\n                }\n            } catch {\n                handle(result: .failure(error))\n            }\n        }\n\n        cancellable = AnyCancellable { task.cancel() }\n    }\n\n    // MARK: Load (Combine)\n\n    /// Loads an image with the given publisher.\n    ///\n    /// - important: Some `FetchImage` features, such as progress reporting and\n    /// dynamically changing the request priority, are not available when\n    /// working with a publisher.\n    public func load<P: Publisher>(_ publisher: P) where P.Output == ImageResponse {\n        reset()\n\n        // Not using `first()` because it should support progressive decoding\n        isLoading = true\n        cancellable = publisher.sink(receiveCompletion: { [weak self] completion in\n            guard let self else { return }\n            self.isLoading = false\n            switch completion {\n            case .finished:\n                if let response = self.lastResponse {\n                    self.result = .success(response)\n                } // else was cancelled, do nothing\n            case .failure(let error):\n                self.result = .failure(error)\n            }\n        }, receiveValue: { [weak self] response in\n            guard let self else { return }\n            self.lastResponse = response\n            self.imageContainer = response.container\n        })\n    }\n\n    // MARK: Cancel\n\n    /// Marks the request as being cancelled. Continues to display a downloaded image.\n    public func cancel() {\n        // pipeline-based\n        imageTask?.cancel() // Guarantees that no more callbacks will be delivered\n        imageTask = nil\n\n        // publisher-based\n        cancellable = nil\n    }\n\n    /// Resets the `FetchImage` instance by cancelling the request and removing\n    /// all of the state including the loaded image.\n    public func reset() {\n        cancel()\n\n        // Avoid publishing unchanged values\n        if isLoading { isLoading = false }\n        if imageContainer != nil { imageContainer = nil }\n        if result != nil { result = nil }\n        if _progress != nil { _progress = nil }\n        lastResponse = nil // publisher-only\n    }\n}\n"
  },
  {
    "path": "Sources/NukeUI/Internal.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\n#if !os(watchOS)\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\nimport SwiftUI\nimport Nuke\n\n#if os(macOS)\npublic typealias _PlatformBaseView = NSView\ntypealias _PlatformImageView = NSImageView\ntypealias _PlatformColor = NSColor\n#else\npublic typealias _PlatformBaseView = UIView\ntypealias _PlatformImageView = UIImageView\ntypealias _PlatformColor = UIColor\n#endif\n\nextension _PlatformBaseView {\n    @discardableResult\n    func pinToSuperview() -> [NSLayoutConstraint] {\n        guard let superview else { return [] }\n\n        translatesAutoresizingMaskIntoConstraints = false\n        let constraints = [\n            topAnchor.constraint(equalTo: superview.topAnchor),\n            bottomAnchor.constraint(equalTo: superview.bottomAnchor),\n            leftAnchor.constraint(equalTo: superview.leftAnchor),\n            rightAnchor.constraint(equalTo: superview.rightAnchor)\n        ]\n        NSLayoutConstraint.activate(constraints)\n        return constraints\n    }\n\n    @discardableResult\n    func centerInSuperview() -> [NSLayoutConstraint] {\n        guard let superview else { return [] }\n\n        translatesAutoresizingMaskIntoConstraints = false\n        let constraints = [\n            centerXAnchor.constraint(equalTo: superview.centerXAnchor),\n            centerYAnchor.constraint(equalTo: superview.centerYAnchor)\n        ]\n        NSLayoutConstraint.activate(constraints)\n        return constraints\n    }\n\n    @discardableResult\n    func layout(with position: LazyImageView.SubviewPosition) -> [NSLayoutConstraint] {\n        switch position {\n        case .center: return centerInSuperview()\n        case .fill: return pinToSuperview()\n        }\n    }\n}\n\nextension CALayer {\n    func animateOpacity(duration: CFTimeInterval) {\n        let animation = CABasicAnimation(keyPath: \"opacity\")\n        animation.duration = duration\n        animation.fromValue = 0\n        animation.toValue = 1\n        add(animation, forKey: \"imageTransition\")\n    }\n}\n\n#if os(macOS)\nextension NSView {\n    func setNeedsUpdateConstraints() {\n        needsUpdateConstraints = true\n    }\n\n    func insertSubview(_ subivew: NSView, at index: Int) {\n        addSubview(subivew, positioned: .below, relativeTo: subviews.first)\n    }\n}\n\nextension NSColor {\n    static var secondarySystemBackground: NSColor {\n        .controlBackgroundColor // Close-enough, but we should define a custom color\n    }\n}\n#endif\n\n#endif\n\n#if os(tvOS) || os(watchOS)\nimport UIKit\n\nextension UIColor {\n    static var secondarySystemBackground: UIColor {\n        lightGray.withAlphaComponent(0.5)\n    }\n}\n#endif\n\nfunc == (lhs: [any ImageProcessing], rhs: [any ImageProcessing]) -> Bool {\n    guard lhs.count == rhs.count else {\n        return false\n    }\n    // Lazily creates `hashableIdentifiers` because for some processors the\n    // identifiers might be expensive to compute.\n    return zip(lhs, rhs).allSatisfy {\n        $0.hashableIdentifier == $1.hashableIdentifier\n    }\n}\n"
  },
  {
    "path": "Sources/NukeUI/LazyImage.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\nimport SwiftUI\nimport Combine\n\npublic typealias ImageRequest = Nuke.ImageRequest\n\n/// A view that asynchronously loads and displays an image.\n///\n/// ``LazyImage`` is designed to be similar to the native [`AsyncImage`](https://developer.apple.com/documentation/SwiftUI/AsyncImage),\n/// but it uses [Nuke](https://github.com/kean/Nuke) for loading images. You\n/// can take advantage of all of its features, such as caching, prefetching,\n/// task coalescing, smart background decompression, request priorities, and more.\n@MainActor\npublic struct LazyImage<Content: View>: View {\n    @StateObject private var viewModel = FetchImage()\n\n    private var context: LazyImageContext?\n    private var makeContent: ((LazyImageState) -> Content)?\n    private var transaction: Transaction\n    private var pipeline: ImagePipeline = .shared\n    private var onStart: (@MainActor @Sendable (ImageTask) -> Void)?\n    private var onDisappearBehavior: DisappearBehavior? = .cancel\n    private var onCompletion: (@MainActor @Sendable (Result<ImageResponse, Error>) -> Void)?\n\n    // MARK: Initializers\n\n    /// Loads and displays an image using `SwiftUI.Image`.\n    ///\n    /// - Parameters:\n    ///   - url: The image URL.\n    public init(url: URL?) where Content == Image {\n        self.init(request: url.map { ImageRequest(url: $0) })\n    }\n\n    /// Loads and displays an image using `SwiftUI.Image`.\n    ///\n    /// - Parameters:\n    ///   - request: The image request.\n    public init(request: ImageRequest?) where Content == Image {\n        self.context = request.map(LazyImageContext.init)\n        self.transaction = Transaction(animation: nil)\n    }\n\n    /// Loads an image and displays custom content for each state.\n    ///\n    /// See also ``init(request:transaction:content:)``\n    public init(url: URL?,\n                transaction: Transaction = Transaction(animation: nil),\n                @ViewBuilder content: @escaping (LazyImageState) -> Content) {\n        self.init(request: url.map { ImageRequest(url: $0) }, transaction: transaction, content: content)\n    }\n\n    /// Loads an image and displays custom content for each state.\n    ///\n    /// - Parameters:\n    ///   - request: The image request.\n    ///   - content: The view to show for each of the image loading states.\n    ///\n    /// ```swift\n    /// LazyImage(request: $0) { state in\n    ///     if let image = state.image {\n    ///         image // Displays the loaded image.\n    ///     } else if state.error != nil {\n    ///         Color.red // Indicates an error.\n    ///     } else {\n    ///         Color.blue // Acts as a placeholder.\n    ///     }\n    /// }\n    /// ```\n    public init(request: ImageRequest?,\n                transaction: Transaction = Transaction(animation: nil),\n                @ViewBuilder content: @escaping (LazyImageState) -> Content) {\n        self.context = request.map { LazyImageContext(request: $0) }\n        self.transaction = transaction\n        self.makeContent = content\n    }\n\n    // MARK: Options\n\n    /// Sets processors to be applied to the image.\n    ///\n    /// Processors are only applied if the request does not already define its\n    /// own processors. The request's processors always take priority.\n    public consuming func processors(_ processors: [any ImageProcessing]?) -> Self {\n        map { $0.context?.request.processors = processors ?? [] }\n    }\n\n    /// Sets the priority of the requests.\n    public consuming func priority(_ priority: ImageRequest.Priority?) -> Self {\n        map { $0.context?.request.priority = priority ?? .normal }\n    }\n\n    /// Changes the underlying pipeline used for image loading.\n    public consuming func pipeline(_ pipeline: ImagePipeline) -> Self {\n        map { $0.pipeline = pipeline }\n    }\n\n    @frozen public enum DisappearBehavior {\n        /// Cancels the current request but keeps the presentation state of\n        /// the already displayed image.\n        case cancel\n        /// Lowers the request's priority to very low.\n        case lowerPriority\n    }\n\n    /// Gets called when the request is started.\n    public consuming func onStart(_ closure: @escaping @MainActor @Sendable (ImageTask) -> Void) -> Self {\n        map { $0.onStart = closure }\n    }\n\n    /// Override the behavior on disappear. By default, the view is reset.\n    public consuming func onDisappear(_ behavior: DisappearBehavior?) -> Self {\n        map { $0.onDisappearBehavior = behavior }\n    }\n\n    /// Gets called when the current request is completed.\n    public consuming func onCompletion(_ closure: @escaping @MainActor @Sendable (Result<ImageResponse, Error>) -> Void) -> Self {\n        map { $0.onCompletion = closure }\n    }\n\n    private consuming func map(_ closure: (inout LazyImage) -> Void) -> Self {\n        var copy = self\n        closure(&copy)\n        return copy\n    }\n\n    // MARK: Body\n\n    public var body: some View {\n        ZStack {\n            if let makeContent {\n                makeContent(viewModel)\n            } else {\n                makeDefaultContent(for: viewModel)\n            }\n        }\n        .onAppear { onAppear() }\n        .onDisappear { onDisappear() }\n        .onChange(of: context) {\n            viewModel.load($0?.request)\n        }\n    }\n\n    @ViewBuilder\n    private func makeDefaultContent(for state: LazyImageState) -> some View {\n        if let image = state.image {\n            image\n        } else {\n            Color(.secondarySystemBackground)\n        }\n    }\n\n    private func onAppear() {\n        viewModel.transaction = transaction\n        viewModel.pipeline = pipeline\n        viewModel.onStart = onStart\n        viewModel.onCompletion = onCompletion\n        viewModel.load(context?.request)\n    }\n\n    private func onDisappear() {\n        guard let behavior = onDisappearBehavior else { return }\n        switch behavior {\n        case .cancel:\n            viewModel.cancel()\n        case .lowerPriority:\n            viewModel.priority = .veryLow\n        }\n    }\n}\n\nprivate struct LazyImageContext: Equatable {\n    var request: ImageRequest\n\n    static func == (lhs: LazyImageContext, rhs: LazyImageContext) -> Bool {\n        let lhs = lhs.request\n        let rhs = rhs.request\n        return lhs.imageID == rhs.imageID &&\n        lhs.priority == rhs.priority &&\n        lhs.processors == rhs.processors &&\n        lhs.priority == rhs.priority &&\n        lhs.options == rhs.options\n    }\n}\n\n#if DEBUG\nstruct LazyImage_Previews: PreviewProvider {\n    static var previews: some View {\n        Group {\n            LazyImageDemoView()\n                .previewDisplayName(\"LazyImage\")\n\n            LazyImage(url: URL(string: \"https://kean.blog/images/pulse/01.png\"))\n                .previewDisplayName(\"LazyImage (Default)\")\n\n            AsyncImage(url: URL(string: \"https://kean.blog/images/pulse/01.png\"))\n                .previewDisplayName(\"AsyncImage\")\n        }\n    }\n}\n\n// This demonstrates that the view reacts correctly to the URL changes.\nprivate struct LazyImageDemoView: View {\n    @State var url = URL(string: \"https://kean.blog/images/pulse/01.png\")\n    @State var isBlured = false\n    @State var imageViewId = UUID()\n\n    var body: some View {\n        VStack {\n            Spacer()\n\n            LazyImage(url: url) { state in\n                if let image = state.image {\n                    image.resizable().aspectRatio(contentMode: .fit)\n                }\n            }\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n            .processors(isBlured ? [ImageProcessors.GaussianBlur()] : [])\n#endif\n            .id(imageViewId) // Example of how to implement retry\n\n            Spacer()\n            VStack(alignment: .leading, spacing: 16) {\n                Button(\"Change Image\") {\n                    if url == URL(string: \"https://kean.blog/images/pulse/01.png\") {\n                        url = URL(string: \"https://kean.blog/images/pulse/02.png\")\n                    } else {\n                        url = URL(string: \"https://kean.blog/images/pulse/01.png\")\n                    }\n                }\n                Button(\"Retry\") { imageViewId = UUID() }\n                Toggle(\"Apply Blur\", isOn: $isBlured)\n            }\n            .padding()\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n            .background(Material.ultraThick)\n#endif\n        }\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/NukeUI/LazyImageState.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\nimport SwiftUI\nimport Combine\n\n/// Describes the current image state.\n@MainActor\npublic protocol LazyImageState {\n    /// Returns the current fetch result.\n    var result: Result<ImageResponse, Error>? { get }\n\n    /// Returns the fetched image.\n    ///\n    /// - note: In case the pipeline has the `isProgressiveDecodingEnabled` option enabled\n    /// and the image being downloaded supports progressive decoding, the `image`\n    /// might be updated multiple times during the download.\n    var imageContainer: ImageContainer? { get }\n\n    /// Returns `true` if the image is being loaded.\n    var isLoading: Bool { get }\n\n    /// The progress of the image download.\n    var progress: FetchImage.Progress { get }\n}\n\nextension LazyImageState {\n    /// Returns the current error.\n    public var error: Error? {\n        if case .failure(let error) = result {\n            return error\n        }\n        return nil\n    }\n\n    /// Returns an image view.\n    public var image: Image? {\n#if os(macOS)\n        imageContainer.map { Image(nsImage: $0.image) }\n#else\n        imageContainer.map { Image(uiImage: $0.image) }\n#endif\n    }\n}\n\nextension FetchImage: LazyImageState {}\n"
  },
  {
    "path": "Sources/NukeUI/LazyImageView.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\n#if !os(watchOS)\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// Lazily loads and displays images.\n///\n/// ``LazyImageView`` is a ``LazyImage`` counterpart for UIKit and AppKit with the equivalent set of APIs.\n///\n/// ```swift\n/// let imageView = LazyImageView()\n/// imageView.placeholderView = UIActivityIndicatorView()\n/// imageView.priority = .high\n/// imageView.pipeline = customPipeline\n/// imageView.onCompletion = { _ in print(\"Request completed\") }\n///\n/// imageView.url = URL(string: \"https://example.com/image.jpeg\")\n/// ```\n@MainActor\npublic final class LazyImageView: _PlatformBaseView {\n\n    // MARK: Placeholder View\n\n    /// An image to be shown while the request is in progress.\n    public var placeholderImage: PlatformImage? {\n        didSet { setPlaceholderImage(placeholderImage) }\n    }\n\n    /// A view to be shown while the request is in progress. For example,\n    /// a spinner.\n    public var placeholderView: _PlatformBaseView? {\n        didSet { setPlaceholderView(oldValue, placeholderView) }\n    }\n\n    /// The position of the placeholder. `.fill` by default.\n    ///\n    /// It also affects `placeholderImage` because it gets converted to a view.\n    public var placeholderViewPosition: SubviewPosition = .fill {\n        didSet {\n            guard oldValue != placeholderViewPosition,\n                  placeholderView != nil else { return }\n            setNeedsUpdateConstraints()\n        }\n    }\n\n    /// Displays the placeholder image or view in the case of a failure.\n    /// `false` by default.\n    public var showPlaceholderOnFailure = false\n\n    private var placeholderViewConstraints: [NSLayoutConstraint] = []\n\n    // MARK: Failure View\n\n    /// An image to be shown if the request fails.\n    public var failureImage: PlatformImage? {\n        didSet { setFailureImage(failureImage) }\n    }\n\n    /// A view to be shown if the request fails.\n    public var failureView: _PlatformBaseView? {\n        didSet { setFailureView(oldValue, failureView) }\n    }\n\n    /// The position of the failure view. `.fill` by default.\n    ///\n    /// It also affects `failureImage` because it gets converted to a view.\n    public var failureViewPosition: SubviewPosition = .fill {\n        didSet {\n            guard oldValue != failureViewPosition,\n                  failureView != nil else { return }\n            setNeedsUpdateConstraints()\n        }\n    }\n\n    private var failureViewConstraints: [NSLayoutConstraint] = []\n\n    // MARK: Transition\n\n    /// An animated transition to be performed when displaying a loaded image.\n    /// `.fadeIn(duration: 0.33)` by default.\n    public var transition: Transition?\n\n    /// An animated transition.\n    @frozen public enum Transition {\n        /// Fade-in transition.\n        case fadeIn(duration: TimeInterval)\n        /// A custom image view transition.\n        ///\n        /// The closure will get called after the image is already displayed but\n        /// before `imageContainer` value is updated.\n        case custom(closure: (LazyImageView, ImageContainer) -> Void)\n    }\n\n    // MARK: Underlying Views\n\n#if os(macOS)\n    /// Returns the underlying image view.\n    public let imageView = NSImageView()\n#else\n    public let imageView = UIImageView()\n#endif\n\n    /// Creates a custom view for displaying the given image response.\n    ///\n    /// Return `nil` to use the default platform image view.\n    public var makeImageView: ((ImageContainer) -> _PlatformBaseView?)?\n\n    private var customImageView: _PlatformBaseView?\n\n    // MARK: Managing Image Tasks\n\n    /// Processors to be applied to the image. `nil` by default.\n    ///\n    /// If you pass an image request with a non-empty list of processors as\n    /// a source, your processors will be applied instead.\n    public var processors: [any ImageProcessing]?\n\n    /// Sets the priority of the image task. The priority can be changed\n    /// dynamically. `nil` by default.\n    public var priority: ImageRequest.Priority? {\n        didSet {\n            if let priority {\n                imageTask?.priority = priority\n            }\n        }\n    }\n\n    /// Current image task.\n    public var imageTask: ImageTask?\n\n    /// The pipeline to be used for download. `shared` by default.\n    public var pipeline: ImagePipeline = .shared\n\n    // MARK: Callbacks\n\n    /// Gets called when the request is started.\n    public var onStart: (@MainActor @Sendable (ImageTask) -> Void)?\n\n    /// Gets called when a progressive image preview is produced.\n    public var onPreview: (@MainActor @Sendable (ImageResponse) -> Void)?\n\n    /// Gets called when the request progress is updated.\n    public var onProgress: (@MainActor @Sendable (ImageTask.Progress) -> Void)?\n\n    /// Gets called when the request finishes successfully.\n    public var onSuccess: (@MainActor @Sendable (ImageResponse) -> Void)?\n\n    /// Gets called when the request fails.\n    public var onFailure: (@MainActor @Sendable (Error) -> Void)?\n\n    /// Gets called when the request is completed.\n    public var onCompletion: (@MainActor @Sendable (Result<ImageResponse, Error>) -> Void)?\n\n    // MARK: Other Options\n\n    /// `true` by default. If disabled, progressive image scans will be ignored.\n    public var isProgressiveImageRenderingEnabled = true\n\n    /// `true` by default. If enabled, the image view will be cleared before the\n    /// new download is started. You can disable it if you want to keep the\n    /// previous content while the new download is in progress.\n    public var isResetEnabled = true\n\n    // MARK: Private\n\n    private var isResetNeeded = false\n\n    // MARK: Initializers\n\n    deinit {\n        imageTask?.cancel()\n    }\n\n    override public init(frame: CGRect) {\n        super.init(frame: frame)\n        didInit()\n    }\n\n    public required init?(coder: NSCoder) {\n        super.init(coder: coder)\n        didInit()\n    }\n\n    private func didInit() {\n        imageView.isHidden = true\n        addSubview(imageView)\n        imageView.pinToSuperview()\n\n        placeholderView = {\n            let view = _PlatformBaseView()\n            let color = _PlatformColor.secondarySystemBackground\n#if os(macOS)\n            view.wantsLayer = true\n            view.layer?.backgroundColor = color.cgColor\n#else\n            view.backgroundColor = color\n#endif\n\n            return view\n        }()\n\n        transition = .fadeIn(duration: 0.33)\n    }\n\n    /// Sets the given URL and immediately starts the download.\n    public var url: URL? {\n        get { request?.url }\n        set { request = newValue.map { ImageRequest(url: $0) } }\n    }\n\n    /// Sets the given request and immediately starts the download.\n    public var request: ImageRequest? {\n        didSet { load(request) }\n    }\n\n    override public func updateConstraints() {\n        super.updateConstraints()\n\n        updatePlaceholderViewConstraints()\n        updateFailureViewConstraints()\n    }\n\n    /// Cancels current request and prepares the view for reuse.\n    public func reset() {\n        cancel()\n\n        imageView.image = nil\n        imageView.isHidden = true\n\n        customImageView?.removeFromSuperview()\n\n        setPlaceholderViewHidden(true)\n        setFailureViewHidden(true)\n\n        isResetNeeded = false\n    }\n\n    /// Cancels current request.\n    public func cancel() {\n        imageTask?.cancel()\n        imageTask = nil\n    }\n\n    // MARK: Loading Images\n\n    /// Loads an image with the given request.\n    private func load(_ request: ImageRequest?) {\n        assert(Thread.isMainThread, \"Must be called from the main thread\")\n\n        cancel()\n\n        if isResetEnabled {\n            reset()\n        } else {\n            isResetNeeded = true\n        }\n\n        guard var request else {\n            handle(result: .failure(ImagePipeline.Error.imageRequestMissing), isSync: true)\n            return\n        }\n\n        if let processors, !processors.isEmpty, request.processors.isEmpty {\n            request.processors = processors\n        }\n        if let priority {\n            request.priority = priority\n        }\n\n        // Quick synchronous memory cache lookup\n        if let image = pipeline.cache[request] {\n            if image.isPreview {\n                display(image, isFromMemory: true) // Display progressive preview\n            } else {\n                let response = ImageResponse(container: image, request: request, cacheType: .memory)\n                handle(result: .success(response), isSync: true)\n                return\n            }\n        }\n\n        setPlaceholderViewHidden(false)\n\n        let task = pipeline.loadImage(\n            with: request,\n            progress: { [weak self] response, completed, total in\n                guard let self else { return }\n                let progress = ImageTask.Progress(completed: completed, total: total)\n                if let response {\n                    self.handle(preview: response)\n                    self.onPreview?(response)\n                } else {\n                    self.onProgress?(progress)\n                }\n            },\n            completion: { [weak self] result in\n                self?.handle(result: result.mapError { $0 }, isSync: false)\n            }\n        )\n        imageTask = task\n        onStart?(task)\n    }\n\n    private func handle(preview: ImageResponse) {\n        guard isProgressiveImageRenderingEnabled else {\n            return\n        }\n        setPlaceholderViewHidden(true)\n        display(preview.container, isFromMemory: false)\n    }\n\n    private func handle(result: Result<ImageResponse, Error>, isSync: Bool) {\n        resetIfNeeded()\n        setPlaceholderViewHidden(true)\n\n        switch result {\n        case let .success(response):\n            display(response.container, isFromMemory: isSync)\n        case .failure:\n            if showPlaceholderOnFailure {\n                setPlaceholderViewHidden(false)\n            } else {\n                setFailureViewHidden(false)\n            }\n        }\n\n        imageTask = nil\n        switch result {\n        case .success(let response): onSuccess?(response)\n        case .failure(let error): onFailure?(error)\n        }\n        onCompletion?(result)\n    }\n\n    private func display(_ container: ImageContainer, isFromMemory: Bool) {\n        resetIfNeeded()\n\n        if let view = makeImageView?(container) {\n            addSubview(view)\n            view.pinToSuperview()\n            customImageView = view\n        } else {\n            imageView.image = container.image\n            imageView.isHidden = false\n        }\n\n        if !isFromMemory, let transition = transition {\n            runTransition(transition, container)\n        }\n    }\n\n    // MARK: Private (Placeholder View)\n\n    private func setPlaceholderViewHidden(_ isHidden: Bool) {\n        placeholderView?.isHidden = isHidden\n    }\n\n    private func setPlaceholderImage(_ placeholderImage: PlatformImage?) {\n        guard let placeholderImage else {\n            placeholderView = nil\n            return\n        }\n        placeholderView = _PlatformImageView(image: placeholderImage)\n    }\n\n    private func setPlaceholderView(_ oldView: _PlatformBaseView?, _ newView: _PlatformBaseView?) {\n        if let oldView {\n            oldView.removeFromSuperview()\n        }\n        if let newView {\n            newView.isHidden = !imageView.isHidden\n            insertSubview(newView, at: 0)\n            setNeedsUpdateConstraints()\n#if os(iOS) || os(tvOS) || os(visionOS)\n            if let spinner = newView as? UIActivityIndicatorView {\n                spinner.startAnimating()\n            }\n#endif\n        }\n    }\n\n    private func updatePlaceholderViewConstraints() {\n        NSLayoutConstraint.deactivate(placeholderViewConstraints)\n        placeholderViewConstraints = placeholderView?.layout(with: placeholderViewPosition) ?? []\n    }\n\n    // MARK: Private (Failure View)\n\n    private func setFailureViewHidden(_ isHidden: Bool) {\n        failureView?.isHidden = isHidden\n    }\n\n    private func setFailureImage(_ failureImage: PlatformImage?) {\n        guard let failureImage else {\n            failureView = nil\n            return\n        }\n        failureView = _PlatformImageView(image: failureImage)\n    }\n\n    private func setFailureView(_ oldView: _PlatformBaseView?, _ newView: _PlatformBaseView?) {\n        if let oldView {\n            oldView.removeFromSuperview()\n        }\n        if let newView {\n            newView.isHidden = true\n            insertSubview(newView, at: 0)\n            setNeedsUpdateConstraints()\n        }\n    }\n\n    private func updateFailureViewConstraints() {\n        NSLayoutConstraint.deactivate(failureViewConstraints)\n        failureViewConstraints = failureView?.layout(with: failureViewPosition) ?? []\n    }\n\n    // MARK: Private (Transitions)\n\n    private func runTransition(_ transition: Transition, _ image: ImageContainer) {\n        switch transition {\n        case .fadeIn(let duration):\n            runFadeInTransition(duration: duration)\n        case .custom(let closure):\n            closure(self, image)\n        }\n    }\n\n#if os(macOS)\n    private func runFadeInTransition(duration: TimeInterval) {\n        guard !imageView.isHidden else { return }\n        imageView.layer?.animateOpacity(duration: duration)\n    }\n#else\n    private func runFadeInTransition(duration: TimeInterval) {\n        guard !imageView.isHidden else { return }\n        imageView.alpha = 0\n        UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction]) {\n            self.imageView.alpha = 1\n        }\n    }\n#endif\n\n    // MARK: Misc\n\n    @frozen public enum SubviewPosition {\n        /// Center in the superview.\n        case center\n\n        /// Fill the superview.\n        case fill\n    }\n\n    private func resetIfNeeded() {\n        if isResetNeeded {\n            reset()\n            isResetNeeded = false\n        }\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/NukeVideo/AVDataAsset.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport AVKit\nimport Foundation\nimport Nuke\n\nextension AssetType {\n    /// Returns `true` if the asset represents a video file.\n    public var isVideo: Bool {\n        self == .mp4 || self == .m4v || self == .mov\n    }\n}\n\n#if !os(watchOS)\n\nprivate extension AssetType {\n    var avFileType: AVFileType? {\n        switch self {\n        case .mp4: return .mp4\n        case .m4v: return .m4v\n        case .mov: return .mov\n        default: return nil\n        }\n    }\n}\n\n// This class keeps strong pointer to DataAssetResourceLoader\nfinal class AVDataAsset: AVURLAsset, @unchecked Sendable {\n    private let resourceLoaderDelegate: DataAssetResourceLoader\n\n    init(data: Data, type: AssetType?) {\n        self.resourceLoaderDelegate = DataAssetResourceLoader(\n            data: data,\n            contentType: type?.avFileType?.rawValue ?? AVFileType.mp4.rawValue\n        )\n\n        // The URL is irrelevant\n        let url = URL(string: \"in-memory-data://\\(UUID().uuidString)\") ?? URL(fileURLWithPath: \"/dev/null\")\n        super.init(url: url, options: nil)\n\n        resourceLoader.setDelegate(resourceLoaderDelegate, queue: .global())\n    }\n}\n\n// This allows LazyImage to play video from memory.\nprivate final class DataAssetResourceLoader: NSObject, AVAssetResourceLoaderDelegate {\n    private let data: Data\n    private let contentType: String\n\n    init(data: Data, contentType: String) {\n        self.data = data\n        self.contentType = contentType\n    }\n\n    // MARK: - DataAssetResourceLoader\n\n    func resourceLoader(\n        _ resourceLoader: AVAssetResourceLoader,\n        shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest\n    ) -> Bool {\n        if let contentRequest = loadingRequest.contentInformationRequest {\n            contentRequest.contentType = contentType\n            contentRequest.contentLength = Int64(data.count)\n            contentRequest.isByteRangeAccessSupported = true\n        }\n\n        if let dataRequest = loadingRequest.dataRequest {\n            if dataRequest.requestsAllDataToEndOfResource {\n                dataRequest.respond(with: data[dataRequest.requestedOffset...])\n            } else {\n                let range = dataRequest.requestedOffset..<(dataRequest.requestedOffset + Int64(dataRequest.requestedLength))\n                dataRequest.respond(with: data[range])\n            }\n        }\n\n        loadingRequest.finishLoading()\n\n        return true\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/NukeVideo/ImageDecoders+Video.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\n#if !os(watchOS) && !os(visionOS)\n\nimport Foundation\nimport AVKit\nimport AVFoundation\nimport Nuke\n\nextension ImageDecoders {\n    /// The video decoder.\n    ///\n    /// To enable the video decoder, register it with a shared registry:\n    ///\n    /// ```swift\n    /// ImageDecoderRegistry.shared.register(ImageDecoders.Video.init)\n    /// ```\n    public final class Video: ImageDecoding, @unchecked Sendable {\n        private var didProducePreview = false\n        private let type: AssetType\n        public var isAsynchronous: Bool { true }\n\n        private let lock = NSLock()\n\n        public init?(context: ImageDecodingContext) {\n            guard let type = AssetType(context.data), type.isVideo else { return nil }\n            self.type = type\n        }\n\n        public func decode(_ data: Data) throws -> ImageContainer {\n            let image = makePreview(for: data, type: type) ?? PlatformImage()\n            return ImageContainer(image: image, type: type, data: data, userInfo: [\n                .videoAssetKey: AVDataAsset(data: data, type: type)\n            ])\n        }\n\n        public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? {\n            lock.lock()\n            defer { lock.unlock() }\n\n            guard let type = AssetType(data), type.isVideo else { return nil }\n            guard !didProducePreview else {\n                return nil // We only need one preview\n            }\n            guard let preview = makePreview(for: data, type: type) else {\n                return nil\n            }\n            didProducePreview = true\n            return ImageContainer(image: preview, type: type, isPreview: true, data: data, userInfo: [\n                .videoAssetKey: AVDataAsset(data: data, type: type)\n            ])\n        }\n    }\n}\n\nextension ImageContainer.UserInfoKey {\n    /// A key for a video asset (`AVAsset`).\n    public static let videoAssetKey: ImageContainer.UserInfoKey = \"com.github/kean/nuke/video-asset\"\n}\n\nprivate func makePreview(for data: Data, type: AssetType) -> PlatformImage? {\n    let asset = AVDataAsset(data: data, type: type)\n    let generator = AVAssetImageGenerator(asset: asset)\n    guard let cgImage = try? generator.copyCGImage(at: CMTime(value: 0, timescale: 1), actualTime: nil) else {\n        return nil\n    }\n    return PlatformImage(cgImage: cgImage)\n}\n\n#endif\n\n#if os(macOS)\nextension NSImage {\n    convenience init(cgImage: CGImage) {\n        self.init(cgImage: cgImage, size: .zero)\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/NukeVideo/VideoPlayerView.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport AVKit\nimport Foundation\n\n#if os(macOS)\npublic typealias _PlatformBaseView = NSView\n#else\npublic typealias _PlatformBaseView = UIView\n#endif\n\n@MainActor\npublic final class VideoPlayerView: _PlatformBaseView {\n    // MARK: Configuration\n\n    /// The video gravity. `.resizeAspectFill` by default.\n    public var videoGravity: AVLayerVideoGravity = .resizeAspectFill {\n        didSet {\n            _playerLayer?.videoGravity = videoGravity\n        }\n    }\n\n    /// `true` by default. If disabled, the video will resize with the frame without animations.\n    public var animatesFrameChanges = true\n\n    /// `true` by default. If disabled, the player will only play the video once.\n    public var isLooping = true {\n        didSet {\n            guard isLooping != oldValue else { return }\n            player?.actionAtItemEnd = isLooping ? .none : .pause\n            if isLooping, !(player?.nowPlaying ?? false) {\n                restart()\n            }\n        }\n    }\n\n    /// A closure called when the video finishes playing.\n    public var onVideoFinished: (() -> Void)?\n\n    // MARK: Initialization\n\n    public var playerLayer: AVPlayerLayer {\n        if let layer = _playerLayer {\n            return layer\n        }\n        let playerLayer = AVPlayerLayer()\n#if os(macOS)\n        wantsLayer = true\n        self.layer?.addSublayer(playerLayer)\n#else\n        self.layer.addSublayer(playerLayer)\n#endif\n        playerLayer.frame = bounds\n        playerLayer.videoGravity = videoGravity\n        _playerLayer = playerLayer\n        return playerLayer\n    }\n\n    private var _playerLayer: AVPlayerLayer?\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n    override public func layoutSubviews() {\n        super.layoutSubviews()\n\n        CATransaction.begin()\n        CATransaction.setDisableActions(!animatesFrameChanges)\n        _playerLayer?.frame = bounds\n        CATransaction.commit()\n    }\n#elseif os(macOS)\n    override public func layout() {\n        super.layout()\n\n        CATransaction.begin()\n        CATransaction.setDisableActions(!animatesFrameChanges)\n        _playerLayer?.frame = bounds\n        CATransaction.commit()\n    }\n#endif\n\n    // MARK: Private\n\n    private var player: AVPlayer? {\n        didSet {\n            unregisterNotifications()\n            if player != nil {\n                registerNotifications()\n            }\n        }\n    }\n\n    private var playerObserver: AnyObject?\n\n    public func reset() {\n        _playerLayer?.player = nil\n        player = nil\n        playerObserver = nil\n    }\n\n    public var asset: AVAsset? {\n        didSet { assetDidChange() }\n    }\n\n    private func assetDidChange() {\n        if asset == nil {\n            reset()\n        }\n    }\n\n    private func unregisterNotifications() {\n        NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)\n#if os(iOS) || os(tvOS) || os(visionOS)\n        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)\n#endif\n    }\n\n    private func registerNotifications() {\n        NotificationCenter.default.addObserver(\n            self,\n            selector: #selector(playerItemDidPlayToEndTimeNotification(_:)),\n            name: .AVPlayerItemDidPlayToEndTime,\n            object: player?.currentItem\n        )\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n        NotificationCenter.default.addObserver(\n            self,\n            selector: #selector(applicationWillEnterForeground),\n            name: UIApplication.willEnterForegroundNotification,\n            object: nil\n        )\n#endif\n    }\n\n    public func restart() {\n        player?.seek(to: CMTime.zero)\n        player?.play()\n    }\n\n    public func play() {\n        guard let asset else {\n            return\n        }\n\n        let playerItem = AVPlayerItem(asset: asset)\n        let player = AVQueuePlayer(playerItem: playerItem)\n        player.isMuted = true\n#if os(visionOS)\n            player.preventsAutomaticBackgroundingDuringVideoPlayback = false\n#else\n            player.preventsDisplaySleepDuringVideoPlayback = false\n#endif\n        player.actionAtItemEnd = isLooping ? .none : .pause\n        self.player = player\n\n        playerLayer.player = player\n\n        playerObserver = player.observe(\\.status, options: [.new, .initial]) { player, _ in\n            Task { @MainActor in\n                if player.status == .readyToPlay {\n                    player.play()\n                }\n            }\n        }\n    }\n\n    @objc private func playerItemDidPlayToEndTimeNotification(_ notification: Notification) {\n        guard let playerItem = notification.object as? AVPlayerItem else {\n            return\n        }\n        if isLooping {\n            playerItem.seek(to: CMTime.zero, completionHandler: nil)\n        } else {\n            onVideoFinished?()\n        }\n    }\n\n    @objc private func applicationWillEnterForeground() {\n        if shouldResumeOnInterruption {\n            player?.play()\n        }\n    }\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n    override public func willMove(toWindow newWindow: UIWindow?) {\n        if newWindow != nil && shouldResumeOnInterruption {\n            player?.play()\n        }\n    }\n#endif\n\n    private var shouldResumeOnInterruption: Bool {\n        return player?.nowPlaying == false &&\n        player?.status == .readyToPlay &&\n        isLooping\n    }\n}\n\n@MainActor\nextension AVPlayer {\n    var nowPlaying: Bool {\n        rate != 0 && error == nil\n    }\n}\n"
  },
  {
    "path": "Tests/Helpers/Helpers.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Nuke\nimport Foundation\nimport CoreGraphics\n\n#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)\nimport UIKit\n#endif\n\n#if os(macOS)\nimport AppKit\n#endif\n\nprivate final class BundleToken {}\n\n// Test data.\nenum Test {\n    static func url(forResource name: String, extension ext: String) -> URL {\n        let bundle = Bundle(for: BundleToken.self)\n        return bundle.url(forResource: name, withExtension: ext)!\n    }\n\n    static func data(name: String, extension ext: String) -> Data {\n        let url = self.url(forResource: name, extension: ext)\n        return try! Data(contentsOf: url)\n    }\n\n    static func image(named name: String) -> PlatformImage {\n        let components = name.split(separator: \".\")\n        return self.image(named: String(components[0]), extension: String(components[1]))\n    }\n\n    static func image(named name: String, extension ext: String) -> PlatformImage {\n        Test.container(named: name, extension: ext).image\n    }\n\n    static func container(named name: String, extension ext: String) -> ImageContainer {\n        let data = Test.data(name: name, extension: ext)\n        return try! ImageDecoders.Default().decode(data)\n    }\n\n    static let url = URL(string: \"http://test.com/example.jpeg\")!\n\n    static let data: Data = Test.data(name: \"fixture\", extension: \"jpeg\")\n\n    // Test.image size is 640 x 480 pixels\n    static var image: PlatformImage {\n        Test.image(named: \"fixture\", extension: \"jpeg\")\n    }\n\n    // Test.image size is 640 x 480 pixels\n    static var container: ImageContainer {\n        ImageContainer(image: image)\n    }\n\n    static var request: ImageRequest {\n        ImageRequest(url: Test.url)\n    }\n\n    static let urlResponse = HTTPURLResponse(\n        url: Test.url,\n        mimeType: \"jpeg\",\n        expectedContentLength: 22_789,\n        textEncodingName: nil\n    )\n\n    static let response = ImageResponse(\n        container: .init(image: Test.image),\n        request: Test.request,\n        urlResponse: urlResponse,\n        cacheType: nil\n    )\n\n    static func save(_ image: PlatformImage) {\n        let url = try! FileManager.default\n            .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)\n            .appendingPathComponent(UUID().uuidString)\n            .appendingPathExtension(\"png\")\n        print(url)\n        let data = ImageEncoders.ImageIO(type: .png, compressionRatio: 1).encode(image)!\n        try! data.write(to: url)\n    }\n}\n\nextension ImageDecodingContext {\n    static var mock: ImageDecodingContext {\n        mock(data: Test.data)\n    }\n\n    static func mock(data: Data) -> ImageDecodingContext {\n        ImageDecodingContext(request: Test.request, data: data)\n    }\n}\n\nextension ImageProcessingContext {\n    static var mock: ImageProcessingContext {\n        ImageProcessingContext(request: Test.request, response: Test.response, isCompleted: true)\n    }\n}\n\n#if os(macOS)\nextension NSImage {\n    var cgImage: CGImage? {\n        cgImage(forProposedRect: nil, context: nil, hints: nil)\n    }\n}\n#endif\n\nextension CGImage {\n    var size: CGSize {\n        CGSize(width: width, height: height)\n    }\n}\n\nextension PlatformImage {\n    var sizeInPixels: CGSize {\n        cgImage!.size\n    }\n}\n\nfunc _groups(regex: String, in text: String) -> [String] {\n    do {\n        let regex = try NSRegularExpression(pattern: regex)\n        let results = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))\n        return results.map {\n            String(text[Range($0.range(at: 1), in: text)!])\n        }\n    } catch let error {\n        print(\"invalid regex: \\(error.localizedDescription)\")\n        return []\n    }\n}\n\n// Supports subranges as well.\nfunc _createChunks(for data: Data, size: Int) -> [Data] {\n    var chunks = [Data]()\n    let endIndex = data.endIndex\n    var offset = data.startIndex\n    while offset < endIndex {\n        let chunkSize = offset + size > endIndex ? endIndex - offset : size\n        let chunk = data[(offset)..<(offset + chunkSize)]\n        offset += chunkSize\n        chunks.append(chunk)\n    }\n    return chunks\n}\n\n// MARK: - Result extension\n\nextension Result {\n    var isSuccess: Bool {\n        return value != nil\n    }\n\n    var isFailure: Bool {\n        return error != nil\n    }\n\n    var value: Success? {\n        switch self {\n        case let .success(value):\n            return value\n        case .failure:\n            return nil\n        }\n    }\n\n    var error: Failure? {\n        switch self {\n        case .success:\n            return nil\n        case let .failure(error):\n            return error\n        }\n    }\n}\n\n@propertyWrapper final class Mutex<T> {\n    private var value: T\n    private let lock: os_unfair_lock_t\n\n    init(wrappedValue value: T) {\n        self.value = value\n        self.lock = .allocate(capacity: 1)\n        self.lock.initialize(to: os_unfair_lock())\n    }\n\n    deinit {\n        lock.deinitialize(count: 1)\n        lock.deallocate()\n    }\n\n    var wrappedValue: T {\n        get { getValue() }\n        set { setValue(newValue) }\n    }\n\n    var projectedValue: Mutex<T> {\n        self\n    }\n\n    func withLock<U>(_ closure: (inout T) -> U) -> U {\n        os_unfair_lock_lock(lock)\n        defer { os_unfair_lock_unlock(lock) }\n        return closure(&value)\n    }\n\n    private func getValue() -> T {\n        os_unfair_lock_lock(lock)\n        defer { os_unfair_lock_unlock(lock) }\n        return value\n    }\n\n    private func setValue(_ newValue: T) {\n        os_unfair_lock_lock(lock)\n        defer { os_unfair_lock_unlock(lock) }\n        value = newValue\n    }\n}\n"
  },
  {
    "path": "Tests/Helpers/MockURLProtocol.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\n// MARK: - URLProtocol Mock\n\n/// A custom URL scheme–based mock that intercepts requests to `mock://` URLs.\n/// Each test registers per-URL handlers before loading.\nfinal class MockURLProtocol: URLProtocol, @unchecked Sendable {\n    /// Per-URL request handler: receives the request, calls methods on\n    /// the protocol client (`startLoading` bridge), and signals completion.\n    nonisolated(unsafe) static var handlers = [URL: Handler]()\n\n    struct Handler {\n        /// Called on the URL loading thread. Use `client` to send response, data, and completion.\n        let handle: (_ request: URLRequest, _ client: any URLProtocolClient, _ protocol: URLProtocol) -> Void\n    }\n\n    override class func canInit(with request: URLRequest) -> Bool {\n        request.url?.scheme == \"mock\"\n    }\n\n    override class func canonicalRequest(for request: URLRequest) -> URLRequest {\n        request\n    }\n\n    override func startLoading() {\n        guard let url = request.url, let handler = MockURLProtocol.handlers[url] else {\n            client?.urlProtocol(self, didFailWithError: URLError(.unsupportedURL))\n            return\n        }\n        handler.handle(request, client!, self)\n    }\n\n    override func stopLoading() {}\n}\n\n// MARK: - Helpers\n\n/// Creates a `DataLoader` configured to use `MockURLProtocol`.\nfunc makeDataLoader(\n    validate: @Sendable @escaping (URLResponse) -> Error? = DataLoader.validate\n) -> DataLoader {\n    let config = URLSessionConfiguration.ephemeral\n    config.protocolClasses = [MockURLProtocol.self]\n    return DataLoader(configuration: config, validate: validate)\n}\n\nfunc mockURL(_ path: String = \"image.jpg\") -> URL {\n    URL(string: \"mock://test/\\(path)\")!\n}\n\n/// Registers a handler that responds with the given status code, headers, and\n/// data chunks (each chunk delivered as a separate `didLoad` call).\nfunc registerMock(\n    url: URL,\n    statusCode: Int = 200,\n    headers: [String: String]? = nil,\n    chunks: [Data]\n) {\n    var allHeaders = headers ?? [:]\n    let totalSize = chunks.reduce(0) { $0 + $1.count }\n    if allHeaders[\"Content-Length\"] == nil {\n        allHeaders[\"Content-Length\"] = \"\\(totalSize)\"\n    }\n    MockURLProtocol.handlers[url] = .init { request, client, proto in\n        let response = HTTPURLResponse(\n            url: url,\n            statusCode: statusCode,\n            httpVersion: \"HTTP/1.1\",\n            headerFields: allHeaders\n        )!\n        client.urlProtocol(proto, didReceive: response, cacheStoragePolicy: .notAllowed)\n        for chunk in chunks {\n            client.urlProtocol(proto, didLoad: chunk)\n        }\n        client.urlProtocolDidFinishLoading(proto)\n    }\n}\n\n/// Registers a handler that fails with the given error (before sending a response).\nfunc registerMockError(url: URL, error: URLError) {\n    MockURLProtocol.handlers[url] = .init { _, client, proto in\n        client.urlProtocol(proto, didFailWithError: error)\n    }\n}\n\n/// Registers a handler that sends a response, some data, then fails mid-stream.\nfunc registerMockPartialFailure(\n    url: URL,\n    statusCode: Int = 200,\n    data: Data,\n    error: URLError\n) {\n    MockURLProtocol.handlers[url] = .init { _, client, proto in\n        let response = HTTPURLResponse(\n            url: url,\n            statusCode: statusCode,\n            httpVersion: \"HTTP/1.1\",\n            headerFields: [\"Content-Length\": \"\\(data.count * 2)\"] // Pretend more data expected\n        )!\n        client.urlProtocol(proto, didReceive: response, cacheStoragePolicy: .notAllowed)\n        client.urlProtocol(proto, didLoad: data)\n        client.urlProtocol(proto, didFailWithError: error)\n    }\n}\n"
  },
  {
    "path": "Tests/Helpers/NukeExtensions.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\nextension ImagePipeline.Error: @retroactive Equatable {\n    public static func == (lhs: ImagePipeline.Error, rhs: ImagePipeline.Error) -> Bool {\n        switch (lhs, rhs) {\n        case (.dataMissingInCache, .dataMissingInCache),\n             (.dataIsEmpty, .dataIsEmpty),\n             (.imageRequestMissing, .imageRequestMissing),\n             (.pipelineInvalidated, .pipelineInvalidated),\n             (.dataDownloadExceededMaximumSize, .dataDownloadExceededMaximumSize),\n             (.cancelled, .cancelled),\n             (.decoderNotRegistered, .decoderNotRegistered),\n             (.decodingFailed, .decodingFailed),\n             (.processingFailed, .processingFailed):\n            return true\n        case let (.dataLoadingFailed(lhs), .dataLoadingFailed(rhs)):\n            return lhs as NSError == rhs as NSError\n        default:\n            return false\n        }\n    }\n}\n\nextension ImageTask.Event: @retroactive Equatable {\n    public static func == (lhs: ImageTask.Event, rhs: ImageTask.Event) -> Bool {\n        switch (lhs, rhs) {\n        case let (.progress(lhs), .progress(rhs)):\n            return lhs == rhs\n        case let (.preview(lhs), .preview(rhs)):\n            return lhs == rhs\n        case let (.finished(lhs), .finished(rhs)):\n            return lhs == rhs\n        default:\n            return false\n        }\n    }\n}\n\nextension ImageResponse: @retroactive Equatable {\n    public static func == (lhs: ImageResponse, rhs: ImageResponse) -> Bool {\n        return lhs.image === rhs.image\n    }\n}\n\nextension ImageRequest {\n    func with(_ configure: (inout ImageRequest) -> Void) -> ImageRequest {\n        var copy = self\n        configure(&copy)\n        return copy\n    }\n}\n\nextension ImagePipeline {\n    nonisolated func reconfigured(_ configure: (inout ImagePipeline.Configuration) -> Void) -> ImagePipeline {\n        var configuration = self.configuration\n        configure(&configuration)\n        return ImagePipeline(configuration: configuration)\n    }\n}\n\nextension ImageProcessing {\n    /// A throwing version of a regular method.\n    func processThrowing(_ image: PlatformImage) throws -> PlatformImage {\n        let context = ImageProcessingContext(request: Test.request, response: Test.response, isCompleted: true)\n        return (try process(ImageContainer(image: image), context: context)).image\n    }\n}\n\nextension ImageCaching {\n    subscript(request: ImageRequest) -> ImageContainer? {\n        get { self[ImageCacheKey(request: request)] }\n        set { self[ImageCacheKey(request: request)] = newValue }\n    }\n}\n\n#if os(macOS)\nimport Cocoa\ntypealias _ImageView = NSImageView\n#elseif os(iOS) || os(tvOS) || os(visionOS)\nimport UIKit\ntypealias _ImageView = UIImageView\n#endif\n"
  },
  {
    "path": "Tests/Helpers/TestExpectation.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Testing\n@testable import Nuke\n\nfinal class TestExpectation: @unchecked Sendable {\n    private let lock = NSLock()\n    private var state: State = .idle\n    fileprivate var recorder: AnyObject?\n\n    private enum State {\n        case idle\n        case fulfilled\n        case cancelled\n        case awaiting(CheckedContinuation<Bool, Never>)\n    }\n\n    init() {}\n\n    func fulfill() {\n        let continuation = lock.withLock { () -> CheckedContinuation<Bool, Never>? in\n            switch state {\n            case .idle:\n                state = .fulfilled\n                return nil\n            case .awaiting(let continuation):\n                state = .fulfilled\n                return continuation\n            case .fulfilled, .cancelled:\n                return nil\n            }\n        }\n        continuation?.resume(returning: true)\n    }\n\n    func wait(timeout: Duration = .seconds(60)) async {\n        let fulfilled = await withTaskGroup(of: Bool.self) { group in\n            group.addTask {\n                await self.waitInternal()\n            }\n            group.addTask {\n                try? await Task.sleep(for: timeout)\n                return false\n            }\n            let result = await group.next()\n            group.cancelAll()\n            return result ?? false\n        }\n        if !fulfilled, !Task.isCancelled {\n            Issue.record(\"TestExpectation timed out after \\(timeout)\")\n        }\n    }\n\n    // Returns true if genuinely fulfilled, false if cancelled.\n    private func waitInternal() async -> Bool {\n        await withTaskCancellationHandler {\n            await withCheckedContinuation { continuation in\n                let result = lock.withLock { () -> Bool? in\n                    switch state {\n                    case .idle:\n                        state = .awaiting(continuation)\n                        return nil\n                    case .fulfilled:\n                        return true\n                    case .cancelled:\n                        return false\n                    case .awaiting:\n                        preconditionFailure(\"wait() called multiple times\")\n                    }\n                }\n                if let result {\n                    continuation.resume(returning: result)\n                }\n            }\n        } onCancel: {\n            let continuation = lock.withLock { () -> CheckedContinuation<Bool, Never>? in\n                switch state {\n                case .idle:\n                    state = .cancelled  // inner block hasn't run yet; it will see .cancelled and resume\n                    return nil\n                case .awaiting(let c):\n                    state = .cancelled\n                    return c\n                case .fulfilled, .cancelled:\n                    return nil\n                }\n            }\n            continuation?.resume(returning: false)\n        }\n    }\n}\n\nextension TestExpectation {\n    convenience init(notification name: Notification.Name, object: AnyObject? = nil) {\n        self.init()\n        let ref = TokenRef()\n        ref.token = NotificationCenter.default.addObserver(forName: name, object: object, queue: nil) { [weak self] _ in\n            if let token = ref.token { NotificationCenter.default.removeObserver(token) }\n            self?.fulfill()\n        }\n    }\n\n    /// Creates a test expectation that waits for a given number of operations\n    /// to be enqueued on the given `TaskQueue`.\n    @ImagePipelineActor convenience init(queue: TaskQueue, count: Int) {\n        self.init()\n        let recorder = TaskQueueOperationRecorder()\n        self.recorder = recorder\n        queue.onEvent = { [weak self] event in\n            if case .enqueued(let op) = event {\n                recorder.record(op)\n                if recorder.operations.count >= count {\n                    self?.fulfill()\n                }\n            }\n        }\n    }\n\n    var operations: [TaskQueue.Operation] {\n        (recorder as? TaskQueueOperationRecorder)?.operations ?? []\n    }\n}\n\nprivate final class TaskQueueOperationRecorder: @unchecked Sendable {\n    private let lock = NSLock()\n    private var _operations = [TaskQueue.Operation]()\n\n    var operations: [TaskQueue.Operation] {\n        lock.withLock { _operations }\n    }\n\n    func record(_ operation: TaskQueue.Operation) {\n        lock.withLock {\n            _operations.append(operation)\n        }\n    }\n}\n\nprivate final class TokenRef: @unchecked Sendable {\n    var token: NSObjectProtocol?\n}\n\nfunc notification(_ name: Notification.Name, object: AnyObject? = nil, isolation: isolated (any Actor)? = #isolation, while action: () -> Void = {}) async {\n    let expectation = TestExpectation(notification: name, object: object)\n    action()\n    await expectation.wait()\n}\n\n// MARK: - TaskQueue Helpers\n\nextension TaskQueue {\n    var operationCount: Int { pendingCount + runningCount }\n\n    /// Waits for the specified number of operations to be enqueued.\n    func waitForOperations(count: Int, while action: () -> Void) async -> [TaskQueue.Operation] {\n        let expectation = TestExpectation(queue: self, count: count)\n        action()\n        await expectation.wait()\n        return expectation.operations\n    }\n\n    /// Waits for a priority change on a TaskQueue.Operation managed by this queue.\n    func waitForPriorityChange(of operation: TaskQueue.Operation, to target: TaskPriority = .high, while action: () -> Void) async {\n        if operation.priority == target { action(); return }\n        let expectation = TestExpectation()\n        let previous = onEvent\n        onEvent = { event in\n            previous?(event)\n            if case .priorityChanged(let op) = event, op === operation, op.priority == target {\n                expectation.fulfill()\n            }\n        }\n        action()\n        await expectation.wait()\n        onEvent = previous\n    }\n\n    /// Waits for an operation managed by this queue to be cancelled.\n    func waitForCancellation(of operation: TaskQueue.Operation, while action: () -> Void) async {\n        if operation.isCancelled { action(); return }\n        let expectation = TestExpectation()\n        let previous = onEvent\n        onEvent = { event in\n            previous?(event)\n            if case .cancelled(let op) = event, op === operation {\n                expectation.fulfill()\n            }\n        }\n        action()\n        await expectation.wait()\n        onEvent = previous\n    }\n}\n\n/// Waits for a priority change on a standalone TaskQueue.Operation (not in a queue).\n@ImagePipelineActor\nfunc waitForPriorityChange(of operation: TaskQueue.Operation, to target: TaskPriority = .high, while action: () -> Void) async {\n    if operation.priority == target { action(); return }\n    let expectation = TestExpectation()\n    operation.onPriorityChanged = { priority in\n        if priority == target { expectation.fulfill() }\n    }\n    action()\n    await expectation.wait()\n    operation.onPriorityChanged = nil\n}\n\n/// Waits for a standalone TaskQueue.Operation to be cancelled (not in a queue).\n@ImagePipelineActor\nfunc waitForCancellation(of operation: TaskQueue.Operation, while action: () -> Void) async {\n    if operation.isCancelled { action(); return }\n    let expectation = TestExpectation()\n    operation.onCancelled = { expectation.fulfill() }\n    action()\n    await expectation.wait()\n    operation.onCancelled = nil\n}\n\n/// A simple mutable reference wrapper for use in test closures.\nfinal class Ref<T>: @unchecked Sendable {\n    var value: T\n    init(_ value: T) { self.value = value }\n}\n\nextension TaskQueue {\n    /// Waits until all enqueued operations have finished executing.\n    /// Modeled after `OperationQueue.waitUntilAllOperationsAreFinished()`.\n    func waitUntilAllOperationsAreFinished() async {\n        guard operationCount > 0 else { return }\n        let expectation = TestExpectation()\n        let previous = onEvent\n        onEvent = { [weak self] event in\n            previous?(event)\n            if case .finished = event, let self, self.operationCount == 0 {\n                expectation.fulfill()\n            }\n        }\n        await expectation.wait()\n        onEvent = previous\n    }\n}\n\n/// Passively records operations enqueued on a TaskQueue.\n/// Use only when you need to observe operations during execution without waiting.\n@ImagePipelineActor\nfinal class TaskQueueObserver: Sendable {\n    private(set) var operations = [TaskQueue.Operation]()\n\n    init(queue: TaskQueue) {\n        queue.onEvent = { [weak self] event in\n            if case .enqueued(let op) = event {\n                self?.operations.append(op)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/Helpers/TestHelpers.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport CoreGraphics\n@testable import Nuke\n\n/// Suspends data loading, executes the body to register pipeline tasks,\n/// waits for all tasks to start, then resumes data loading.\n@discardableResult\nfunc withSuspendedDataLoading<T>(\n    for pipeline: ImagePipeline,\n    expectedCount: Int,\n    _ body: @Sendable () -> T\n) async -> T {\n    let dataLoader = pipeline.configuration.dataLoader as! MockDataLoader\n    dataLoader.isSuspended = true\n    let expectation = TestExpectation()\n    var count = 0\n    let lock = NSLock()\n    pipeline.onTaskStarted = { _ in\n        lock.lock()\n        count += 1\n        let done = count == expectedCount\n        lock.unlock()\n        if done { expectation.fulfill() }\n    }\n    let result = body()\n    await expectation.wait()\n    pipeline.onTaskStarted = nil\n    dataLoader.isSuspended = false\n    return result\n}\n\n// MARK: - Image Comparison\n\nfunc isEqualImages(_ lhs: PlatformImage, _ rhs: PlatformImage) -> Bool {\n    guard lhs.sizeInPixels == rhs.sizeInPixels else {\n        return false\n    }\n    guard let lhsData = bitmapData(for: lhs),\n          let rhsData = bitmapData(for: rhs) else {\n        return false\n    }\n    return lhsData == rhsData\n}\n\nprivate func bitmapData(for image: PlatformImage) -> Data? {\n    guard let cgImage = image.cgImage else { return nil }\n    let width = cgImage.width\n    let height = cgImage.height\n    let bytesPerRow = width * 4\n    var data = Data(count: height * bytesPerRow)\n    guard let context = data.withUnsafeMutableBytes({ ptr -> CGContext? in\n        CGContext(\n            data: ptr.baseAddress,\n            width: width,\n            height: height,\n            bitsPerComponent: 8,\n            bytesPerRow: bytesPerRow,\n            space: CGColorSpaceCreateDeviceRGB(),\n            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue\n        )\n    }) else { return nil }\n    context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))\n    return data\n}\n"
  },
  {
    "path": "Tests/Host/AppDelegate.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport UIKit\n\n@main\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n    var window: UIWindow?\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {\n        window = UIWindow()\n        window?.rootViewController = UIViewController()\n        window?.makeKeyAndVisible()\n        return true\n    }\n}\n"
  },
  {
    "path": "Tests/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>en</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>BNDL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>1</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "Tests/Mocks/ImagePipelineObserver.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\nfinal class ImagePipelineObserver: ImagePipeline.Delegate, @unchecked Sendable {\n    var startedTaskCount = 0\n    var cancelledTaskCount = 0\n    var completedTaskCount = 0\n\n    static let didStartTask = Notification.Name(\"com.github.kean.Nuke.Tests.ImagePipelineObserver.DidStartTask\")\n    static let didCancelTask = Notification.Name(\"com.github.kean.Nuke.Tests.ImagePipelineObserver.DidCancelTask\")\n    static let didCompleteTask = Notification.Name(\"com.github.kean.Nuke.Tests.ImagePipelineObserver.DidFinishTask\")\n\n    static let taskKey = \"taskKey\"\n    static let resultKey = \"resultKey\"\n\n    var events = [ImageTaskEvent]()\n\n    var onTaskCreated: ((ImageTask) -> Void)?\n\n    private let lock = NSLock()\n\n    private func append(_ event: ImageTaskEvent) {\n        lock.lock()\n        events.append(event)\n        lock.unlock()\n    }\n\n    func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {\n        onTaskCreated?(task)\n        append(.created)\n    }\n\n    func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) {\n        switch event {\n        case .started:\n            startedTaskCount += 1\n            append(.started)\n            NotificationCenter.default.post(name: ImagePipelineObserver.didStartTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task])\n        case .progress(let progress):\n            append(.progressUpdated(completedUnitCount: progress.completed, totalUnitCount: progress.total))\n        case .preview(let response):\n            append(.intermediateResponseReceived(response: response))\n        case .finished(let result):\n            if case .failure(.cancelled) = result {\n                cancelledTaskCount += 1\n                append(.cancelled)\n                NotificationCenter.default.post(name: ImagePipelineObserver.didCancelTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task])\n            } else {\n                completedTaskCount += 1\n                append(.completed(result: result))\n                NotificationCenter.default.post(name: ImagePipelineObserver.didCompleteTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task, ImagePipelineObserver.resultKey: result])\n            }\n        }\n    }\n}\n\nenum ImageTaskEvent: Equatable {\n    case created\n    case started\n    case cancelled\n    case intermediateResponseReceived(response: ImageResponse)\n    case progressUpdated(completedUnitCount: Int64, totalUnitCount: Int64)\n    case completed(result: Result<ImageResponse, ImagePipeline.Error>)\n\n    static func == (lhs: ImageTaskEvent, rhs: ImageTaskEvent) -> Bool {\n        switch (lhs, rhs) {\n        case (.created, .created): return true\n        case (.started, .started): return true\n        case (.cancelled, .cancelled): return true\n        case let (.intermediateResponseReceived(lhs), .intermediateResponseReceived(rhs)): return lhs == rhs\n        case let (.progressUpdated(lhsTotal, lhsCompleted), .progressUpdated(rhsTotal, rhsCompleted)):\n            return (lhsTotal, lhsCompleted) == (rhsTotal, rhsCompleted)\n        case let (.completed(lhs), .completed(rhs)): return lhs == rhs\n        default: return false\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/Mocks/MockDataCache.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\nfinal class MockDataCache: DataCaching, @unchecked Sendable {\n    var store = [String: Data]()\n    var readCount = 0\n    var writeCount = 0\n\n    func resetCounters() {\n        readCount = 0\n        writeCount = 0\n    }\n\n    func cachedData(for key: String) -> Data? {\n        readCount += 1\n        return store[key]\n    }\n\n    func containsData(for key: String) -> Bool {\n        store[key] != nil\n    }\n\n    func storeData(_ data: Data, for key: String) {\n        writeCount += 1\n        store[key] = data\n    }\n\n    func removeData(for key: String) {\n        store[key] = nil\n    }\n\n    func removeAll() {\n        store.removeAll()\n    }\n}\n"
  },
  {
    "path": "Tests/Mocks/MockDataLoader.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\nprivate let data: Data = Test.data(name: \"fixture\", extension: \"jpeg\")\n\nclass MockDataLoader: DataLoading, @unchecked Sendable {\n    static let DidStartTask = Notification.Name(\"com.github.kean.Nuke.Tests.MockDataLoader.DidStartTask\")\n    static let DidCancelTask = Notification.Name(\"com.github.kean.Nuke.Tests.MockDataLoader.DidCancelTask\")\n\n    @Mutex var createdTaskCount = 0\n    var results = [URL: Result<(Data, URLResponse), NSError>]()\n    let queue = Gate()\n    var isSuspended: Bool {\n        get { queue.isSuspended }\n        set { queue.isSuspended = newValue }\n    }\n\n    // - warning: these get executed in a background now\n    func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse) {\n        let response: URLResponse\n        if let result = results[request.url!] {\n            switch result {\n            case let .success(val): response = val.1\n            case .failure: response = URLResponse(url: request.url ?? Test.url, mimeType: nil, expectedContentLength: -1, textEncodingName: nil)\n            }\n        } else {\n            response = URLResponse(url: request.url ?? Test.url, mimeType: \"jpeg\", expectedContentLength: 22789, textEncodingName: nil)\n        }\n\n        let stream = AsyncThrowingStream<Data, Error> { continuation in\n            continuation.onTermination = { @Sendable termination in\n                if case .cancelled = termination {\n                    NotificationCenter.default.post(name: MockDataLoader.DidCancelTask, object: self)\n                }\n            }\n            Task {\n                await self.queue.wait()\n                if let result = self.results[request.url!] {\n                    switch result {\n                    case let .success(val):\n                        let data = val.0\n                        if !data.isEmpty {\n                            continuation.yield(data.prefix(data.count / 2))\n                            continuation.yield(data.suffix(data.count / 2))\n                        }\n                        continuation.finish()\n                    case let .failure(err):\n                        continuation.finish(throwing: err)\n                    }\n                } else {\n                    continuation.yield(data)\n                    continuation.finish()\n                }\n            }\n        }\n\n        if Task.isCancelled {\n            NotificationCenter.default.post(name: MockDataLoader.DidCancelTask, object: self)\n            throw CancellationError()\n        }\n\n        // - warning: Important so it runs atomically\n        $createdTaskCount.withLock { $0 = $0 + 1 }\n        NotificationCenter.default.post(name: MockDataLoader.DidStartTask, object: self)\n\n        return (stream, response)\n    }\n}\n\n/// A Swift-concurrency-native suspension gate. Replaces OperationQueue to avoid\n/// starving GCD's thread pool when many concurrent async tests are running.\nfinal class Gate: @unchecked Sendable {\n    private let lock = NSLock()\n    private var _isSuspended = false\n    private var waiters: [UUID: CheckedContinuation<Void, Never>] = [:]\n\n    var isSuspended: Bool {\n        get { lock.withLock { _isSuspended } }\n        set {\n            let toResume = lock.withLock { () -> [CheckedContinuation<Void, Never>] in\n                _isSuspended = newValue\n                guard !newValue else { return [] }\n                defer { waiters.removeAll() }\n                return Array(waiters.values)\n            }\n            for c in toResume { c.resume() }\n        }\n    }\n\n    func wait() async {\n        let id = UUID()\n        await withTaskCancellationHandler {\n            await withCheckedContinuation { continuation in\n                lock.withLock {\n                    if _isSuspended {\n                        waiters[id] = continuation\n                    } else {\n                        continuation.resume()\n                    }\n                }\n            }\n        } onCancel: {\n            lock.withLock { waiters.removeValue(forKey: id) }?.resume()\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/Mocks/MockImageCache.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\nclass MockImageCache: ImageCaching, @unchecked Sendable {\n    private let lock = NSLock()\n    var enabled = true\n    var images = [AnyHashable: ImageContainer]()\n    var readCount = 0\n    var writeCount = 0\n\n    init() {}\n\n    func resetCounters() {\n        readCount = 0\n        writeCount = 0\n    }\n\n    subscript(key: ImageCacheKey) -> ImageContainer? {\n        get {\n            lock.withLock {\n                readCount += 1\n                return enabled ? images[key] : nil\n            }\n        }\n        set {\n            lock.withLock {\n                writeCount += 1\n                if let image = newValue {\n                    if enabled { images[key] = image }\n                } else {\n                    images[key] = nil\n                }\n            }\n        }\n    }\n\n    func removeAll() {\n        images.removeAll()\n    }\n}\n"
  },
  {
    "path": "Tests/Mocks/MockImageDecoder.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\nclass MockFailingDecoder: Nuke.ImageDecoding, @unchecked Sendable {\n    func decode(_ data: Data) throws -> ImageContainer {\n        throw MockError(description: \"decoder-failed\")\n    }\n}\n\nclass MockImageDecoder: ImageDecoding, @unchecked Sendable {\n    private let decoder = ImageDecoders.Default()\n\n    let name: String\n\n    init(name: String) {\n        self.name = name\n    }\n\n    func decode(_ data: Data) throws -> ImageContainer {\n        try decoder.decode(data)\n    }\n\n    func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? {\n        decoder.decodePartiallyDownloadedData(data)\n    }\n}\n\nclass MockAnonymousImageDecoder: ImageDecoding, @unchecked Sendable {\n    let closure: (Data, Bool) -> PlatformImage?\n\n    init(_ closure: @escaping (Data, Bool) -> PlatformImage?) {\n        self.closure = closure\n    }\n\n    convenience init(output: PlatformImage) {\n        self.init { _, _ in output }\n    }\n\n    func decode(_ data: Data) throws -> ImageContainer {\n        guard let image = closure(data, true) else {\n            throw ImageDecodingError.unknown\n        }\n        return ImageContainer(image: image)\n    }\n\n    func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? {\n        closure(data, false).map { ImageContainer(image: $0) }\n    }\n}\n"
  },
  {
    "path": "Tests/Mocks/MockImageEncoder.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\nfinal class MockImageEncoder: ImageEncoding, @unchecked Sendable {\n    let result: Data?\n    var encodeCount = 0\n\n    init(result: Data?) {\n        self.result = result\n    }\n\n    func encode(_ image: PlatformImage) -> Data? {\n        encodeCount += 1\n        return result\n    }\n}\n"
  },
  {
    "path": "Tests/Mocks/MockImageProcessor.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\nextension PlatformImage {\n    var nk_test_processorIDs: [String] {\n        get {\n            return (objc_getAssociatedObject(self, AssociatedKeys.processorId) as? [String]) ?? [String]()\n        }\n        set {\n            objc_setAssociatedObject(self, AssociatedKeys.processorId, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)\n        }\n    }\n}\n\nprivate enum AssociatedKeys {\n    // Safe because it's never mutated.\n    nonisolated(unsafe) static let processorId = malloc(1)!\n}\n\n// MARK: - MockImageProcessor\n\nfinal class MockImageProcessor: ImageProcessing, CustomStringConvertible {\n    let identifier: String\n\n    init(id: String) {\n        self.identifier = id\n    }\n\n    func process(_ image: PlatformImage) -> PlatformImage? {\n        var processorIDs: [String] = image.nk_test_processorIDs\n#if os(macOS)\n        let processedImage = image.copy() as! PlatformImage\n#else\n        guard let copy = image.cgImage?.copy() else {\n            return image\n        }\n        let processedImage = PlatformImage(cgImage: copy)\n#endif\n        processorIDs.append(identifier)\n        processedImage.nk_test_processorIDs = processorIDs\n        return processedImage\n    }\n\n    var description: String {\n        \"MockImageProcessor(id: \\(identifier))\"\n    }\n}\n\n// MARK: - MockFailingProcessor\n\nfinal class MockFailingProcessor: ImageProcessing {\n    func process(_ image: PlatformImage) -> PlatformImage? {\n        nil\n    }\n\n    var identifier: String {\n        \"MockFailingProcessor\"\n    }\n}\n\nstruct MockError: Error, Equatable {\n    let description: String\n}\n\n// MARK: - MockEmptyImageProcessor\n\nfinal class MockEmptyImageProcessor: ImageProcessing {\n    let identifier = \"MockEmptyImageProcessor\"\n\n    func process(_ image: PlatformImage) -> PlatformImage? {\n        image\n    }\n\n    static func == (lhs: MockEmptyImageProcessor, rhs: MockEmptyImageProcessor) -> Bool {\n        true\n    }\n}\n\n// MARK: - MockProcessorFactory\n\n/// Counts number of applied processors\nfinal class MockProcessorFactory: @unchecked Sendable {\n    var numberOfProcessorsApplied: Int = 0\n    let lock = NSLock()\n\n    private final class Processor: ImageProcessing, @unchecked Sendable {\n        var identifier: String { processor.identifier }\n        var factory: MockProcessorFactory!\n        let processor: MockImageProcessor\n\n        init(id: String) {\n            self.processor = MockImageProcessor(id: id)\n        }\n\n        func process(_ image: PlatformImage) -> PlatformImage? {\n            factory.lock.lock()\n            factory.numberOfProcessorsApplied += 1\n            factory.lock.unlock()\n            return processor.process(image)\n        }\n    }\n\n    func make(id: String) -> any ImageProcessing {\n        let processor = Processor(id: id)\n        processor.factory = self\n        return processor\n    }\n}\n"
  },
  {
    "path": "Tests/Mocks/MockProgressiveDataLoader.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\n// One-shot data loader that servers data split into chunks, only send one chunk\n// per one `resume()` call.\nfinal class MockProgressiveDataLoader: DataLoading, @unchecked Sendable {\n    let urlResponse: HTTPURLResponse\n    var chunks: [Data]\n    let data = Test.data(name: \"progressive\", extension: \"jpeg\")\n\n    private var streamContinuation: AsyncThrowingStream<Data, Error>.Continuation?\n\n    init() {\n        self.urlResponse = HTTPURLResponse(url: Test.url, statusCode: 200, httpVersion: \"HTTP/1.1\", headerFields: [\"Content-Length\": \"\\(data.count)\"])!\n        self.chunks = Array(_createChunks(for: data, size: data.count / 3))\n    }\n\n    func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse) {\n        let stream = AsyncThrowingStream<Data, Error> { continuation in\n            self.streamContinuation = continuation\n        }\n        // Serve the first chunk immediately\n        DispatchQueue.main.async {\n            self.serveNextChunk()\n        }\n        return (stream, urlResponse)\n    }\n\n    func resumeServingChunks(_ count: Int) {\n        for _ in 0..<count {\n            serveNextChunk()\n        }\n    }\n\n    func serveNextChunk() {\n        guard let chunk = chunks.first else { return }\n        chunks.removeFirst()\n        streamContinuation?.yield(chunk)\n        if chunks.isEmpty {\n            streamContinuation?.finish()\n        }\n    }\n\n    // Serves the next chunk.\n    func resume(_ completed: @escaping @Sendable () -> Void = {}) {\n        DispatchQueue.main.async {\n            if let chunk = self.chunks.first {\n                self.chunks.removeFirst()\n                self.streamContinuation?.yield(chunk)\n                if self.chunks.isEmpty {\n                    self.streamContinuation?.finish()\n                    completed()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/NukeExtensionsTests/ImageViewExtensionsProgressiveDecodingTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n@testable import NukeExtensions\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineProgressiveDecodingTests {\n    let dataLoader: MockProgressiveDataLoader\n    let pipeline: ImagePipeline\n    let cache: MockImageCache\n\n    init() async {\n        let dataLoader = MockProgressiveDataLoader()\n        let cache = MockImageCache()\n        self.dataLoader = dataLoader\n        self.cache = cache\n\n        await ResumableDataStorage.shared.removeAllResponses()\n\n        // We make two important assumptions with this setup:\n        //\n        // 1. Image processing is serial which means that all partial images are\n        // going to be processed and sent to the client before the final image is\n        // processed. So there's never going to be a situation where the final\n        // image is processed before one of the partial images.\n        //\n        // 2. Each data chunk produced by a data loader always results in a new\n        // scan. The way we split the data guarantees that.\n\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = cache\n            $0.isProgressiveDecodingEnabled = true\n            $0.progressiveDecodingInterval = 0\n            $0.isStoringPreviewsInMemoryCache = true\n            $0.imageProcessingQueue.maxConcurrentOperationCount = 1\n        }\n    }\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n    @Test @MainActor func paritalImagesAreDisplayed() async {\n        // Given\n        let imageView = _ImageView()\n\n        var options = ImageLoadingOptions()\n        options.pipeline = pipeline\n\n        let expectPartialImageProduced = TestExpectation()\n        nonisolated(unsafe) var partialCount = 0\n\n        let expectedFinalLoaded = TestExpectation()\n\n        // When/Then\n        NukeExtensions.loadImage(\n            with: Test.request,\n            options: options,\n            into: imageView,\n            progress: { response, _, _ in\n                if let image = response?.image {\n                    #expect(imageView.image === image)\n                    partialCount += 1\n                    // We expect two partial images (at 5 scans, and 9 scans marks).\n                    if partialCount == 2 {\n                        expectPartialImageProduced.fulfill()\n                    }\n                    self.dataLoader.resume()\n                }\n            },\n            completion: { result in\n                #expect(imageView.image === result.value?.image)\n                expectedFinalLoaded.fulfill()\n            }\n        )\n        await expectPartialImageProduced.wait()\n        await expectedFinalLoaded.wait()\n    }\n\n    @Test @MainActor func disablingProgressiveRendering() async {\n        // Given\n        let imageView = _ImageView()\n\n        var options = ImageLoadingOptions()\n        options.pipeline = pipeline\n        options.isProgressiveRenderingEnabled = false\n\n        let expectPartialImageProduced = TestExpectation()\n        nonisolated(unsafe) var partialCount = 0\n\n        let expectedFinalLoaded = TestExpectation()\n\n        // When/Then\n        NukeExtensions.loadImage(\n            with: Test.request,\n            options: options,\n            into: imageView,\n            progress: { response, _, _ in\n                if response?.image != nil {\n                    #expect(imageView.image == nil)\n                    partialCount += 1\n                    // We expect two partial images (at 5 scans, and 9 scans marks).\n                    if partialCount == 2 {\n                        expectPartialImageProduced.fulfill()\n                    }\n                    self.dataLoader.resume()\n                }\n            },\n            completion: { result in\n                #expect(imageView.image === result.value?.image)\n                expectedFinalLoaded.fulfill()\n            }\n        )\n        await expectPartialImageProduced.wait()\n        await expectedFinalLoaded.wait()\n    }\n#endif\n}\n"
  },
  {
    "path": "Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n#if os(tvOS)\nimport TVUIKit\n#endif\n@testable import Nuke\n@testable import NukeExtensions\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n@Suite(.timeLimit(.minutes(2))) @MainActor\nstruct ImageViewExtensionsTests {\n    let imageView: _ImageView\n    let observer: ImagePipelineObserver\n    let imageCache: MockImageCache\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n    let options: ImageLoadingOptions\n\n    init() {\n        let imageCache = MockImageCache()\n        let dataLoader = MockDataLoader()\n        let observer = ImagePipelineObserver()\n        self.imageCache = imageCache\n        self.dataLoader = dataLoader\n        self.observer = observer\n        self.pipeline = ImagePipeline(delegate: observer) {\n            $0.dataLoader = dataLoader\n            $0.imageCache = imageCache\n        }\n        self.imageView = _ImageView()\n        var options = ImageLoadingOptions()\n        options.pipeline = pipeline\n        self.options = options\n    }\n\n    // MARK: - Loading\n\n    @Test func imageLoaded() async {\n        await loadImageExpectingSuccess(with: Test.request, options: options, into: imageView)\n        #expect(imageView.image != nil)\n    }\n\n#if os(tvOS)\n    @Test func imageLoadedToTVPosterView() async {\n        let posterView = TVPosterView()\n        await loadImageExpectingSuccess(with: Test.request, options: options, into: posterView)\n        #expect(posterView.image != nil)\n    }\n#endif\n\n    @Test func imageLoadedWithURL() async {\n        let expectation = TestExpectation()\n        NukeExtensions.loadImage(with: Test.url, options: options, into: imageView) { _ in\n            expectation.fulfill()\n        }\n        await expectation.wait()\n        #expect(imageView.image != nil)\n    }\n\n    @Test func loadImageWithNilRequest() async {\n        imageView.image = Test.image\n\n        let expectation = TestExpectation()\n        let request: ImageRequest? = nil\n        NukeExtensions.loadImage(with: request, options: options, into: imageView) {\n            #expect($0.error == .imageRequestMissing)\n            expectation.fulfill()\n        }\n        await expectation.wait()\n        #expect(imageView.image == nil)\n    }\n\n    @Test func loadImageWithNilRequestAndPlaceholder() {\n        let failureImage = Test.image\n        var options = options\n        options.failureImage = failureImage\n        let request: ImageRequest? = nil\n        NukeExtensions.loadImage(with: request, options: options, into: imageView)\n        #expect(imageView.image === failureImage)\n    }\n\n    // MARK: - Managing Tasks\n\n    @Test func taskReturned() {\n        let task = NukeExtensions.loadImage(with: Test.request, options: options, into: imageView)\n        #expect(task != nil)\n        #expect(task?.request.urlRequest == Test.request.urlRequest)\n    }\n\n    @Test func taskIsNilWhenImageInMemoryCache() {\n        let request = Test.request\n        imageCache[request] = ImageContainer(image: PlatformImage())\n        let task = NukeExtensions.loadImage(with: request, options: options, into: imageView)\n        #expect(task == nil)\n    }\n\n    // MARK: - Prepare For Reuse\n\n    @Test func viewPreparedForReuse() {\n        imageView.image = Test.image\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView)\n        #expect(imageView.image == nil)\n    }\n\n    @Test func viewPreparedForReuseDisabled() {\n        let image = Test.image\n        imageView.image = image\n        var options = options\n        options.isPrepareForReuseEnabled = false\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView)\n        #expect(imageView.image == image)\n    }\n\n    // MARK: - Memory Cache\n\n    @Test func memoryCacheUsed() {\n        let image = Test.image\n        imageCache[Test.request] = ImageContainer(image: image)\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView)\n        #expect(imageView.image == image)\n    }\n\n    @Test func memoryCacheDisabled() {\n        imageCache[Test.request] = Test.container\n        var request = Test.request\n        request.options.insert(.disableMemoryCacheReads)\n        NukeExtensions.loadImage(with: request, options: options, into: imageView)\n        #expect(imageView.image == nil)\n    }\n\n    // MARK: - Completion and Progress Closures\n\n    @Test func completionCalled() async {\n        var didCallCompletion = false\n        let expectation = TestExpectation()\n        NukeExtensions.loadImage(\n            with: Test.request,\n            options: options,\n            into: imageView,\n            completion: { result in\n                #expect(Thread.isMainThread)\n                #expect(result.isSuccess)\n                didCallCompletion = true\n                expectation.fulfill()\n            }\n        )\n\n        // Expect completion to be called asynchronously\n        #expect(!didCallCompletion)\n        await expectation.wait()\n    }\n\n    @Test func completionCalledImageFromCache() {\n        // GIVEN the requested image stored in memory cache\n        imageCache[Test.request] = Test.container\n\n        var didCallCompletion = false\n        NukeExtensions.loadImage(\n            with: Test.request,\n            options: options,\n            into: imageView,\n            completion: { result in\n                // Expect completion to be called synchronously on the main thread\n                #expect(Thread.isMainThread)\n                #expect(result.isSuccess)\n                didCallCompletion = true\n            }\n        )\n        #expect(didCallCompletion)\n    }\n\n    @Test func progressHandlerCalled() async {\n        // GIVEN\n        dataLoader.results[Test.url] = .success(\n            (Data(count: 20), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        var progressValues: [(Int64, Int64)] = []\n        let expectation = TestExpectation()\n\n        // WHEN loading an image into a view\n        NukeExtensions.loadImage(\n            with: Test.request,\n            options: options,\n            into: imageView,\n            progress: { _, completed, total in\n                // Expect progress to be reported, on the main thread\n                #expect(Thread.isMainThread)\n                progressValues.append((completed, total))\n            },\n            completion: { _ in\n                expectation.fulfill()\n            }\n        )\n\n        await expectation.wait()\n        #expect(progressValues.count == 2)\n        #expect(progressValues[0] == (10, 20))\n        #expect(progressValues[1] == (20, 20))\n    }\n\n    // MARK: - Cancellation\n\n    @Test func requestCancelled() async {\n        dataLoader.isSuspended = true\n\n        // Given an image view with an associated image task\n        let startExpectation = TestExpectation(notification: ImagePipelineObserver.didStartTask, object: observer)\n        NukeExtensions.loadImage(with: Test.url, options: options, into: imageView)\n        await startExpectation.wait()\n\n        // Expect the task to get cancelled\n        // When asking Nuke to cancel the request for the view\n        await notification(ImagePipelineObserver.didCancelTask, object: observer) {\n            NukeExtensions.cancelRequest(for: imageView)\n        }\n    }\n\n    @Test func requestCancelledWhenNewRequestStarted() async {\n        dataLoader.isSuspended = true\n\n        // Given an image view with an associated image task\n        let startExpectation = TestExpectation(notification: ImagePipelineObserver.didStartTask, object: observer)\n        NukeExtensions.loadImage(with: Test.url, options: options, into: imageView)\n        await startExpectation.wait()\n\n        // When starting loading a new image\n        // Expect previous task to get cancelled\n        let cancelExpectation = TestExpectation(notification: ImagePipelineObserver.didCancelTask, object: observer)\n        NukeExtensions.loadImage(with: Test.url, options: options, into: imageView)\n        await cancelExpectation.wait()\n    }\n\n    @Test func requestCancelledWhenTargetGetsDeallocated() async {\n        dataLoader.isSuspended = true\n\n        // Wrap everything in autorelease pool to make sure that imageView\n        // gets deallocated immediately.\n        var localImageView: _ImageView? = _ImageView()\n        let startExpectation = TestExpectation(notification: ImagePipelineObserver.didStartTask, object: observer)\n        NukeExtensions.loadImage(with: Test.url, options: options, into: localImageView!)\n        await startExpectation.wait()\n\n        // Expect the task to be cancelled automatically\n        // When the view is deallocated\n        await notification(ImagePipelineObserver.didCancelTask, object: observer) {\n            localImageView = nil\n        }\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n#if os(iOS) || os(tvOS) || os(visionOS)\nimport UIKit\n#endif\n@testable import Nuke\n@testable import NukeExtensions\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n@Suite(.timeLimit(.minutes(2))) @MainActor\nstruct ImageViewIntegrationTests {\n    let imageView: _ImageView\n    let pipeline: ImagePipeline\n    let options: ImageLoadingOptions\n\n    init() {\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = DataLoader()\n            $0.imageCache = MockImageCache()\n        }\n        self.imageView = _ImageView()\n        var options = ImageLoadingOptions()\n        options.pipeline = pipeline\n        self.options = options\n    }\n\n    var url: URL {\n        Test.url(forResource: \"fixture\", extension: \"jpeg\")\n    }\n\n    var request: ImageRequest {\n        ImageRequest(url: url)\n    }\n\n    // MARK: - Loading\n\n    @Test func imageLoaded() async {\n        await loadImageExpectingSuccess(with: request, options: options, into: imageView)\n        #expect(imageView.image != nil)\n    }\n\n    @Test func imageLoadedWithURL() async {\n        let expectation = TestExpectation()\n        NukeExtensions.loadImage(with: url, options: options, into: imageView) { _ in\n            expectation.fulfill()\n        }\n        await expectation.wait()\n        #expect(imageView.image != nil)\n    }\n\n    // MARK: - Loading with Invalid URL\n\n    @Test func loadImageWithInvalidURLString() async {\n        let expectation = TestExpectation()\n        NukeExtensions.loadImage(with: URL(string: \"\"), options: options, into: imageView) { result in\n            #expect(result.error == .imageRequestMissing)\n            expectation.fulfill()\n        }\n        await expectation.wait()\n        #expect(imageView.image == nil)\n    }\n\n    @Test func loadingWithNilURL() async {\n        // GIVEN\n        var urlRequest = URLRequest(url: Test.url)\n        urlRequest.url = nil // Not sure why this is even possible\n\n        // WHEN\n        let expectation = TestExpectation()\n        NukeExtensions.loadImage(with: ImageRequest(urlRequest: urlRequest), options: options, into: imageView) { result in\n            // THEN\n            #expect(result.error?.dataLoadingError != nil)\n            expectation.fulfill()\n        }\n        await expectation.wait()\n        #expect(imageView.image == nil)\n    }\n\n    @Test func loadingWithRequestWithNilURL() async {\n        // GIVEN\n        let input = ImageRequest(url: nil)\n\n        // WHEN/THEN\n        let expectation = TestExpectation()\n        pipeline.loadImage(with: input) {\n            #expect($0.isFailure)\n            expectation.fulfill()\n        }\n        await expectation.wait()\n    }\n\n    // MARK: - Data Passed\n\n#if os(iOS) || os(visionOS)\n    private final class MockView: UIView, Nuke_ImageDisplaying {\n        func nuke_display(image: PlatformImage?, data: Data?) {\n            recordedData.append(data)\n        }\n\n        var recordedData = [Data?]()\n    }\n\n    // Disabled test\n    func _testThatAttachedDataIsPassed() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in\n                ImageDecoders.Empty()\n            }\n        }\n\n        let imageView = MockView()\n\n        var options = ImageLoadingOptions()\n        options.pipeline = pipeline\n        options.isPrepareForReuseEnabled = false\n\n        // WHEN\n        let expectation = TestExpectation()\n        NukeExtensions.loadImage(with: Test.url, options: options, into: imageView) { result in\n            #expect(result.value != nil)\n            #expect(result.value?.container.data != nil)\n            expectation.fulfill()\n        }\n        await expectation.wait()\n\n        // THEN\n        let data = try #require(imageView.recordedData.first)\n        #expect(data != nil)\n    }\n\n#endif\n}\n\n#endif\n"
  },
  {
    "path": "Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n@testable import NukeExtensions\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n@Suite(.timeLimit(.minutes(2))) @MainActor\nstruct ImageViewLoadingOptionsTests {\n    let mockCache: MockImageCache\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n    let imageView: _ImageView\n    let options: ImageLoadingOptions\n\n    init() {\n        let mockCache = MockImageCache()\n        let dataLoader = MockDataLoader()\n        self.mockCache = mockCache\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = mockCache\n        }\n        self.imageView = _ImageView()\n        var options = ImageLoadingOptions()\n        options.pipeline = pipeline\n        self.options = options\n    }\n\n    // MARK: - Transition\n\n    @Test func customTransitionPerformed() async {\n        // Given\n        var options = options\n\n        let expectTransition = TestExpectation()\n        options.transition = .custom({ (view, image) in\n            // Then\n            #expect(view == self.imageView)\n            #expect(self.imageView.image == nil) // Image isn't displayed automatically.\n            #expect(view == self.imageView)\n            self.imageView.image = image\n            expectTransition.fulfill()\n        })\n\n        // When\n        await loadImageExpectingSuccess(with: Test.request, options: options, into: imageView)\n        await expectTransition.wait()\n    }\n\n    // Tests https://github.com/kean/Nuke/issues/206\n    @Test func imageIsDisplayedFadeInTransition() async {\n        // Given options with .fadeIn transition\n        var options = options\n        options.transition = .fadeIn(duration: 10)\n\n        // When loading an image into an image view\n        await loadImageExpectingSuccess(with: Test.request, options: options, into: imageView)\n\n        // Then image is actually displayed\n        #expect(imageView.image != nil)\n    }\n\n    // MARK: - Placeholder\n\n    @Test func placeholderDisplayed() {\n        // Given\n        var options = options\n        let placeholder = PlatformImage()\n        options.placeholder = placeholder\n\n        // When\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView)\n\n        // Then\n        #expect(imageView.image == placeholder)\n    }\n\n    // MARK: - Failure Image\n\n    @Test func failureImageDisplayed() async {\n        // Given\n        dataLoader.results[Test.url] = .failure(\n            NSError(domain: \"ErrorDomain\", code: 42, userInfo: nil)\n        )\n\n        var options = options\n        let failureImage = PlatformImage()\n        options.failureImage = failureImage\n\n        // When\n        await loadImageAndWait(with: Test.request, options: options, into: imageView)\n\n        // Then\n        #expect(imageView.image == failureImage)\n    }\n\n    @Test func failureImageTransitionRun() async {\n        // Given\n        dataLoader.results[Test.url] = .failure(\n            NSError(domain: \"t\", code: 42, userInfo: nil)\n        )\n\n        var options = options\n        let failureImage = PlatformImage()\n        options.failureImage = failureImage\n\n        // Given\n        let expectTransition = TestExpectation()\n        options.failureImageTransition = .custom({ (view, image) in\n            // Then\n            #expect(view == self.imageView)\n            #expect(image == failureImage)\n            self.imageView.image = image\n            expectTransition.fulfill()\n        })\n\n        // When\n        await loadImageAndWait(with: Test.request, options: options, into: imageView)\n        await expectTransition.wait()\n\n        // Then\n        #expect(imageView.image == failureImage)\n    }\n\n#if !os(macOS)\n\n    // MARK: - Content Modes\n\n    @Test func placeholderAndSuccessContentModesApplied() async {\n        // Given\n        var options = options\n        options.contentModes = .init(\n            success: .scaleAspectFill, // default is .scaleToFill\n            failure: .center,\n            placeholder: .center\n        )\n        options.placeholder = PlatformImage()\n\n        // When\n        let expectation = TestExpectation()\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView) { _ in\n            expectation.fulfill()\n        }\n\n        // Then\n        #expect(imageView.contentMode == .center)\n        await expectation.wait()\n        #expect(imageView.contentMode == .scaleAspectFill)\n    }\n\n    @Test func successContentModeAppliedWhenFromMemoryCache() {\n        // Given\n        var options = options\n        options.contentModes = ImageLoadingOptions.ContentModes(\n            success: .scaleAspectFill,\n            failure: .center,\n            placeholder: .center\n        )\n\n        mockCache[Test.request] = Test.container\n\n        // When\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView)\n\n        // Then\n        #expect(imageView.contentMode == .scaleAspectFill)\n    }\n\n    @Test func failureContentModeApplied() async {\n        // Given\n        var options = options\n        options.contentModes = ImageLoadingOptions.ContentModes(\n            success: .scaleAspectFill,\n            failure: .center,\n            placeholder: .center\n        )\n        options.failureImage = PlatformImage()\n\n        dataLoader.results[Test.url] = .failure(\n            NSError(domain: \"t\", code: 42, userInfo: nil)\n        )\n\n        // When\n        await loadImageAndWait(with: Test.request, options: options, into: imageView)\n\n        // Then\n        #expect(imageView.contentMode == .center)\n    }\n\n#endif\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n\n    // MARK: - Tint Colors\n\n    @Test func placeholderAndSuccessTintColorApplied() async {\n        // Given\n        var options = options\n        options.tintColors = .init(\n            success: .blue,\n            failure: nil,\n            placeholder: .yellow\n        )\n        options.placeholder = PlatformImage()\n\n        // When\n        let expectation = TestExpectation()\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView) { _ in\n            expectation.fulfill()\n        }\n\n        // Then\n        #expect(imageView.tintColor == .yellow)\n        await expectation.wait()\n        #expect(imageView.tintColor == .blue)\n        #expect(imageView.image?.renderingMode == .alwaysTemplate)\n    }\n\n    @Test func successTintColorAppliedWhenFromMemoryCache() {\n        // Given\n        var options = options\n        options.tintColors = .init(\n            success: .blue,\n            failure: nil,\n            placeholder: nil\n        )\n\n        mockCache[Test.request] = Test.container\n\n        // When\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView)\n\n        // Then\n        #expect(imageView.tintColor == .blue)\n        #expect(imageView.image?.renderingMode == .alwaysTemplate)\n    }\n\n    @Test func failureTintColorApplied() async {\n        // Given\n        var options = options\n        options.tintColors = .init(\n            success: nil,\n            failure: .red,\n            placeholder: nil\n        )\n        options.failureImage = PlatformImage()\n\n        dataLoader.results[Test.url] = .failure(\n            NSError(domain: \"t\", code: 42, userInfo: nil)\n        )\n\n        // When\n        await loadImageAndWait(with: Test.request, options: options, into: imageView)\n\n        // Then\n        #expect(imageView.tintColor == .red)\n        #expect(imageView.image?.renderingMode == .alwaysTemplate)\n    }\n\n#endif\n\n    // MARK: - Pipeline\n\n    @Test func customPipelineUsed() async {\n        // Given\n        let dataLoader = MockDataLoader()\n        let pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n\n        var options = ImageLoadingOptions()\n        options.pipeline = pipeline\n\n        // When\n        await loadImageAndWait(with: Test.request, options: options, into: imageView)\n\n        // Then\n        _ = pipeline\n        #expect(dataLoader.createdTaskCount == 1)\n        #expect(self.dataLoader.createdTaskCount == 0)\n    }\n\n    // MARK: - Shared Options\n\n    @Test func sharedOptionsUsed() {\n        // Given\n        var options = options\n        let placeholder = PlatformImage()\n        options.placeholder = placeholder\n\n        // When\n        NukeExtensions.loadImage(with: Test.request, options: options, into: imageView)\n\n        // Then\n        #expect(imageView.image == placeholder)\n    }\n\n    // MARK: - Cache Policy\n\n    @Test func reloadIgnoringCachedData() async {\n        // When the requested image is stored in memory cache\n        var request = Test.request\n        mockCache[request] = ImageContainer(image: PlatformImage())\n\n        request.options = [.reloadIgnoringCachedData]\n\n        // When\n        await loadImageAndWait(with: request, options: options, into: imageView)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    // MARK: - Misc\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n    @Test func transitionCrossDissolve() async {\n        // GIVEN\n        var options = options\n        options.placeholder = Test.image\n        options.transition = .fadeIn(duration: 0.33)\n        options.isPrepareForReuseEnabled = false\n        options.contentModes = .init(\n            success: .scaleAspectFill,\n            failure: .center,\n            placeholder: .center\n        )\n\n        imageView.image = Test.image\n\n        // WHEN\n        await loadImageAndWait(with: Test.request, options: options, into: imageView)\n\n        // THEN make sure we run the pass with cross-disolve and at least\n        // it doesn't crash\n    }\n#endif\n\n    @Test func settingDefaultProcessor() async {\n        // GIVEN\n        var options = options\n        options.processors = [MockImageProcessor(id: \"p1\")]\n\n        // WHEN\n        await loadImageAndWait(with: Test.request, options: options, into: imageView)\n\n        // THEN\n        #expect(imageView.image?.nk_test_processorIDs == [\"p1\"])\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Tests/NukeExtensionsTests/NukeExtensionsTestsHelpers.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n@testable import NukeExtensions\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n@MainActor\nfunc loadImageAndWait(\n    with request: ImageRequest,\n    options: ImageLoadingOptions? = nil,\n    into imageView: ImageDisplayingView\n) async {\n    let expectation = TestExpectation()\n    NukeExtensions.loadImage(\n        with: request,\n        options: options,\n        into: imageView,\n        completion: { _ in\n            expectation.fulfill()\n        })\n    await expectation.wait()\n}\n\n@MainActor\nfunc loadImageExpectingSuccess(\n    with request: ImageRequest,\n    options: ImageLoadingOptions? = nil,\n    into imageView: ImageDisplayingView\n) async {\n    let expectation = TestExpectation()\n    NukeExtensions.loadImage(\n        with: request,\n        options: options,\n        into: imageView,\n        completion: { result in\n            #expect(result.isSuccess)\n            expectation.fulfill()\n        })\n    await expectation.wait()\n}\n\n#endif\n"
  },
  {
    "path": "Tests/NukePerformanceTests/DataCachePeformanceTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport XCTest\nimport Nuke\n\nclass DataCachePeformanceTests: XCTestCase {\n    var cache: DataCache!\n    var count = 1000\n\n    override func setUp() {\n        super.setUp()\n\n        cache = try! DataCache(name: UUID().uuidString)\n        _ = cache[\"key\"] // Wait till index is loaded.\n    }\n\n    override func tearDown() {\n        super.tearDown()\n\n        try? FileManager.default.removeItem(at: cache.path)\n    }\n\n    // MARK: - Write\n\n    func testWriteWithFlush() {\n        let data = Array(0..<count).map { _ in generateRandomData() }\n\n        measure {\n            for index in data.indices {\n                cache[\"\\(index)\"] = data[index]\n            }\n            cache.flush()\n        }\n    }\n\n    func testWriteWithFlushIndividual() {\n        let data = Array(0..<count).map { _ in generateRandomData() }\n\n        measure {\n            for index in data.indices {\n                let key = \"\\(index)\"\n                cache[key] = data[index]\n                cache.flush(for: key)\n            }\n        }\n    }\n\n    func testWriteWithoutFlush() {\n        let data = Array(0..<count).map { _ in generateRandomData() }\n\n        measure {\n            for index in data.indices {\n                cache[\"\\(index)\"] = data[index]\n            }\n        }\n    }\n\n    // MARK: - Read\n\n    func testReadFlushedPerformance() {\n        populate()\n\n        let queue = OperationQueue()\n        queue.maxConcurrentOperationCount = 2\n        measure { [cache] in\n            for idx in 0..<count {\n                queue.addOperation {\n                    _ = cache?[\"\\(idx)\"]\n                }\n            }\n            queue.waitUntilAllOperationsAreFinished()\n        }\n    }\n\n    func testReadFlushedPerformanceSync() {\n        populate()\n\n        measure {\n            for idx in 0..<count {\n                _ = self.cache[\"\\(idx)\"]\n            }\n        }\n    }\n\n    // MARK: - Helpers\n\n    func populate() {\n        for idx in 0..<count {\n            cache[\"\\(idx)\"] = generateRandomData()\n        }\n        cache.flush()\n    }\n}\n\nprivate func generateRandomData(count: Int = 256 * 1024) -> Data {\n    var bytes = [UInt8](repeating: 0, count: count)\n    let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)\n    assert(status == errSecSuccess)\n    return Data(bytes)\n}\n"
  },
  {
    "path": "Tests/NukePerformanceTests/ImageCachePerformanceTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport XCTest\nimport Nuke\n\nclass ImageCachePerformanceTests: XCTestCase {\n    func testCacheWrite() {\n        let cache = ImageCache()\n        let image = ImageContainer(image: PlatformImage())\n\n        let urls = (0..<100_000).map { _ in return URL(string: \"http://test.com/\\(Int.random(in: 0..<500))\")! }\n        let requests = urls.map { ImageRequest(url: $0) }\n\n        measure {\n            for request in requests {\n                cache[request] = image\n            }\n        }\n    }\n\n    func testCacheHit() {\n        let cache = ImageCache()\n        let image = ImageContainer(image: PlatformImage())\n\n        for index in 0..<2000 {\n            cache[ImageRequest(url: URL(string: \"http://test.com/\\(index)\")!)] = image\n        }\n\n        var hits = 0\n\n        let urls = (0..<100_000).map { _ in return URL(string: \"http://test.com/\\(Int.random(in: 0..<2000))\")! }\n        let requests = urls.map { ImageRequest(url: $0) }\n\n        measure {\n            for request in requests {\n                if cache[request] != nil {\n                    hits += 1\n                }\n            }\n        }\n\n        print(\"hits: \\(hits)\")\n    }\n\n    func testCacheMiss() {\n        let cache = ImageCache()\n\n        var misses = 0\n\n        let urls = (0..<100_000).map { _ in return URL(string: \"http://test.com/\\(Int.random(in: 0..<200))\")! }\n        let requests = urls.map { ImageRequest(url: $0) }\n\n        measure {\n            for request in requests {\n                if cache[request] != nil {\n                    misses += 1\n                }\n            }\n        }\n\n        print(\"misses: \\(misses)\")\n    }\n\n    func testCacheReplacement() {\n        let cache = ImageCache()\n        let request = Test.request\n        let image = Test.container\n\n        measure {\n            for _ in 0..<100_000 {\n                cache[request] = image\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/NukePerformanceTests/ImagePipelinePerformanceTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport XCTest\nimport Nuke\n\nclass ImagePipelinePerfomanceTests: XCTestCase {\n    /// A very broad test that establishes how long in general it takes to load\n    /// data, decode, and decompress 50+ images. It's very useful to get a\n    /// broad picture about how loader options affect performance.\n    func testLoaderOverallPerformance() {\n        let pipeline = makePipeline()\n\n        let requests = (0..<1000).map { ImageRequest(url: URL(string: \"http://test.com/\\($0)\")) }\n        measure {\n            let group = DispatchGroup()\n            for request in requests {\n                group.enter()\n                pipeline.loadImage(with: request, progress: nil) { _ in\n                    group.leave()\n                }\n            }\n            group.wait()\n        }\n    }\n\n    func testAsyncAwaitPerformance() {\n        let pipeline = makePipeline()\n\n        let requests = (0..<1000).map { ImageRequest(url: URL(string: \"http://test.com/\\($0)\")) }\n\n        measure {\n            let semaphore = DispatchSemaphore(value: 0)\n            Task.detached {\n                await withTaskGroup(of: Void.self) { group in\n                    for request in requests {\n                        group.addTask {\n                            _ = try? await pipeline.image(for: request)\n                        }\n                    }\n                }\n                semaphore.signal()\n            }\n            semaphore.wait()\n        }\n    }\n\n    func testAsyncImageTaskPerformance() {\n        let pipeline = makePipeline()\n\n        let requests = (0..<5000).map { ImageRequest(url: URL(string: \"http://test.com/\\($0)\")) }\n\n        measure {\n            let semaphore = DispatchSemaphore(value: 0)\n            Task.detached {\n                await withTaskGroup(of: Void.self) { group in\n                    for request in requests {\n                        group.addTask {\n                            _ = try? await pipeline.imageTask(with: request).image\n                        }\n                    }\n                }\n                semaphore.signal()\n            }\n            semaphore.wait()\n        }\n    }\n}\n\nprivate func makePipeline() -> ImagePipeline {\n    struct MockDecoder: ImageDecoding {\n        static let container = ImageContainer(image: Test.image)\n\n        func decode(_ data: Data) throws -> ImageContainer {\n            MockDecoder.container\n        }\n    }\n\n    let pipeline = ImagePipeline {\n        $0.imageCache = nil\n\n        $0.dataLoader = MockDataLoader()\n\n        $0.isDecompressionEnabled = false\n\n        // This must be off for this test, because rate limiter is optimized for\n        // the actual loading in the apps and not the synthetic tests like this.\n        $0.isRateLimiterEnabled = false\n\n        // Remove decoding from the equation\n        $0.makeImageDecoder = { _ in ImageDecoders.Empty() }\n    }\n\n    return pipeline\n}\n"
  },
  {
    "path": "Tests/NukePerformanceTests/ImageProcessingPerformanceTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport XCTest\nimport Nuke\n\nclass ImageProcessingPerformanceTests: XCTestCase {\n    func testCreatingProcessorIdentifiers() {\n        let decompressor = ImageProcessors.Resize(size: CGSize(width: 1, height: 1), contentMode: .aspectFill, upscale: false)\n\n        measure {\n            for _ in 0..<25_000 {\n                _ = decompressor.identifier\n            }\n        }\n    }\n\n    func testComparingTwoProcessorCompositions() {\n        let lhs = ImageProcessors.Composition([MockImageProcessor(id: \"123\"), ImageProcessors.Resize(size: CGSize(width: 1, height: 1), contentMode: .aspectFill, upscale: false)])\n        let rhs = ImageProcessors.Composition([MockImageProcessor(id: \"124\"), ImageProcessors.Resize(size: CGSize(width: 1, height: 1), contentMode: .aspectFill, upscale: false)])\n\n        measure {\n            for _ in 0..<25_000 {\n                if lhs.hashableIdentifier == rhs.hashableIdentifier {\n                    // do nothing\n                }\n            }\n        }\n    }\n\n    func testImageDecoding() {\n        let decoder = ImageDecoders.Default()\n\n        let data = Test.data\n        measure {\n            for _ in 0..<1_000 {\n                _ = try? decoder.decode(data)\n            }\n        }\n    }\n\n    // MARK: Creating Thumbnails\n\n    func testResizeImage() throws {\n        let image = try XCTUnwrap(makeHighResolutionImage())\n        let processor = ImageProcessors.Resize(size: CGSize(width: 64, height: 64), unit: .pixels)\n\n        measure {\n            for _ in 0..<10 {\n                _ = processor.process(image)\n            }\n        }\n    }\n\n    func testCreateThumbnail() throws {\n        let image = try XCTUnwrap(makeHighResolutionImage())\n        let data = try XCTUnwrap(ImageEncoders.ImageIO(type: .jpeg).encode(image))\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 64, height: 64), unit: .pixels)\n\n        measure {\n            for _ in 0..<10 {\n                _ = options.makeThumbnail(with: data)\n            }\n        }\n    }\n\n    // Should be roughly identical to the flexible target size.\n    func testCreateThumbnailStaticSize() throws {\n        let image = try XCTUnwrap(makeHighResolutionImage())\n        let data = try XCTUnwrap(ImageEncoders.ImageIO(type: .jpeg).encode(image))\n        let options = ImageRequest.ThumbnailOptions(maxPixelSize: 64)\n\n        measure {\n            for _ in 0..<10 {\n                _ = options.makeThumbnail(with: data)\n            }\n        }\n    }\n}\n\nprivate func makeHighResolutionImage() -> PlatformImage? {\n    ImageProcessors.Resize(width: 4000, unit: .pixels, upscale: true).process(Test.image)\n}\n"
  },
  {
    "path": "Tests/NukePerformanceTests/ImageRequestPerformanceTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport XCTest\nimport Nuke\n\nclass ImageRequestPerformanceTests: XCTestCase {\n    func testStoringRequestInCollections() {\n        let urls = (0..<200_000).map { _ in return URL(string: \"http://test.com/\\(Int.random(in: 0..<200))\")! }\n        let requests = urls.map { ImageRequest(url: $0) }\n\n        measure {\n            var array = [ImageRequest]()\n            for request in requests {\n                array.append(request)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/NukePerformanceTests/ImageViewPerformanceTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport XCTest\nimport Nuke\nimport NukeExtensions\n\n@MainActor\nclass ImageViewPerformanceTests: XCTestCase {\n    private let dummyCacheRequest = ImageRequest(url: URL(string: \"http://test.com/9999999)\")!, processors: [ImageProcessors.Resize(size: CGSize(width: 2, height: 2))])\n\n    override func setUp() {\n        super.setUp()\n\n        // Store something in memory cache to avoid going through an optimized empty Dictionary path\n        ImagePipeline.shared.configuration.imageCache?[dummyCacheRequest] = ImageContainer(image: PlatformImage())\n    }\n\n    override func tearDown() {\n        super.tearDown()\n\n        ImagePipeline.shared.configuration.imageCache?[dummyCacheRequest] = nil\n    }\n\n    // This is the primary use case that we are optimizing for - loading images\n    // into target, the API that majoriy of the apps are going to use.\n    func testImageViewMainThreadPerformance() {\n        let view = _ImageView()\n\n        let urls = (0..<20_000).map { _ in return URL(string: \"http://test.com/1)\")! }\n\n        measure {\n            for url in urls {\n                NukeExtensions.loadImage(with: url, into: view)\n            }\n        }\n    }\n\n    func testImageViewMainThreadPerformanceCacheHit() {\n        let view = _ImageView()\n\n        let requests = (0..<50_000).map { _ in ImageRequest(url: URL(string: \"http://test.com/1)\")!) }\n        for request in requests {\n            ImagePipeline.shared.configuration.imageCache?[request] = ImageContainer(image: PlatformImage())\n        }\n\n        measure {\n            for request in requests {\n                NukeExtensions.loadImage(with: request, into: view)\n            }\n        }\n    }\n\n    func testImageViewMainThreadPerformanceWithProcessor() {\n        let view = _ImageView()\n\n        let urls = (0..<20_000).map { _ in return URL(string: \"http://test.com/1)\")! }\n\n        measure {\n            for url in urls {\n                let request = ImageRequest(url: url, processors: [ImageProcessors.Resize(size: CGSize(width: 1, height: 1))])\n                NukeExtensions.loadImage(with: request, into: view)\n            }\n        }\n    }\n\n    func testImageViewMainThreadPerformanceWithProcessorAndSimilarImageInCache() {\n        let view = _ImageView()\n\n        let urls = (0..<20_000).map { _ in return URL(string: \"http://test.com/9999999)\")! }\n\n        measure {\n            for url in urls {\n                let request = ImageRequest(url: url, processors: [ImageProcessors.Resize(size: CGSize(width: 1, height: 1))])\n                NukeExtensions.loadImage(with: request, into: view)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/DataCacheTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\nimport Security\n@testable import Nuke\n\nprivate let blob = \"123\".data(using: .utf8)\nprivate let otherBlob = \"456\".data(using: .utf8)\n\n@Suite(.timeLimit(.minutes(2)))\nstruct DataCacheTests {\n    private let cache: DataCache\n\n    init() throws {\n        cache = try DataCache(\n            name: UUID().uuidString,\n            filenameGenerator: { String($0.reversed()) }\n        )\n    }\n\n    // MARK: Init\n\n    @Test func initWithName() throws {\n        // Given\n        let name = UUID().uuidString\n\n        // When\n        let cache = try DataCache(name: name, filenameGenerator: { $0 })\n\n        // Then\n        #expect(cache.path.lastPathComponent == name)\n        #expect(FileManager.default.fileExists(atPath: cache.path.path))\n    }\n\n    @Test func initWithPath() throws {\n        // Given\n        let name = UUID().uuidString\n        let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent(name)\n\n        // When\n        let cache = try DataCache(path: path, filenameGenerator: { $0 })\n\n        // Then\n        #expect(cache.path == path)\n        #expect(FileManager.default.fileExists(atPath: path.path))\n    }\n\n    // MARK: Default Key Encoder\n\n    @Test func defaultKeyEncoder() throws {\n        let cache = try DataCache(name: UUID().uuidString)\n        let filename = cache.filename(for: \"http://test.com\")\n        #expect(filename == \"50334ee0b51600df6397ce93ceed4728c37fee4e\")\n    }\n\n    @Test func sha1() {\n        #expect(\"http://test.com\".sha1 == \"50334ee0b51600df6397ce93ceed4728c37fee4e\")\n    }\n\n    // MARK: Add\n\n    @Test func add() {\n        cache.withSuspendedIO {\n            // When\n            cache[\"key\"] = blob\n\n            // Then\n            #expect(cache[\"key\"] == blob)\n        }\n    }\n\n    @Test func whenAddContentNotFlushedImmediately() {\n        cache.withSuspendedIO {\n            // When\n            cache[\"key\"] = blob\n\n            // Then\n            #expect(cache.contents.count == 0)\n        }\n    }\n\n    @Test func addAndFlush() {\n        // Given\n        cache.withSuspendedIO {\n            cache[\"key\"] = blob\n        }\n\n        // When\n        cache.flush()\n\n        // Then\n        #expect(cache.contents.count == 1)\n        #expect(cache[\"key\"] == blob)\n        #expect((try? Data(contentsOf: cache.contents.first!)) == blob)\n    }\n\n    @Test func replace() {\n        cache.withSuspendedIO {\n            // Given\n            cache[\"key\"] = blob\n\n            // When\n            cache[\"key\"] = otherBlob\n\n            // Then\n            #expect(cache[\"key\"] == otherBlob)\n        }\n    }\n\n    @Test func replaceFlushed() {\n        // Given\n        cache[\"key\"] = blob\n        cache.flush()\n\n        cache.withSuspendedIO {\n            cache[\"key\"] = otherBlob\n            #expect(cache.contents.count == 1)\n            // Test that before flush we still have the old blob on disk,\n            // but new blob in staging\n            #expect((try? Data(contentsOf: cache.contents.first!)) == blob)\n            #expect(cache[\"key\"] == otherBlob)\n        }\n\n        // Flush and test that data on disk was updated.\n        cache.flush()\n        #expect(cache.contents.count == 1)\n        #expect((try? Data(contentsOf: cache.contents.first!)) == otherBlob)\n        #expect(cache[\"key\"] == otherBlob)\n    }\n\n    // MARK: Removal\n\n    @Test func removeNonExistent() {\n        cache[\"key\"] = nil\n        cache.flush()\n    }\n\n    // - Remove + write (new) staged -> remove from staging\n    @Test func removeFromStaging() {\n        cache.withSuspendedIO {\n            cache[\"key\"] = blob\n            cache[\"key\"] = nil\n            #expect(cache[\"key\"] == nil)\n        }\n        cache.flush()\n        #expect(cache[\"key\"] == nil)\n    }\n\n    // - Remove + write (new) staged -> remove from staging\n    @Test func removeReplaced() {\n        cache.withSuspendedIO {\n            cache[\"key\"] = blob\n            cache[\"key\"] = otherBlob\n            cache[\"key\"] = nil\n        }\n        cache.flush()\n        #expect(cache[\"key\"] == nil)\n        #expect(cache.contents.count == 0)\n    }\n\n    // - Remove + write (replace) staged -> schedule removal\n    @Test func removeReplacedFlushed() {\n        cache[\"key\"] = blob\n        cache.flush()\n\n        cache.withSuspendedIO {\n            cache[\"key\"] = otherBlob\n            cache[\"key\"] = nil\n            #expect(cache[\"key\"] == nil)\n            #expect((try? Data(contentsOf: cache.contents.first!)) == blob)\n        }\n\n        cache.flush() // Should still perform deletion of \"blob\"\n        #expect(cache.contents.count == 0)\n    }\n\n    // - Remove + flushed -> schedule removal\n    @Test func removeFlushed() {\n        // Given\n        cache[\"key\"] = blob\n        cache.flush()\n\n        cache.withSuspendedIO {\n            cache[\"key\"] = nil\n            #expect(cache[\"key\"] == nil)\n            // Still have data in cache\n            #expect(cache.contents.count == 1)\n            #expect((try? Data(contentsOf: cache.contents.first!)) == blob)\n        }\n        cache.flush()\n\n        #expect(cache[\"key\"] == nil)\n\n        // IO performed\n        #expect(cache.contents.count == 0)\n    }\n\n    // - Remove + removal staged -> noop\n    @Test func removeWhenRemovalAlreadyScheduled() {\n        // Given\n        cache[\"key\"] = blob\n        cache.flush()\n\n        // When\n        cache[\"key\"] = nil\n        cache[\"key\"] = nil\n        cache.flush()\n\n        // Then\n        #expect(cache.contents.count == 0)\n    }\n\n    @Test func removeAndThenReplace() {\n        // Given\n        cache[\"key\"] = blob\n        cache.flush()\n\n        // When\n        cache[\"key\"] = nil\n        cache[\"key\"] = blob\n        cache.flush()\n\n        // Then\n        #expect(cache[\"key\"] == blob)\n        #expect(cache.contents.count == 1)\n        #expect((try? Data(contentsOf: cache.contents.first!)) == blob)\n    }\n\n    // MARK: Remove All\n\n    @Test func removeAll() {\n        cache.withSuspendedIO {\n            // Given\n            cache[\"key\"] = blob\n\n            // When\n            cache.removeAll()\n\n            // Then\n            #expect(cache[\"key\"] == nil)\n        }\n    }\n\n    @Test func removeAllFlushed() {\n        // Given\n        cache[\"key\"] = blob\n        cache.flush()\n\n        // When\n        cache.withSuspendedIO {\n            cache.removeAll()\n            #expect(cache[\"key\"] == nil)\n        }\n    }\n\n    @Test func removeAllFlushedAndFlush() {\n        // Given\n        cache[\"key\"] = blob\n        cache.flush()\n\n        // When\n        cache.removeAll()\n        cache.flush()\n\n        // Then\n        #expect(cache[\"key\"] == nil)\n        #expect(cache.contents.count == 0)\n    }\n\n    @Test func removeAllAndAdd() {\n        // Given\n        cache.withSuspendedIO {\n            cache[\"key\"] = blob\n\n            // When\n            cache.removeAll()\n            cache[\"key\"] = blob\n\n            // Then\n            #expect(cache[\"key\"] == blob)\n        }\n    }\n\n    @Test func removeAllTwice() {\n        // Given\n        cache.withSuspendedIO {\n            cache[\"key\"] = blob\n\n            // When\n            cache.removeAll()\n            cache[\"key\"] = blob\n            cache.removeAll()\n\n            // Then\n            #expect(cache[\"key\"] == nil)\n        }\n    }\n\n    // MARK: DataCaching\n\n    @Test func getCachedDataHitFromStaging() {\n        // Given\n        cache.flush() // Index is loaded\n\n        cache.withSuspendedIO {\n            // Given\n            cache[\"key\"] = blob\n\n            // When/Then\n            let data = cache.cachedData(for: \"key\")\n            #expect(data == blob)\n        }\n    }\n\n    @Test func getCachedData() {\n        // Given\n        cache[\"key\"] = blob\n        cache.flush()\n\n        // When/Then\n        let data = cache.cachedData(for: \"key\")\n        #expect(data == blob)\n    }\n\n    // MARK: Flush\n\n    @Test func flush() {\n        // Given\n        cache.flushInterval = .seconds(20)\n        cache[\"key\"] = blob\n\n        // When\n        cache.flush()\n\n        // Then\n        #expect(cache.contents == [cache.url(for: \"key\")].compactMap { $0 })\n    }\n\n    @Test func flushForKey() {\n        // Given\n        cache.flushInterval = .seconds(20)\n        cache[\"key\"] = blob\n\n        // When\n        cache.flush(for: \"key\")\n\n        // Then\n        #expect(cache.contents == [cache.url(for: \"key\")].compactMap { $0 })\n    }\n\n    @Test func flushForKey2() {\n        // Given\n        cache.flushInterval = .seconds(20)\n        cache[\"key1\"] = blob\n        cache[\"key2\"] = blob\n\n        // When\n        cache.flush(for: \"key1\")\n\n        // Then only flushes content for the specific key\n        #expect(cache.contents == [cache.url(for: \"key1\")].compactMap { $0 })\n    }\n\n    // MARK: Sweep\n\n    @Test func sweep() {\n        // GIVEN\n        let mb = 1024 * 1024 // allocated size is usually about 4 KB on APFS, so use 1 MB just to be sure\n        cache.sizeLimit = mb * 3\n        cache[\"key1\"] = Data(repeating: 1, count: mb)\n        cache[\"key2\"] = Data(repeating: 1, count: mb)\n        cache[\"key3\"] = Data(repeating: 1, count: mb)\n        cache[\"key4\"] = Data(repeating: 1, count: mb)\n        cache.flush()\n\n        // WHEN\n        cache.sweep()\n\n        // THEN\n        #expect(cache.totalSize == mb * 2)\n    }\n\n    @Test func sweepReducesTotalCount() {\n        // GIVEN - 5 entries, limit fits only 3\n        let mb = 1024 * 1024\n        cache.sizeLimit = mb * 3\n        for i in 1...5 {\n            cache[\"key\\(i)\"] = Data(repeating: UInt8(i), count: mb)\n        }\n        cache.flush()\n\n        // WHEN\n        cache.sweep()\n\n        // THEN - at most 3 MB worth of entries remain\n        #expect(cache.totalCount <= 3)\n        #expect(cache.totalSize <= mb * 3)\n    }\n\n    @Test func sweepIsNoOpWhenUnderLimit() {\n        // GIVEN - total size well under the limit\n        let mb = 1024 * 1024\n        cache.sizeLimit = mb * 10\n        cache[\"small1\"] = Data(repeating: 1, count: mb)\n        cache[\"small2\"] = Data(repeating: 2, count: mb)\n        cache.flush()\n\n        let countBefore = cache.totalCount\n\n        // WHEN\n        cache.sweep()\n\n        // THEN - nothing is removed\n        #expect(cache.totalCount == countBefore)\n    }\n\n    // MARK: Inspection\n\n    @Test func containsData() {\n        // GIVEN\n        cache[\"key\"] = blob\n        cache.flush(for: \"key\")\n\n        // WHEN/THEN\n        #expect(cache.containsData(for: \"key\"))\n    }\n\n    @Test func containsDataInStaging() {\n        // GIVEN\n        cache.flushInterval = .seconds(20)\n        cache[\"key\"] = blob\n\n        // WHEN/THEN\n        #expect(cache.containsData(for: \"key\"))\n    }\n\n    @Test func containsDataAfterRemoval() {\n        // GIVEN\n        cache.flushInterval = .seconds(20)\n        cache[\"key\"] = blob\n        cache.flush(for: \"key\")\n        cache[\"key\"] = nil\n\n        // WHEN/THEN\n        #expect(!cache.containsData(for: \"key\"))\n    }\n\n    @Test func totalCount() {\n        #expect(cache.totalCount == 0)\n\n        cache[\"1\"] = \"1\".data(using: .utf8)\n        cache.flush()\n\n        #expect(cache.totalCount == 1)\n    }\n\n    @Test func totalSize() {\n        #expect(cache.totalSize == 0)\n\n        cache[\"1\"] = \"1\".data(using: .utf8)\n        cache.flush()\n\n        #expect(cache.totalSize > 0)\n    }\n\n    @Test func totalAllocatedSize() {\n        #expect(cache.totalAllocatedSize == 0)\n\n        cache[\"1\"] = \"1\".data(using: .utf8)\n        cache.flush()\n\n        // Depends on the file system.\n        #expect(cache.totalAllocatedSize > 0)\n    }\n\n    // MARK: Resilience\n\n    @Test func whenDirectoryDeletedCacheAutomaticallyRecreatesIt() throws {\n        cache[\"1\"] = \"2\".data(using: .utf8)\n        cache.flush()\n\n        try FileManager.default.removeItem(at: cache.path)\n\n        cache[\"1\"] = \"2\".data(using: .utf8)\n        cache.flush()\n\n        let url = try #require(cache.url(for: \"1\"))\n        let data = try Data(contentsOf: url)\n        #expect(String(data: data, encoding: .utf8) == \"2\")\n    }\n\n    // MARK: Default Filename Generator\n\n    @Test func initWithPathUsingDefaultFilenameGenerator() throws {\n        let name = UUID().uuidString\n        let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!\n            .appendingPathComponent(name, isDirectory: true)\n        let cache = try DataCache(path: path)\n\n        cache[\"http://example.com/image.png\"] = blob\n        cache.flush()\n\n        #expect(cache.containsData(for: \"http://example.com/image.png\"))\n        #expect(cache.filename(for: \"http://example.com/image.png\") != nil)\n    }\n\n    // MARK: Invalid Keys\n\n    @Test func cachedDataForEmptyKey() throws {\n        let cache = try DataCache(name: UUID().uuidString)\n        #expect(cache.cachedData(for: \"\") == nil)\n    }\n\n    @Test func containsDataForEmptyKey() throws {\n        let cache = try DataCache(name: UUID().uuidString)\n        #expect(!cache.containsData(for: \"\"))\n    }\n\n    @Test func urlForEmptyKey() throws {\n        let cache = try DataCache(name: UUID().uuidString)\n        #expect(cache.url(for: \"\") == nil)\n    }\n\n    // MARK: Metadata\n\n@Test func scheduledSweepUpdatesMetadata() async throws {\n        let expectation = TestExpectation()\n        let cache = try DataCache(\n            name: UUID().uuidString,\n            filenameGenerator: { String($0.reversed()) },\n            sweepDelay: .milliseconds(0),\n            onSweepCompleted: { expectation.fulfill() }\n        )\n        await expectation.wait()\n\n        let metadataURL = cache.path.appendingPathComponent(\".data-cache-info\")\n        struct CacheMetadata: Codable { var lastSweepDate: Date? }\n        let data = try Data(contentsOf: metadataURL)\n        let metadata = try JSONDecoder().decode(CacheMetadata.self, from: data)\n        #expect(metadata.lastSweepDate != nil)\n        _ = cache\n    }\n\n    @Test func initWithExistingMetadataSkipsSweep() throws {\n        let name = UUID().uuidString\n        let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!\n        let path = root.appendingPathComponent(name, isDirectory: true)\n        try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true)\n\n        struct CacheMetadata: Codable { var lastSweepDate: Date? }\n        let metadata = CacheMetadata(lastSweepDate: Date())\n        try JSONEncoder().encode(metadata).write(\n            to: path.appendingPathComponent(\".data-cache-info\")\n        )\n\n        let cache = try DataCache(path: path, filenameGenerator: { String($0.reversed()) })\n\n        cache[\"key\"] = blob\n        cache.flush()\n        #expect(cache[\"key\"] == blob)\n    }\n\n    // MARK: Sweep Edge Cases\n\n    @Test func sweepWhenSizeUnderLimit() throws {\n        let cache = try DataCache(\n            name: UUID().uuidString,\n            filenameGenerator: { String($0.reversed()) }\n        )\n        cache.sizeLimit = 1024 * 1024 * 100\n        cache[\"a\"] = Data(repeating: 1, count: 100)\n        cache.flush()\n\n        cache.sweep()\n        #expect(cache.containsData(for: \"a\"))\n    }\n\n    @Test func sweepWhenEmpty() throws {\n        let cache = try DataCache(\n            name: UUID().uuidString,\n            filenameGenerator: { String($0.reversed()) }\n        )\n        cache.sweep()\n        #expect(cache.totalCount == 0)\n    }\n\n    // MARK: Large Data\n\n    @Test func storeLargeData() {\n        // GIVEN - a 500 KB payload (well above typical image sizes used in other tests)\n        let largeData = Data(repeating: 0xAB, count: 500_000)\n\n        // WHEN\n        cache[\"large-key\"] = largeData\n        cache.flush()\n\n        // THEN - data survives the flush and is retrieved intact\n        let retrieved = cache.cachedData(for: \"large-key\")\n        #expect(retrieved?.count == largeData.count)\n    }\n\n    @Test func storeLargeDataReplacedBySmallData() {\n        // GIVEN - write a large blob, then overwrite with a small blob\n        let largeData = Data(repeating: 0xFF, count: 500_000)\n        let smallData = Data(repeating: 0x01, count: 100)\n\n        cache[\"key\"] = largeData\n        cache.flush()\n\n        cache[\"key\"] = smallData\n        cache.flush()\n\n        // THEN - the latest (small) payload wins\n        let retrieved = cache.cachedData(for: \"key\")\n        #expect(retrieved?.count == smallData.count)\n    }\n\n    // MARK: Store Data for Invalid Key\n\n    @Test func storeDataForEmptyKeyIsNoOp() throws {\n        let cache = try DataCache(name: UUID().uuidString)\n        cache.storeData(blob!, for: \"\")\n        cache.flush()\n\n        #expect(cache.totalCount == 0)\n    }\n\n}\n\nextension DataCache {\n    var contents: [URL] {\n        return try! FileManager.default.contentsOfDirectory(at: self.path, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)\n    }\n\n    func withSuspendedIO(_ closure: () -> Void) {\n        queue.suspend()\n        closure()\n        queue.resume()\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/DataLoaderTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.serialized, .timeLimit(.minutes(2)))\nstruct DataLoaderTests {\n\n    init() {\n        MockURLProtocol.handlers.removeAll()\n    }\n\n    // MARK: - Successful Loading\n\n    @Test func loadSingleChunk() async throws {\n        let url = mockURL(\"single\")\n        let body = Data(\"hello\".utf8)\n        registerMock(url: url, chunks: [body])\n\n        let loader = makeDataLoader()\n        let request = URLRequest(url: url)\n        let (stream, response) = try await loader.loadData(with: request)\n\n        let httpResponse = try #require(response as? HTTPURLResponse)\n        #expect(httpResponse.statusCode == 200)\n\n        var received = Data()\n        for try await chunk in stream {\n            received.append(chunk)\n        }\n        #expect(received == body)\n    }\n\n    @Test func loadMultipleChunks() async throws {\n        let url = mockURL(\"multi\")\n        let chunk1 = Data(\"aaa\".utf8)\n        let chunk2 = Data(\"bbb\".utf8)\n        let chunk3 = Data(\"ccc\".utf8)\n        registerMock(url: url, chunks: [chunk1, chunk2, chunk3])\n\n        let loader = makeDataLoader()\n        let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n\n        var chunks = [Data]()\n        for try await chunk in stream {\n            chunks.append(chunk)\n        }\n        let combined = chunks.reduce(Data(), +)\n        #expect(combined == chunk1 + chunk2 + chunk3)\n    }\n\n    @Test func loadEmptyBody() async throws {\n        let url = mockURL(\"empty\")\n        registerMock(url: url, chunks: [])\n\n        let loader = makeDataLoader()\n        let (stream, response) = try await loader.loadData(with: URLRequest(url: url))\n\n        let httpResponse = try #require(response as? HTTPURLResponse)\n        #expect(httpResponse.statusCode == 200)\n\n        var received = Data()\n        for try await chunk in stream {\n            received.append(chunk)\n        }\n        #expect(received.isEmpty)\n    }\n\n    @Test func responseHeadersAreDelivered() async throws {\n        let url = mockURL(\"headers\")\n        registerMock(url: url, headers: [\"X-Custom\": \"value123\"], chunks: [Data(\"x\".utf8)])\n\n        let loader = makeDataLoader()\n        let (stream, response) = try await loader.loadData(with: URLRequest(url: url))\n\n        let httpResponse = try #require(response as? HTTPURLResponse)\n        #expect(httpResponse.value(forHTTPHeaderField: \"X-Custom\") == \"value123\")\n\n        // Drain stream\n        for try await _ in stream {}\n    }\n\n    // MARK: - Validation\n\n    @Test func validationRejectsNon2xxStatusCode() async throws {\n        let url = mockURL(\"404\")\n        registerMock(url: url, statusCode: 404, chunks: [Data(\"not found\".utf8)])\n\n        let loader = makeDataLoader()\n        do {\n            _ = try await loader.loadData(with: URLRequest(url: url))\n            Issue.record(\"Expected validation error\")\n        } catch let error as DataLoader.Error {\n            guard case .statusCodeUnacceptable(let code) = error else {\n                Issue.record(\"Wrong error case\")\n                return\n            }\n            #expect(code == 404)\n        }\n    }\n\n    @Test func validationRejects500() async throws {\n        let url = mockURL(\"500\")\n        registerMock(url: url, statusCode: 500, chunks: [Data(\"fail\".utf8)])\n\n        let loader = makeDataLoader()\n        do {\n            _ = try await loader.loadData(with: URLRequest(url: url))\n            Issue.record(\"Expected validation error\")\n        } catch let error as DataLoader.Error {\n            guard case .statusCodeUnacceptable(let code) = error else {\n                Issue.record(\"Wrong error case\")\n                return\n            }\n            #expect(code == 500)\n        }\n    }\n\n    @Test func validationAccepts2xxRange() async throws {\n        for statusCode in [200, 201, 204, 299] {\n            let url = mockURL(\"status-\\(statusCode)\")\n            registerMock(url: url, statusCode: statusCode, chunks: [Data(\"ok\".utf8)])\n\n            let loader = makeDataLoader()\n            let (stream, response) = try await loader.loadData(with: URLRequest(url: url))\n\n            let httpResponse = try #require(response as? HTTPURLResponse)\n            #expect(httpResponse.statusCode == statusCode)\n            for try await _ in stream {}\n        }\n    }\n\n    @Test func customValidation() async throws {\n        let url = mockURL(\"custom-val\")\n        registerMock(url: url, statusCode: 200, chunks: [Data(\"ok\".utf8)])\n\n        struct CustomError: Error {}\n        let loader = makeDataLoader { _ in CustomError() }\n\n        do {\n            _ = try await loader.loadData(with: URLRequest(url: url))\n            Issue.record(\"Expected custom validation error\")\n        } catch {\n            #expect(error is CustomError)\n        }\n    }\n\n    @Test func noValidationPassesEverything() async throws {\n        let url = mockURL(\"no-val\")\n        registerMock(url: url, statusCode: 500, chunks: [Data(\"ok\".utf8)])\n\n        let loader = makeDataLoader { _ in nil }\n        let (stream, response) = try await loader.loadData(with: URLRequest(url: url))\n\n        let httpResponse = try #require(response as? HTTPURLResponse)\n        #expect(httpResponse.statusCode == 500)\n        for try await _ in stream {}\n    }\n\n    // MARK: - Errors\n\n    @Test func errorBeforeResponse() async throws {\n        let url = mockURL(\"dns-fail\")\n        registerMockError(url: url, error: URLError(.cannotFindHost))\n\n        let loader = makeDataLoader()\n        do {\n            _ = try await loader.loadData(with: URLRequest(url: url))\n            Issue.record(\"Expected error\")\n        } catch {\n            #expect((error as? URLError)?.code == .cannotFindHost)\n        }\n    }\n\n    @Test func errorMidStream() async throws {\n        let url = mockURL(\"mid-fail\")\n        let partialData = Data(\"partial\".utf8)\n        registerMockPartialFailure(url: url, data: partialData, error: URLError(.networkConnectionLost))\n\n        let loader = makeDataLoader()\n\n        // URLProtocol delivers response + data + error synchronously, so\n        // URLSession may surface the error from `loadData` or the stream.\n        do {\n            let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n            var received = Data()\n            do {\n                for try await chunk in stream {\n                    received.append(chunk)\n                }\n                Issue.record(\"Expected stream error\")\n            } catch {\n                #expect((error as? URLError)?.code == .networkConnectionLost)\n            }\n        } catch {\n            #expect((error as? URLError)?.code == .networkConnectionLost)\n        }\n    }\n\n    // MARK: - Cancellation\n\n    @Test func taskCancellationThrows() async throws {\n        let url = mockURL(\"cancel\")\n        let started = TestExpectation()\n        // Handler that never completes — the task will be cancelled before it finishes\n        MockURLProtocol.handlers[url] = .init { _, client, proto in\n            let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: \"HTTP/1.1\", headerFields: nil)!\n            client.urlProtocol(proto, didReceive: response, cacheStoragePolicy: .notAllowed)\n            started.fulfill()\n            // Don't send data or finish — simulate a slow response\n        }\n\n        let loader = makeDataLoader()\n        let task = Task {\n            let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n            for try await _ in stream {}\n        }\n\n        // Wait for the request to actually start, then cancel\n        await started.wait()\n        task.cancel()\n\n        do {\n            try await task.value\n            Issue.record(\"Expected cancellation error\")\n        } catch is CancellationError {\n            // Expected\n        } catch {\n            // URLError.cancelled is also acceptable\n            #expect((error as? URLError)?.code == .cancelled)\n        }\n    }\n\n    // MARK: - Incremental Delivery\n\n    @Test func prefersIncrementalDeliveryDefault() async throws {\n        let url = mockURL(\"incr-default\")\n        registerMock(url: url, chunks: [Data(\"x\".utf8)])\n\n        let loader = makeDataLoader()\n        #expect(loader.prefersIncrementalDelivery == false)\n\n        let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n        for try await _ in stream {}\n    }\n\n    // MARK: - Static Validation Helper\n\n    @Test func staticValidateAccepts200() {\n        let response = HTTPURLResponse(url: mockURL(), statusCode: 200, httpVersion: nil, headerFields: nil)!\n        #expect(DataLoader.validate(response: response) == nil)\n    }\n\n    @Test func staticValidateRejects400() {\n        let response = HTTPURLResponse(url: mockURL(), statusCode: 400, httpVersion: nil, headerFields: nil)!\n        let error = DataLoader.validate(response: response)\n        #expect(error != nil)\n        if let dlError = error as? DataLoader.Error, case .statusCodeUnacceptable(let code) = dlError {\n            #expect(code == 400)\n        } else {\n            Issue.record(\"Expected DataLoader.Error.statusCodeUnacceptable\")\n        }\n    }\n\n    @Test func staticValidateAcceptsNonHTTPResponse() {\n        let response = URLResponse(url: mockURL(), mimeType: nil, expectedContentLength: 0, textEncodingName: nil)\n        #expect(DataLoader.validate(response: response) == nil)\n    }\n\n    // MARK: - Error Description\n\n    @Test func errorDescription() {\n        let error = DataLoader.Error.statusCodeUnacceptable(404)\n        #expect(error.description.contains(\"404\"))\n    }\n\n    // MARK: - Large Data\n\n    @Test func loadLargeData() async throws {\n        let url = mockURL(\"large\")\n        let largeData = Data(repeating: 0xAB, count: 1_000_000)\n        // Split into multiple chunks\n        let chunkSize = 100_000\n        var chunks = [Data]()\n        var offset = 0\n        while offset < largeData.count {\n            let end = min(offset + chunkSize, largeData.count)\n            chunks.append(largeData[offset..<end])\n            offset = end\n        }\n        registerMock(url: url, chunks: chunks)\n\n        let loader = makeDataLoader()\n        let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n\n        var received = Data()\n        for try await chunk in stream {\n            received.append(chunk)\n        }\n        #expect(received == largeData)\n    }\n\n    // MARK: - Multiple Concurrent Loads\n\n    @Test func concurrentLoads() async throws {\n        let loader = makeDataLoader()\n\n        for i in 0..<5 {\n            let url = mockURL(\"concurrent-\\(i)\")\n            let body = Data(\"response-\\(i)\".utf8)\n            registerMock(url: url, chunks: [body])\n        }\n\n        try await withThrowingTaskGroup(of: (Int, Data).self) { group in\n            for i in 0..<5 {\n                let url = mockURL(\"concurrent-\\(i)\")\n                group.addTask {\n                    let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n                    var data = Data()\n                    for try await chunk in stream {\n                        data.append(chunk)\n                    }\n                    return (i, data)\n                }\n            }\n            for try await (i, data) in group {\n                #expect(data == Data(\"response-\\(i)\".utf8))\n            }\n        }\n    }\n\n    // MARK: - Delegate Forwarding\n\n    @Test func settingDelegateProperty() async throws {\n        let url = mockURL(\"delegate-set\")\n        registerMock(url: url, chunks: [Data(\"ok\".utf8)])\n\n        let loader = makeDataLoader()\n        let spy = SpyURLSessionDelegate()\n        loader.delegate = spy\n\n        let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n        for try await _ in stream {}\n\n        // Drain the delegate queue to ensure all callbacks have been processed\n        await withCheckedContinuation { continuation in\n            loader.session.delegateQueue.addBarrierBlock {\n                continuation.resume()\n            }\n        }\n\n        #expect(spy.didReceiveResponseCount > 0)\n        #expect(spy.didReceiveDataCount > 0)\n        #expect(spy.didCompleteCount > 0)\n    }\n\n    @Test func delegateReceivesMetricsCallback() async throws {\n        let url = mockURL(\"delegate-metrics\")\n        registerMock(url: url, chunks: [Data(\"data\".utf8)])\n\n        let loader = makeDataLoader()\n        let spy = SpyURLSessionDelegate()\n        loader.delegate = spy\n\n        let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n        for try await _ in stream {}\n\n        await withCheckedContinuation { continuation in\n            loader.session.delegateQueue.addBarrierBlock {\n                continuation.resume()\n            }\n        }\n\n        #expect(spy.didFinishMetricsCount > 0)\n    }\n\n    // MARK: - Default Validation\n\n    @Test func initWithDefaultValidation() async throws {\n        let url = mockURL(\"default-val\")\n        registerMock(url: url, chunks: [Data(\"ok\".utf8)])\n\n        let config = URLSessionConfiguration.ephemeral\n        config.protocolClasses = [MockURLProtocol.self]\n        let loader = DataLoader(configuration: config)\n\n        let (stream, response) = try await loader.loadData(with: URLRequest(url: url))\n        let httpResponse = try #require(response as? HTTPURLResponse)\n        #expect(httpResponse.statusCode == 200)\n        for try await _ in stream {}\n    }\n\n    @Test func initWithDefaultValidationRejectsNon2xx() async throws {\n        let url = mockURL(\"default-val-reject\")\n        registerMock(url: url, statusCode: 403, chunks: [Data(\"forbidden\".utf8)])\n\n        let config = URLSessionConfiguration.ephemeral\n        config.protocolClasses = [MockURLProtocol.self]\n        let loader = DataLoader(configuration: config)\n\n        do {\n            _ = try await loader.loadData(with: URLRequest(url: url))\n            Issue.record(\"Expected validation error\")\n        } catch {\n            #expect(error is DataLoader.Error)\n        }\n    }\n\n    // MARK: - Static Properties\n\n    @Test func defaultConfigurationHasUrlCache() {\n        let config = DataLoader.defaultConfiguration\n        #expect(config.urlCache === DataLoader.sharedUrlCache)\n    }\n\n    @Test func sharedUrlCacheHasExpectedCapacity() {\n        let cache = DataLoader.sharedUrlCache\n        #expect(cache.memoryCapacity == 0)\n        #expect(cache.diskCapacity == 150 * 1048576)\n    }\n\n    // MARK: - Caching\n\n    @Test func willCacheResponseIsForwarded() async throws {\n        let url = mockURL(\"cache-fwd\")\n        MockURLProtocol.handlers[url] = .init { _, client, proto in\n            let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: \"HTTP/1.1\", headerFields: [\"Content-Length\": \"5\"])!\n            client.urlProtocol(proto, didReceive: response, cacheStoragePolicy: .allowedInMemoryOnly)\n            client.urlProtocol(proto, didLoad: Data(\"hello\".utf8))\n            client.urlProtocolDidFinishLoading(proto)\n        }\n\n        let config = URLSessionConfiguration.ephemeral\n        config.protocolClasses = [MockURLProtocol.self]\n        config.urlCache = URLCache(memoryCapacity: 1_000_000, diskCapacity: 0)\n        let loader = DataLoader(configuration: config, validate: { _ in nil })\n\n        let spy = SpyURLSessionDelegate()\n        loader.delegate = spy\n\n        let (stream, _) = try await loader.loadData(with: URLRequest(url: url))\n        for try await _ in stream {}\n\n        await withCheckedContinuation { continuation in\n            loader.session.delegateQueue.addBarrierBlock {\n                continuation.resume()\n            }\n        }\n\n        #expect(spy.didReceiveResponseCount > 0)\n    }\n\n    // MARK: - Redirect\n\n    @Test func redirectIsFollowed() async throws {\n        let sourceURL = mockURL(\"redirect-source\")\n        let destURL = mockURL(\"redirect-dest\")\n\n        registerMock(url: destURL, chunks: [Data(\"redirected\".utf8)])\n\n        MockURLProtocol.handlers[sourceURL] = .init { _, client, proto in\n            let redirectResponse = HTTPURLResponse(url: sourceURL, statusCode: 302, httpVersion: \"HTTP/1.1\", headerFields: [\"Location\": destURL.absoluteString])!\n            client.urlProtocol(proto, wasRedirectedTo: URLRequest(url: destURL), redirectResponse: redirectResponse)\n        }\n\n        let loader = makeDataLoader { _ in nil }\n\n        do {\n            let (stream, _) = try await loader.loadData(with: URLRequest(url: sourceURL))\n            var received = Data()\n            for try await chunk in stream {\n                received.append(chunk)\n            }\n            #expect(received == Data(\"redirected\".utf8))\n        } catch {\n            // Some URLSession implementations may handle redirects differently with MockURLProtocol\n        }\n    }\n}\n\n// MARK: - Spy Delegate\n\nprivate final class SpyURLSessionDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable {\n    @Mutex var didReceiveResponseCount = 0\n    @Mutex var didReceiveDataCount = 0\n    @Mutex var didCompleteCount = 0\n    @Mutex var didFinishMetricsCount = 0\n\n    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {\n        didReceiveResponseCount += 1\n        completionHandler(.allow)\n    }\n\n    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {\n        didReceiveDataCount += 1\n    }\n\n    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {\n        didCompleteCount += 1\n    }\n\n    func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {\n        didFinishMetricsCount += 1\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/DataPublisherTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct DataRequestTests {\n    @Test func initDoesNotStartExecutionRightAway() async throws {\n        let operation = MockOperation()\n        let pipeline = ImagePipeline()\n\n        // Creating the request should NOT trigger the closure\n        let request = ImageRequest(id: UUID().uuidString, data: {\n            await operation.execute()\n        })\n\n        #expect(operation.executeCalls == 0)\n\n        // Loading the image should trigger the closure\n        _ = try await pipeline.image(for: request)\n\n        #expect(operation.executeCalls == 1)\n    }\n\n    @Test func dataRequestDeliversCorrectImage() async throws {\n        // GIVEN a request that provides image data via a closure\n        let pipeline = ImagePipeline()\n        let request = ImageRequest(id: \"test-image\", data: {\n            Test.data\n        })\n\n        // WHEN\n        let image = try await pipeline.image(for: request)\n\n        // THEN the decoded image has the expected size\n        #expect(image.sizeInPixels == CGSize(width: 640, height: 480))\n    }\n\n    @Test func dataRequestThrowingClosurePropagatesError() async throws {\n        // GIVEN a request whose data closure throws\n        struct DataFetchError: Error {}\n        let pipeline = ImagePipeline()\n        let request = ImageRequest(id: \"failing\", data: {\n            throw DataFetchError()\n        })\n\n        // WHEN / THEN the error is wrapped in a pipeline dataLoadingFailed error\n        do {\n            _ = try await pipeline.image(for: request)\n            Issue.record(\"Expected an error\")\n        } catch {\n            guard case .dataLoadingFailed = error else {\n                Issue.record(\"Expected dataLoadingFailed, got \\(error)\")\n                return\n            }\n        }\n    }\n\n    private final class MockOperation: @unchecked Sendable {\n        private(set) var executeCalls = 0\n\n        func execute() async -> Data {\n            executeCalls += 1\n            await Task.yield()\n            return Test.data\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageCacheKeyTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageCacheKeyTests {\n    @Test func customKeyEquality() {\n        let key1 = ImageCacheKey(key: \"test-key\")\n        let key2 = ImageCacheKey(key: \"test-key\")\n        #expect(key1 == key2)\n    }\n\n    @Test func customKeyInequality() {\n        let key1 = ImageCacheKey(key: \"key-a\")\n        let key2 = ImageCacheKey(key: \"key-b\")\n        #expect(key1 != key2)\n    }\n\n    @Test func requestKeyEquality() {\n        let request = ImageRequest(url: URL(string: \"https://example.com/image.png\")!)\n        let key1 = ImageCacheKey(request: request)\n        let key2 = ImageCacheKey(request: request)\n        #expect(key1 == key2)\n    }\n\n    @Test func requestKeyDiffers() {\n        let request1 = ImageRequest(url: URL(string: \"https://example.com/a.png\")!)\n        let request2 = ImageRequest(url: URL(string: \"https://example.com/b.png\")!)\n        let key1 = ImageCacheKey(request: request1)\n        let key2 = ImageCacheKey(request: request2)\n        #expect(key1 != key2)\n    }\n\n    @Test func customKeyHashable() {\n        let key1 = ImageCacheKey(key: \"same\")\n        let key2 = ImageCacheKey(key: \"same\")\n        #expect(key1.hashValue == key2.hashValue)\n    }\n\n    @Test func customAndRequestKeysDiffer() {\n        let customKey = ImageCacheKey(key: \"custom\")\n        let requestKey = ImageCacheKey(request: ImageRequest(url: URL(string: \"https://example.com/custom\")!))\n        #expect(customKey != requestKey)\n    }\n\n    @Test func requestKeyHashable() {\n        let request = ImageRequest(url: URL(string: \"https://example.com/image.png\")!)\n        let key1 = ImageCacheKey(request: request)\n        let key2 = ImageCacheKey(request: request)\n        #expect(key1.hashValue == key2.hashValue)\n    }\n\n    @Test func customKeyCanBeUsedInSet() {\n        let key1 = ImageCacheKey(key: \"a\")\n        let key2 = ImageCacheKey(key: \"b\")\n        let key3 = ImageCacheKey(key: \"a\")\n        let set: Set<ImageCacheKey> = [key1, key2, key3]\n        #expect(set.count == 2)\n    }\n\n    @Test func customKeyCanBeUsedAsDictionaryKey() {\n        var dict = [ImageCacheKey: String]()\n        dict[ImageCacheKey(key: \"k1\")] = \"value1\"\n        dict[ImageCacheKey(key: \"k2\")] = \"value2\"\n        #expect(dict[ImageCacheKey(key: \"k1\")] == \"value1\")\n        #expect(dict[ImageCacheKey(key: \"k2\")] == \"value2\")\n        #expect(dict[ImageCacheKey(key: \"k3\")] == nil)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageCacheTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n#if os(iOS) || os(tvOS) || os(visionOS)\nimport UIKit\n#endif\n\nprivate func _request(index: Int) -> ImageRequest {\n    return ImageRequest(url: URL(string: \"http://example.com/img\\(index)\")!)\n}\nprivate let request1 = _request(index: 1)\nprivate let request2 = _request(index: 2)\nprivate let request3 = _request(index: 3)\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageCacheTests {\n    let cache: ImageCache\n\n    init() {\n        cache = ImageCache()\n        cache.entryCostLimit = 2\n    }\n\n    // MARK: - Basics\n\n    @Test func cacheCreation() {\n        #expect(cache.totalCount == 0)\n        #expect(cache[Test.request] == nil)\n    }\n\n    @Test func imageIsStored() {\n        // When\n        cache[Test.request] = Test.container\n\n        // Then\n        #expect(cache.totalCount == 1)\n        #expect(cache[Test.request] != nil)\n    }\n\n    // MARK: - Subscript\n\n    @Test func imageIsStoredUsingSubscript() {\n        // When\n        cache[Test.request] = Test.container\n\n        // Then\n        #expect(cache[Test.request] != nil)\n    }\n\n    // MARK: - Count\n\n    @Test func totalCountChanges() {\n        #expect(cache.totalCount == 0)\n\n        cache[request1] = Test.container\n        #expect(cache.totalCount == 1)\n\n        cache[request2] = Test.container\n        #expect(cache.totalCount == 2)\n\n        cache[request2] = nil\n        #expect(cache.totalCount == 1)\n\n        cache[request1] = nil\n        #expect(cache.totalCount == 0)\n    }\n\n    @Test func countLimitChanges() {\n        // When\n        cache.countLimit = 1\n\n        // Then\n        #expect(cache.countLimit == 1)\n    }\n\n    @Test func ttlChanges() {\n        // When\n        cache.ttl = 1\n\n        // Then\n        #expect(cache.ttl == 1)\n    }\n\n    @Test func itemsAreRemovedImmediatelyWhenCountLimitIsReached() {\n        // Given\n        cache.countLimit = 1\n\n        // When\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n\n        // Then\n        #expect(cache[request1] == nil)\n        #expect(cache[request2] != nil)\n    }\n\n    @Test func trimToCount() {\n        // Given\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n\n        // When\n        cache.trim(toCount: 1)\n\n        // Then\n        #expect(cache[request1] == nil)\n        #expect(cache[request2] != nil)\n    }\n\n    @Test func countLimitOfZeroPreventsCaching() {\n        // Given\n        cache.countLimit = 0\n\n        // When\n        cache[request1] = Test.container\n\n        // Then\n        #expect(cache.totalCount == 0)\n        #expect(cache[request1] == nil)\n    }\n\n    @Test func imagesAreRemovedOnCountLimitChange() {\n        // Given\n        cache.countLimit = 2\n\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n\n        // When\n        cache.countLimit = 1\n\n        // Then\n        #expect(cache[request1] == nil)\n        #expect(cache[request2] != nil)\n    }\n\n    // MARK: Cost\n\n#if !os(macOS)\n\n    @Test func defaultImageCost() {\n        #expect(cache.cost(for: ImageContainer(image: Test.image)) == 1228800)\n    }\n\n    @Test func totalCostChanges() {\n        let imageCost = cache.cost(for: ImageContainer(image: Test.image))\n        #expect(cache.totalCost == 0)\n\n        cache[request1] = Test.container\n        #expect(cache.totalCost == imageCost)\n\n        cache[request2] = Test.container\n        #expect(cache.totalCost == 2 * imageCost)\n\n        cache[request2] = nil\n        #expect(cache.totalCost == imageCost)\n\n        cache[request1] = nil\n        #expect(cache.totalCost == 0)\n    }\n\n    @Test func replacingExistingEntryKeepsCostConsistent() {\n        // Given\n        cache.costLimit = Int.max\n        cache[request1] = Test.container\n        let costAfterFirstStore = cache.totalCost\n\n        // When - store a new container at the same key\n        cache[request1] = Test.container\n\n        // Then - cost should not double; the old entry cost is replaced\n        #expect(cache.totalCost == costAfterFirstStore)\n        #expect(cache.totalCount == 1)\n    }\n\n    @Test func costLimitOfZeroPreventsCaching() {\n        // Given\n        cache.costLimit = 0\n\n        // When\n        cache[request1] = Test.container\n\n        // Then\n        #expect(cache.totalCost == 0)\n        #expect(cache[request1] == nil)\n    }\n\n    @Test func costLimitChanged() {\n        // Given\n        let cost = cache.cost(for: ImageContainer(image: Test.image))\n\n        // When\n        cache.costLimit = Int(Double(cost) * 1.5)\n\n        // Then\n        #expect(cache.costLimit == Int(Double(cost) * 1.5))\n    }\n\n    @Test func itemsAreRemovedImmediatelyWhenCostLimitIsReached() {\n        // Given\n        let cost = cache.cost(for: ImageContainer(image: Test.image))\n        cache.costLimit = Int(Double(cost) * 1.5)\n\n        // When/Then\n        cache[request1] = Test.container\n\n        // LRU item is released\n        cache[request2] = Test.container\n        #expect(cache[request1] == nil)\n        #expect(cache[request2] != nil)\n    }\n\n    @Test func entryCostLimitEntryStored() {\n        // Given\n        let container = ImageContainer(image: Test.image)\n        let cost = cache.cost(for: container)\n        cache.costLimit = Int(Double(cost) * 15)\n        cache.entryCostLimit = 0.1\n\n        // When\n        cache[Test.request] = container\n\n        // Then\n        #expect(cache[Test.request] != nil)\n        #expect(cache.totalCount == 1)\n    }\n\n    @Test func entryCostLimitEntryNotStored() {\n        // Given\n        let container = ImageContainer(image: Test.image)\n        let cost = cache.cost(for: container)\n        cache.costLimit = Int(Double(cost) * 3)\n        cache.entryCostLimit = 0.1\n\n        // When\n        cache[Test.request] = container\n\n        // Then\n        #expect(cache[Test.request] == nil)\n        #expect(cache.totalCount == 0)\n    }\n\n    @Test func trimToCost() {\n        // Given\n        cache.costLimit = Int.max\n\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n\n        // When\n        let cost = cache.cost(for: ImageContainer(image: Test.image))\n        cache.trim(toCost: Int(Double(cost) * 1.5))\n\n        // Then\n        #expect(cache[request1] == nil)\n        #expect(cache[request2] != nil)\n    }\n\n    @Test func imagesAreRemovedOnCostLimitChange() {\n        // Given\n        let cost = cache.cost(for: ImageContainer(image: Test.image))\n        cache.costLimit = Int(Double(cost) * 2.5)\n\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n\n        // When\n        cache.costLimit = cost\n\n        // Then\n        #expect(cache[request1] == nil)\n        #expect(cache[request2] != nil)\n    }\n\n    @Test func imageContainerWithoutAssociatedDataCost() {\n        // Given\n        let data = Test.data(name: \"cat\", extension: \"gif\")\n        let image = PlatformImage(data: data)!\n        let container = ImageContainer(image: image, data: nil)\n\n        // Then\n        #expect(cache.cost(for: container) == 558000)\n    }\n\n    @Test func imageContainerWithAssociatedDataCost() {\n        // Given\n        let data = Test.data(name: \"cat\", extension: \"gif\")\n        let image = PlatformImage(data: data)!\n        let container = ImageContainer(image: image, data: data)\n\n        // Then\n        #expect(cache.cost(for: container) == 558000 + 427672)\n    }\n\n#endif\n\n    // MARK: LRU\n\n    @Test func leastRecentItemsAreRemoved() {\n        // Given\n        let cost = cache.cost(for: ImageContainer(image: Test.image))\n        cache.costLimit = Int(Double(cost) * 2.5)\n\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n        cache[request3] = Test.container\n\n        // Then\n        #expect(cache[request1] == nil)\n        #expect(cache[request2] != nil)\n        #expect(cache[request3] != nil)\n    }\n\n    @Test func itemsAreTouched() {\n        // Given\n        let cost = cache.cost(for: ImageContainer(image: Test.image))\n        cache.costLimit = Int(Double(cost) * 2.5)\n\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n        _ = cache[request1] // Touched image\n\n        // When\n        cache[request3] = Test.container\n\n        // Then\n        #expect(cache[request1] != nil)\n        #expect(cache[request2] == nil)\n        #expect(cache[request3] != nil)\n    }\n\n    @Test func trimToCountRespectsLRUOrder() {\n        // Given - three items inserted in order\n        cache.countLimit = Int.max\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n        cache[request3] = Test.container\n\n        // When - access request1 and request3, making request2 the LRU item\n        _ = cache[request1]\n        _ = cache[request3]\n\n        // Trim down to 2 items\n        cache.trim(toCount: 2)\n\n        // Then - the LRU item (request2) is evicted; recently accessed items remain\n        #expect(cache[request1] != nil)\n        #expect(cache[request2] == nil)\n        #expect(cache[request3] != nil)\n        #expect(cache.totalCount == 2)\n    }\n\n    // MARK: Misc\n\n    @Test func removeAll() {\n        // GIVEN\n        cache[request1] = Test.container\n        cache[request2] = Test.container\n\n        // WHEN\n        cache.removeAll()\n\n        // THEN\n        #expect(cache.totalCount == 0)\n        #expect(cache.totalCost == 0)\n    }\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n    @MainActor\n    @Test func someImagesAreRemovedOnDidEnterBackground() async {\n        // GIVEN\n        cache.costLimit = Int.max\n        cache.countLimit = 10 // 1 out of 10 images should remain\n\n        for i in 0..<10 {\n            cache[_request(index: i)] = Test.container\n        }\n        #expect(cache.totalCount == 10)\n\n        // WHEN\n        await Task.yield() // Allow the background notification observer to be registered\n        NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)\n\n        // THEN\n        #expect(cache.totalCount == 1)\n    }\n\n    @MainActor\n    @Test func someImagesAreRemovedBasedOnCostOnDidEnterBackground() async {\n        // GIVEN\n        let cost = cache.cost(for: ImageContainer(image: Test.image))\n        cache.costLimit = cost * 10\n        cache.countLimit = Int.max\n\n        for index in 0..<10 {\n            let request = ImageRequest(url: URL(string: \"http://example.com/img\\(index)\")!)\n            cache[request] = Test.container\n        }\n        #expect(cache.totalCount == 10)\n\n        // WHEN\n        await Task.yield() // Allow the background notification observer to be registered\n        NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)\n\n        // THEN\n        #expect(cache.totalCount == 1)\n    }\n\n#endif\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct InternalCacheTTLTests {\n    let cache = Cache<Int, Int>(costLimit: 1000, countLimit: 1000)\n\n    // MARK: TTL\n\n    @Test func ttl() async throws {\n        // Given\n        cache.set(1, forKey: 1, cost: 1, ttl: 0.05)  // 50 ms\n        #expect(cache.value(forKey: 1) != nil)\n\n        // When\n        try await Task.sleep(for: .milliseconds(55))\n\n        // Then\n        #expect(cache.value(forKey: 1) == nil)\n    }\n\n    @Test func defaultTTLIsUsed() async throws {\n        // Given\n        cache.conf.ttl = 0.05 // 50 ms\n        cache.set(1, forKey: 1, cost: 1)\n        #expect(cache.value(forKey: 1) != nil)\n\n        // When\n        try await Task.sleep(for: .milliseconds(55))\n\n        // Then\n        #expect(cache.value(forKey: 1) == nil)\n    }\n\n    @Test func defaultToNonExpiringEntries() async throws {\n        // Given\n        cache.set(1, forKey: 1, cost: 1)\n        #expect(cache.value(forKey: 1) != nil)\n\n        // When\n        try await Task.sleep(for: .milliseconds(55))\n\n        // Then\n        #expect(cache.value(forKey: 1) != nil)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageContainerTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageContainerTests {\n\n    // MARK: - Copy-on-Write\n\n    @Test func copyOnWriteIsPreview() {\n        // GIVEN\n        let a = ImageContainer(image: Test.image, isPreview: false)\n\n        // WHEN - copy and mutate\n        var b = a\n        b.isPreview = true\n\n        // THEN - original is unchanged\n        #expect(a.isPreview == false)\n        #expect(b.isPreview == true)\n    }\n\n    @Test func copyOnWriteType() {\n        // GIVEN\n        let a = ImageContainer(image: Test.image, type: .jpeg)\n\n        // WHEN\n        var b = a\n        b.type = .png\n\n        // THEN\n        #expect(a.type == .jpeg)\n        #expect(b.type == .png)\n    }\n\n    @Test func copyOnWriteUserInfo() {\n        // GIVEN\n        let a = ImageContainer(image: Test.image)\n\n        // WHEN\n        var b = a\n        b.userInfo[\"key\"] = \"value\"\n\n        // THEN original is unaffected\n        #expect(a.userInfo.isEmpty)\n        #expect(b.userInfo[\"key\"] as? String == \"value\")\n    }\n\n    @Test func copyOnWriteData() {\n        // GIVEN\n        let originalData = Data([0x01, 0x02])\n        let a = ImageContainer(image: Test.image, data: originalData)\n\n        // WHEN\n        var b = a\n        b.data = Data([0xFF])\n\n        // THEN\n        #expect(a.data == originalData)\n        #expect(b.data == Data([0xFF]))\n    }\n\n    // MARK: - UserInfoKey\n\n    @Test func userInfoKeyEquality() {\n        let k1 = ImageContainer.UserInfoKey(\"test-key\")\n        let k2 = ImageContainer.UserInfoKey(\"test-key\")\n        let k3 = ImageContainer.UserInfoKey(\"other-key\")\n        #expect(k1 == k2)\n        #expect(k1 != k3)\n        #expect(k1.hashValue == k2.hashValue)\n    }\n\n    @Test func userInfoKeyExpressibleByStringLiteral() {\n        let key: ImageContainer.UserInfoKey = \"my-key\"\n        #expect(key.rawValue == \"my-key\")\n    }\n\n    // MARK: - Default Values\n\n    @Test func defaultValuesAreCorrect() {\n        let container = ImageContainer(image: Test.image)\n        #expect(container.isPreview == false)\n        #expect(container.data == nil)\n        #expect(container.type == nil)\n        #expect(container.userInfo.isEmpty)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageDecoderRegistryTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageDecoderRegistryTests {\n    @Test func defaultDecoderIsReturned() {\n        // Given\n        let context = ImageDecodingContext.mock\n\n        // Then\n        let decoder = ImageDecoderRegistry().decoder(for: context)\n        #expect(decoder is ImageDecoders.Default)\n    }\n\n    @Test func registerDecoder() {\n        // Given\n        let registry = ImageDecoderRegistry()\n        let context = ImageDecodingContext.mock\n\n        // When\n        registry.register { _ in\n            return MockImageDecoder(name: \"A\")\n        }\n\n        // Then\n        let decoder1 = registry.decoder(for: context) as? MockImageDecoder\n        #expect(decoder1?.name == \"A\")\n\n        // When\n        registry.register { _ in\n            return MockImageDecoder(name: \"B\")\n        }\n\n        // Then\n        let decoder2 = registry.decoder(for: context) as? MockImageDecoder\n        #expect(decoder2?.name == \"B\")\n    }\n\n    @Test func clearDecoders() {\n        // Given\n        let registry = ImageDecoderRegistry()\n        let context = ImageDecodingContext.mock\n\n        registry.register { _ in\n            return MockImageDecoder(name: \"A\")\n        }\n\n        // When\n        registry.clear()\n\n        // Then\n        let noDecoder = registry.decoder(for: context)\n        #expect(noDecoder == nil)\n    }\n\n    @Test func whenReturningNextDecoderIsEvaluated() {\n        // Given\n        let registry = ImageDecoderRegistry()\n        registry.register { _ in\n            return nil\n        }\n\n        // When\n        let context = ImageDecodingContext.mock\n        let decoder = ImageDecoderRegistry().decoder(for: context)\n\n        // Then\n        #expect(decoder is ImageDecoders.Default)\n    }\n\n    // MARK: - Fallthrough and Ordering\n\n    @Test func whenRegisteredDecoderReturnsNilFallsToBuiltIn() {\n        // GIVEN a registry with one decoder that always declines\n        let registry = ImageDecoderRegistry()\n        registry.register { _ in nil }\n\n        // WHEN\n        let context = ImageDecodingContext.mock\n        let decoder = registry.decoder(for: context)\n\n        // THEN the built-in default decoder is returned\n        #expect(decoder is ImageDecoders.Default)\n    }\n\n    @Test func decodersEvaluatedInLIFOOrder() {\n        // GIVEN a registry with two custom decoders registered in sequence\n        let registry = ImageDecoderRegistry()\n        registry.register { _ in MockImageDecoder(name: \"first\") }\n        registry.register { _ in MockImageDecoder(name: \"second\") }\n\n        // WHEN\n        let context = ImageDecodingContext.mock\n        let decoder = registry.decoder(for: context) as? MockImageDecoder\n\n        // THEN the most-recently registered decoder wins (LIFO)\n        #expect(decoder?.name == \"second\")\n    }\n\n    @Test func whenAllCustomDecodersDeclineBuiltInIsReturned() {\n        // GIVEN a registry where every custom decoder returns nil\n        let registry = ImageDecoderRegistry()\n        registry.register { _ in nil }\n        registry.register { _ in nil }\n        registry.register { _ in nil }\n\n        // WHEN\n        let context = ImageDecodingContext.mock\n        let decoder = registry.decoder(for: context)\n\n        // THEN falls through to the built-in Default decoder\n        #expect(decoder is ImageDecoders.Default)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageDecoderTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\nimport ImageIO\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageDecoderTests {\n    @Test func decodePNG() throws {\n        // Given\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n        let decoder = ImageDecoders.Default()\n\n        // When\n        let container = try decoder.decode(data)\n\n        // Then\n        #expect(container.type == .png)\n        #expect(!container.isPreview)\n        #expect(container.data == nil)\n        #expect(container.userInfo.isEmpty)\n    }\n\n    @Test func decodeJPEG() throws {\n        // Given\n        let data = Test.data(name: \"baseline\", extension: \"jpeg\")\n        let decoder = ImageDecoders.Default()\n\n        // When\n        let container = try decoder.decode(data)\n\n        // Then\n        #expect(container.type == .jpeg)\n        #expect(!container.isPreview)\n        #expect(container.data == nil)\n        #expect(container.userInfo.isEmpty)\n    }\n\n    @Test func decodingProgressiveJPEG() {\n        let data = Test.data(name: \"progressive\", extension: \"jpeg\")\n        let decoder = ImageDecoders.Default()\n\n        // Not enough data for progressive detection (SOF2 not yet reached)\n        #expect(decoder.decodePartiallyDownloadedData(data[0...358]) == nil)\n        #expect(decoder.numberOfScans == 0)\n\n        // After SOF2 marker, CGImageSource produces previews immediately\n        let scan1 = decoder.decodePartiallyDownloadedData(data[0...500])\n        #expect(scan1 != nil)\n        #expect(scan1?.isPreview == true)\n        #expect(decoder.numberOfScans == 1)\n        if let image = scan1?.image {\n#if os(macOS)\n            #expect(image.size.width == 450)\n            #expect(image.size.height == 300)\n#else\n            #expect(image.size.width * image.scale == 450)\n            #expect(image.size.height * image.scale == 300)\n#endif\n        }\n        #expect(scan1?.userInfo[.scanNumberKey] as? Int == 1)\n\n        // More data produces additional previews\n        let scan2 = decoder.decodePartiallyDownloadedData(data[0...5000])\n        #expect(scan2 != nil)\n        #expect(scan2?.isPreview == true)\n        #expect(decoder.numberOfScans == 2)\n\n        // Feed all data\n        let final = decoder.decodePartiallyDownloadedData(data)\n        #expect(final != nil)\n        #expect(decoder.numberOfScans == 3)\n    }\n\n\n    @Test func decodingBaselineJPEG() throws {\n        let data = Test.data(name: \"baseline\", extension: \"jpeg\")\n\n        // Default policy for baseline JPEG is .disabled — no previews\n        var context = ImageDecodingContext.mock(data: data)\n        context.previewPolicy = .default(for: data)\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        let partial = decoder.decodePartiallyDownloadedData(data[0...(data.count / 2)])\n        #expect(partial == nil)\n\n        // Full decode always works\n        let container = try decoder.decode(data)\n        #expect(container.type == .jpeg)\n        #expect(!container.isPreview)\n    }\n\n    @Test func decodingBaselineJPEGWithIncrementalPolicy() throws {\n        let data = Test.data(name: \"baseline\", extension: \"jpeg\")\n\n        // With .incremental policy, Image I/O produces partial top-down renders\n        var context = ImageDecodingContext.mock(data: data)\n        context.previewPolicy = .incremental\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        let partial = decoder.decodePartiallyDownloadedData(data[0...(data.count / 2)])\n        #expect(partial != nil)\n        #expect(partial?.isPreview == true)\n\n        let container = try decoder.decode(data)\n        #expect(container.type == .jpeg)\n        #expect(!container.isPreview)\n    }\n\n    @Test func decodingBaselineJPEGWithThumbnailPolicy() throws {\n        let data = Test.data(name: \"baseline\", extension: \"jpeg\")\n\n        var context = ImageDecodingContext.mock(data: data)\n        context.previewPolicy = .thumbnail\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        // Baseline JPEG typically has no embedded EXIF thumbnail\n        _ = decoder.decodePartiallyDownloadedData(data)\n        // Whether this returns an image depends on the specific file;\n        // either way, subsequent calls should return nil\n        #expect(decoder.decodePartiallyDownloadedData(data) == nil)\n    }\n\n    @Test func decodingProgressiveJPEGWithDisabledPolicy() throws {\n        let data = Test.data(name: \"progressive\", extension: \"jpeg\")\n\n        var context = ImageDecodingContext.mock(data: data)\n        context.previewPolicy = .disabled\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        // No previews with .disabled policy\n        #expect(decoder.decodePartiallyDownloadedData(data[0...500]) == nil)\n        #expect(decoder.decodePartiallyDownloadedData(data[0...5000]) == nil)\n        #expect(decoder.numberOfScans == 0)\n\n        // Full decode still works\n        let container = try decoder.decode(data)\n        #expect(container.type == .jpeg)\n    }\n\n    @Test func decodingPNGPartialData() throws {\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n\n        // Default policy for PNG is .disabled — no previews\n        var context = ImageDecodingContext.mock(data: data)\n        context.previewPolicy = .default(for: data)\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        #expect(decoder.decodePartiallyDownloadedData(data[0...100]) == nil)\n\n        // Full decode still works\n        let container = try decoder.decode(data)\n        #expect(container.type == .png)\n        #expect(!container.isPreview)\n    }\n\n    @Test func decoderAlwaysCreatedFromContext() throws {\n        // The decoder should always initialize, regardless of image format.\n        let jpegData = Test.data(name: \"baseline\", extension: \"jpeg\")\n        let jpegContext = ImageDecodingContext.mock(data: jpegData)\n        #expect(ImageDecoders.Default(context: jpegContext) != nil)\n\n        let pngData = Test.data(name: \"fixture\", extension: \"png\")\n        let pngContext = ImageDecodingContext.mock(data: pngData)\n        #expect(ImageDecoders.Default(context: pngContext) != nil)\n\n        let progressiveData = Test.data(name: \"progressive\", extension: \"jpeg\")\n        let progressiveContext = ImageDecodingContext.mock(data: progressiveData)\n        #expect(ImageDecoders.Default(context: progressiveContext) != nil)\n    }\n\n    @Test func defaultPreviewPolicy() {\n        // Progressive JPEG → .incremental\n        let progressiveData = Test.data(name: \"progressive\", extension: \"jpeg\")\n        #expect(ImagePipeline.PreviewPolicy.default(for: progressiveData) == .incremental)\n\n        // Baseline JPEG → .disabled\n        let baselineData = Test.data(name: \"baseline\", extension: \"jpeg\")\n        #expect(ImagePipeline.PreviewPolicy.default(for: baselineData) == .disabled)\n\n        // PNG → .disabled\n        let pngData = Test.data(name: \"fixture\", extension: \"png\")\n        #expect(ImagePipeline.PreviewPolicy.default(for: pngData) == .disabled)\n\n        // GIF → .incremental\n        let gifData = Test.data(name: \"cat\", extension: \"gif\")\n        #expect(ImagePipeline.PreviewPolicy.default(for: gifData) == .incremental)\n    }\n\n    @Test func decodingTrickyProgressiveJPEG() throws {\n        let data = Test.data(name: \"tricky_progressive\", extension: \"jpeg\")\n        let decoder = ImageDecoders.Default()\n\n        // This progressive JPEG has a ~7 KB EXIF header (SOF2 at offset 7394).\n        // CGImageSourceCreateIncremental fails to produce images until enough\n        // data past SOF2 is available. With small chunks, the thumbnail\n        // fallback kicks in first.\n        #expect(decoder.decodePartiallyDownloadedData(data[0...2000]) == nil)\n\n        // With enough data, the decoder produces a preview (either via\n        // thumbnail fallback or incremental decoding).\n        let preview = decoder.decodePartiallyDownloadedData(data[0...8000])\n        #expect(preview != nil)\n        #expect(preview?.isPreview == true)\n        #expect(decoder.numberOfScans == 1)\n\n        // Full decode at full resolution\n        let container = try decoder.decode(data)\n        #expect(container.image.sizeInPixels == CGSize(width: 450, height: 300))\n    }\n\n    @Test func decodeGIF() throws {\n        // Given\n        let data = Test.data(name: \"cat\", extension: \"gif\")\n        let decoder = ImageDecoders.Default()\n\n        // When\n        let container = try decoder.decode(data)\n\n        // Then\n        #expect(container.type == .gif)\n        #expect(!container.isPreview)\n        #expect(container.data != nil)\n        #expect(container.userInfo.isEmpty)\n    }\n\n    @Test func decodeHEIC() throws {\n        // Given\n        let data = Test.data(name: \"img_751\", extension: \"heic\")\n        let decoder = ImageDecoders.Default()\n\n        // When\n        let container = try decoder.decode(data)\n\n        // Then\n        #expect(container.type == AssetType.heic)\n        #expect(!container.isPreview)\n        #expect(container.data == nil)\n        #expect(container.userInfo.isEmpty)\n    }\n\n    @Test func decodeICO() throws {\n        // Given\n        let data = Test.data(name: \"fixture\", extension: \"ico\")\n        let decoder = ImageDecoders.Default()\n\n        // When\n        let container = try decoder.decode(data)\n\n        // Then\n        #expect(container.type == AssetType.ico)\n        #expect(!container.isPreview)\n        #expect(container.data == nil)\n        #expect(container.userInfo.isEmpty)\n        #expect(container.image.sizeInPixels == CGSize(width: 32, height: 32))\n    }\n\n    @Test func decodingGIFDataAttached() throws {\n        let data = Test.data(name: \"cat\", extension: \"gif\")\n        #expect(try ImageDecoders.Default().decode(data).data != nil)\n    }\n\n    @Test func decodingGIFPreview() throws {\n        let data = Test.data(name: \"cat\", extension: \"gif\")\n        #expect(data.count == 427672) // 427 KB\n        let chunk = data[...60000] // 6 KB\n        let response = try ImageDecoders.Default().decode(chunk)\n        #expect(response.image.sizeInPixels == CGSize(width: 500, height: 279))\n    }\n\n    @Test func decodingGIFPreviewGeneratedOnlyOnce() throws {\n        let data = Test.data(name: \"cat\", extension: \"gif\")\n        #expect(data.count == 427672) // 427 KB\n        let chunk = data[...60000] // 6 KB\n\n        let context = ImageDecodingContext.mock(data: chunk)\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        #expect(decoder.decodePartiallyDownloadedData(chunk) != nil)\n        #expect(decoder.decodePartiallyDownloadedData(chunk) == nil)\n    }\n\n    @Test func decodingPNGDataNotAttached() throws {\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n        let container = try ImageDecoders.Default().decode(data)\n        #expect(container.data == nil)\n    }\n\n#if os(iOS) || os(macOS) || os(visionOS)\n    @Test func decodeBaselineWebP() throws {\n        let data = Test.data(name: \"baseline\", extension: \"webp\")\n        let container = try ImageDecoders.Default().decode(data)\n        #expect(container.image.sizeInPixels == CGSize(width: 550, height: 368))\n        #expect(container.data == nil)\n    }\n#endif\n\n    // MARK: - Downscaling\n\n    @Test func downscalingWhenOverLimit() throws {\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n\n        // 640×360 = 230,400 pixels × 4 = 921,600 bytes decoded.\n        // Set a limit well below that to force downscaling.\n        var context = ImageDecodingContext.mock(data: data)\n        context.maximumDecodedImageSize = 40_000 // ~10,000 pixels\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        let container = try decoder.decode(data)\n        let size = container.image.sizeInPixels\n        #expect(size.width < 640)\n        #expect(size.height < 360)\n    }\n\n    @Test func downscalingSkippedWhenUnderLimit() throws {\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n\n        // Set a limit above the decoded size (640×360×4 = 921,600 bytes)\n        var context = ImageDecodingContext.mock(data: data)\n        context.maximumDecodedImageSize = 2_000_000\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        let container = try decoder.decode(data)\n        #expect(container.image.sizeInPixels == CGSize(width: 640, height: 360))\n    }\n\n    @Test func downscalingDisabledWhenNil() throws {\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n\n        var context = ImageDecodingContext.mock(data: data)\n        context.maximumDecodedImageSize = nil\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        let container = try decoder.decode(data)\n        #expect(container.image.sizeInPixels == CGSize(width: 640, height: 360))\n    }\n\n    @Test func downscalingSkippedWhenThumbnailSet() throws {\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n\n        var context = ImageDecodingContext.mock(data: data)\n        context.maximumDecodedImageSize = 40_000\n        context.request = ImageRequest(url: Test.url).with {\n            $0.thumbnail = .init(\n                size: CGSize(width: 100, height: 100),\n                unit: .pixels,\n                contentMode: .aspectFit\n            )\n        }\n        let decoder = try #require(ImageDecoders.Default(context: context))\n\n        // Thumbnail options take priority — downscaling is not applied\n        let container = try decoder.decode(data)\n        let size = container.image.sizeInPixels\n        #expect(size.width <= 100)\n        #expect(size.height <= 100)\n    }\n\n    // MARK: - Invalid / Corrupted Data\n\n    @Test func decodeRandomDataThrows() {\n        // GIVEN - bytes that share no resemblance with any image format\n        let data = Data(repeating: 0xAB, count: 512)\n        let decoder = ImageDecoders.Default()\n\n        // WHEN / THEN - decoding must throw (not crash)\n        #expect(throws: (any Error).self) {\n            try decoder.decode(data)\n        }\n    }\n\n    @Test func decodeEmptyDataThrows() {\n        let decoder = ImageDecoders.Default()\n        #expect(throws: (any Error).self) {\n            try decoder.decode(Data())\n        }\n    }\n\n    @Test func partialDataReturnsNilForUnsupportedFormat() {\n        // GIVEN - only 2 bytes of PNG data (not enough to decode)\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n        let decoder = ImageDecoders.Default()\n\n        // WHEN - attempt partial decode with too-little data\n        let preview = decoder.decodePartiallyDownloadedData(data[0..<2])\n\n        // THEN - no preview produced for this tiny slice\n        #expect(preview == nil)\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageTypeTests {\n    // MARK: PNG\n\n    @Test func detectPNG() {\n        let data = Test.data(name: \"fixture\", extension: \"png\")\n        #expect(AssetType(data[0..<1]) == nil)\n        #expect(AssetType(data[0..<7]) == nil)\n        #expect(AssetType(data[0..<8]) == .png)\n        #expect(AssetType(data) == .png)\n    }\n\n    // MARK: GIF\n\n    @Test func detectGIF() {\n        let data = Test.data(name: \"cat\", extension: \"gif\")\n        #expect(AssetType(data) == .gif)\n    }\n\n    // MARK: JPEG\n\n    @Test func detectBaselineJPEG() {\n        let data = Test.data(name: \"baseline\", extension: \"jpeg\")\n        #expect(AssetType(data[0..<1]) == nil)\n        #expect(AssetType(data[0..<2]) == nil)\n        #expect(AssetType(data[0..<3]) == .jpeg)\n        #expect(AssetType(data) == .jpeg)\n    }\n\n    @Test func detectProgressiveJPEG() {\n        let data = Test.data(name: \"progressive\", extension: \"jpeg\")\n        // Not enough data\n        #expect(AssetType(Data()) == nil)\n        #expect(AssetType(data[0..<2]) == nil)\n\n        // Enough to determine image format\n        #expect(AssetType(data[0..<3]) == .jpeg)\n        #expect(AssetType(data[0..<33]) == .jpeg)\n\n        // Full image\n        #expect(AssetType(data) == .jpeg)\n    }\n\n    // MARK: ICO\n\n    @Test func detectICO() {\n        let data = Test.data(name: \"fixture\", extension: \"ico\")\n        #expect(AssetType(data[0..<1]) == nil)\n        #expect(AssetType(data[0..<3]) == nil)\n        #expect(AssetType(data[0..<4]) == .ico)\n        #expect(AssetType(data) == .ico)\n    }\n\n    // MARK: WebP\n\n    @Test func detectBaselineWebP() {\n        let data = Test.data(name: \"baseline\", extension: \"webp\")\n        #expect(AssetType(data[0..<1]) == nil)\n        #expect(AssetType(data[0..<2]) == nil)\n        #expect(AssetType(data[0..<12]) == .webp)\n        #expect(AssetType(data) == .webp)\n    }\n\n    // MARK: HEIC\n\n    @Test func detectHEIC() {\n        let data = Test.data(name: \"img_751\", extension: \"heic\")\n        // HEIC detection requires the ftyp box at byte offset 4 (8 bytes),\n        // so 12 bytes total are needed. Shorter slices must return nil.\n        #expect(AssetType(data[0..<1]) == nil)\n        #expect(AssetType(data[0..<11]) == nil) // one byte short of the required 12\n        // Exactly 12 bytes — enough to identify HEIC\n        #expect(AssetType(data[0..<12]) == .heic)\n        // Full data\n        #expect(AssetType(data) == .heic)\n    }\n\n    // MARK: Edge Cases\n\n    @Test func detectEmptyData() {\n        #expect(AssetType(Data()) == nil)\n    }\n\n    @Test func detectUnknownFormat() {\n        // Random bytes that don't match any known format\n        let data = Data([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07])\n        #expect(AssetType(data) == nil)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageDecodersEmptyTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageDecodersEmptyTests {\n    @Test func isAsynchronousReturnsFalse() {\n        let decoder = ImageDecoders.Empty()\n        #expect(decoder.isAsynchronous == false)\n    }\n\n    @Test func isProgressiveDefaultIsFalse() {\n        let decoder = ImageDecoders.Empty()\n        #expect(decoder.isProgressive == false)\n    }\n\n    @Test func isProgressiveWhenEnabled() {\n        let decoder = ImageDecoders.Empty(isProgressive: true)\n        #expect(decoder.isProgressive == true)\n    }\n\n    @Test func decodeReturnsContainerWithData() throws {\n        let decoder = ImageDecoders.Empty()\n        let data = Data(\"test-data\".utf8)\n\n        let container = try decoder.decode(data)\n\n        #expect(container.data == data)\n        #expect(container.type == nil)\n        #expect(container.userInfo.isEmpty)\n    }\n\n    @Test func decodeWithAssetType() throws {\n        let decoder = ImageDecoders.Empty(assetType: .png)\n        let data = Data(\"test\".utf8)\n\n        let container = try decoder.decode(data)\n\n        #expect(container.type == .png)\n        #expect(container.data == data)\n    }\n\n    @Test func decodePartiallyDownloadedDataReturnsNilWhenNotProgressive() {\n        let decoder = ImageDecoders.Empty(isProgressive: false)\n        let data = Data(\"partial\".utf8)\n\n        let result = decoder.decodePartiallyDownloadedData(data)\n\n        #expect(result == nil)\n    }\n\n    @Test func decodePartiallyDownloadedDataReturnsContainerWhenProgressive() {\n        let decoder = ImageDecoders.Empty(assetType: .jpeg, isProgressive: true)\n        let data = Data(\"partial\".utf8)\n\n        let result = decoder.decodePartiallyDownloadedData(data)\n\n        #expect(result != nil)\n        #expect(result?.data == data)\n        #expect(result?.type == .jpeg)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageEncoderTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageEncoderTests {\n    @Test func encodeImage() throws {\n        // Given\n        let image = Test.image\n        let encoder = ImageEncoders.Default()\n\n        // When\n        let data = try #require(encoder.encode(image))\n\n        // Then\n        #expect(AssetType(data) == .jpeg)\n    }\n\n    @Test func encodeImagePNGOpaque() throws {\n        // Given\n        let image = Test.image(named: \"fixture\", extension: \"png\")\n        let encoder = ImageEncoders.Default()\n\n        // When\n        let data = try #require(encoder.encode(image))\n\n        // Then\n#if os(macOS)\n        // It seems that on macOS, NSImage created from png has an alpha\n        // component regardless of whether the input image has it.\n        #expect(AssetType(data) == .png)\n#else\n        #expect(AssetType(data) == .jpeg)\n#endif\n    }\n\n    @Test func encodeImagePNGTransparent() throws {\n        // Given\n        let image = Test.image(named: \"swift\", extension: \"png\")\n        let encoder = ImageEncoders.Default()\n\n        // When\n        let data = try #require(encoder.encode(image))\n\n        // Then\n        #expect(AssetType(data) == .png)\n    }\n\n    @Test func prefersHEIF() throws {\n        // Given\n        let image = Test.image\n        var encoder = ImageEncoders.Default()\n        encoder.isHEIFPreferred = true\n\n        // When\n        let data = try #require(encoder.encode(image))\n\n        // Then\n        #expect(AssetType(data) == AssetType.heic)\n    }\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n\n    @Test func encodeCoreImageBackedImage() throws {\n        // Given\n        let image = try ImageProcessors.GaussianBlur().processThrowing(Test.image)\n        let encoder = ImageEncoders.Default()\n\n        // When\n        let data = try #require(encoder.encode(image))\n\n        // Then encoded as PNG because GaussianBlur produces\n        // images with alpha channel\n        #expect(AssetType(data) == .png)\n    }\n\n#endif\n\n    // MARK: - Compression Quality\n\n    @Test func higherCompressionQualityProducesLargerJPEG() throws {\n        // GIVEN\n        let lowQualityEncoder = ImageEncoders.Default(compressionQuality: 0.1)\n        let highQualityEncoder = ImageEncoders.Default(compressionQuality: 0.9)\n\n        // WHEN\n        let lowData = try #require(lowQualityEncoder.encode(Test.image))\n        let highData = try #require(highQualityEncoder.encode(Test.image))\n\n        // THEN - higher quality produces more bytes\n        #expect(highData.count > lowData.count)\n    }\n\n    @Test func encodeDecodedRoundTrip() throws {\n        // GIVEN - encode an image to JPEG\n        let encoder = ImageEncoders.Default(compressionQuality: 0.8)\n        let encoded = try #require(encoder.encode(Test.image))\n        #expect(AssetType(encoded) == .jpeg)\n\n        // WHEN - decode it back\n        let decoder = ImageDecoders.Default()\n        let container = try decoder.decode(encoded)\n\n        // THEN - decoded image is valid\n        #expect(container.type == .jpeg)\n    }\n\n    // MARK: - Misc\n\n    @Test func isOpaqueWithOpaquePNG() {\n        let image = Test.image(named: \"fixture\", extension: \"png\")\n#if os(macOS)\n        #expect(!image.cgImage!.isOpaque)\n#else\n        #expect(image.cgImage!.isOpaque)\n#endif\n    }\n\n    @Test func isOpaqueWithTransparentPNG() {\n        let image = Test.image(named: \"swift\", extension: \"png\")\n        #expect(!image.cgImage!.isOpaque)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageEncodingTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageEncodingProtocolTests {\n\n    // MARK: - Default encode(container:context:) for GIF pass-through\n\n    @Test func encodeContainerPassesThroughGIFData() {\n        let encoder = ImageEncoders.Default()\n        let gifData = Test.data(name: \"cat\", extension: \"gif\")\n        let container = ImageContainer(image: Test.image, type: .gif, data: gifData)\n        let context = ImageEncodingContext(\n            request: Test.request,\n            image: Test.image,\n            urlResponse: nil\n        )\n\n        let result = encoder.encode(container, context: context)\n        #expect(result == gifData)\n    }\n\n    @Test func encodeContainerEncodesNonGIFNormally() throws {\n        let encoder = ImageEncoders.Default()\n        let container = ImageContainer(image: Test.image, type: .jpeg)\n        let context = ImageEncodingContext(\n            request: Test.request,\n            image: Test.image,\n            urlResponse: nil\n        )\n\n        let result = try #require(encoder.encode(container, context: context))\n        #expect(!result.isEmpty)\n    }\n\n    // MARK: - Factory methods\n\n    @Test func defaultFactoryMethod() throws {\n        let encoder: ImageEncoders.Default = .default()\n        let data = try #require(encoder.encode(Test.image))\n        #expect(!data.isEmpty)\n    }\n\n    @Test func defaultFactoryMethodWithCompression() throws {\n        let encoder: ImageEncoders.Default = .default(compressionQuality: 0.5)\n        let data = try #require(encoder.encode(Test.image))\n        #expect(!data.isEmpty)\n    }\n\n    @Test func imageIOFactoryMethod() throws {\n        let encoder: ImageEncoders.ImageIO = .imageIO(type: .png)\n        let data = try #require(encoder.encode(Test.image))\n        #expect(AssetType(data) == .png)\n    }\n\n    @Test func imageIOFactoryMethodWithCompression() throws {\n        let encoder: ImageEncoders.ImageIO = .imageIO(type: .jpeg, compressionRatio: 0.5)\n        let data = try #require(encoder.encode(Test.image))\n        #expect(!data.isEmpty)\n    }\n\n    // MARK: - GIF Pass-Through Edge Cases\n\n    @Test func gifContainerWithoutDataReturnsNil() throws {\n        // GIVEN a GIF-typed container with no associated data (animation data lost)\n        let encoder = ImageEncoders.Default()\n        let container = ImageContainer(image: Test.image, type: .gif, data: nil)\n        let context = ImageEncodingContext(\n            request: Test.request,\n            image: Test.image,\n            urlResponse: nil\n        )\n\n        // WHEN\n        let result = encoder.encode(container, context: context)\n\n        // THEN returns nil — GIF encoding requires the original animated data\n        #expect(result == nil)\n    }\n\n    // MARK: - Context\n\n    @Test func encodingContextContainsExpectedValues() {\n        let context = ImageEncodingContext(\n            request: Test.request,\n            image: Test.image,\n            urlResponse: nil\n        )\n\n        #expect(context.request.url == Test.url)\n        #expect(context.urlResponse == nil)\n    }\n\n    // MARK: - ImageEncoders.ImageIO\n\n    @Test func imageIOEncoderProducesJPEGData() throws {\n        let encoder = ImageEncoders.ImageIO(type: .jpeg)\n        let data = try #require(encoder.encode(Test.image))\n        #expect(AssetType(data) == .jpeg)\n    }\n\n    @Test func imageIOEncoderProducesPNGData() throws {\n        let encoder = ImageEncoders.ImageIO(type: .png)\n        let data = try #require(encoder.encode(Test.image))\n        #expect(AssetType(data) == .png)\n    }\n\n    @Test func imageIOEncoderIsSupportedForJPEG() {\n        #expect(ImageEncoders.ImageIO.isSupported(type: .jpeg))\n    }\n\n    @Test func imageIOEncoderIsSupportedForPNG() {\n        #expect(ImageEncoders.ImageIO.isSupported(type: .png))\n    }\n\n    @Test func imageIOHigherCompressionProducesLargerData() throws {\n        let lowQuality = ImageEncoders.ImageIO(type: .jpeg, compressionRatio: 0.1)\n        let highQuality = ImageEncoders.ImageIO(type: .jpeg, compressionRatio: 0.9)\n        let lowData = try #require(lowQuality.encode(Test.image))\n        let highData = try #require(highQuality.encode(Test.image))\n        #expect(highData.count > lowData.count)\n    }\n\n    @Test func imageIOEncoderDefaultCompressionRatio() {\n        let encoder = ImageEncoders.ImageIO(type: .jpeg)\n        #expect(encoder.compressionRatio == 0.8)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineErrorTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineErrorTests {\n\n    // MARK: - dataLoadingError\n\n    @Test func dataLoadingErrorReturnsUnderlyingError() {\n        let underlying = URLError(.notConnectedToInternet)\n        let error = ImagePipeline.Error.dataLoadingFailed(error: underlying)\n\n        let result = error.dataLoadingError as? URLError\n        #expect(result?.code == .notConnectedToInternet)\n    }\n\n    @Test func dataLoadingErrorReturnsNilForOtherCases() {\n        let cases: [ImagePipeline.Error] = [\n            .dataMissingInCache,\n            .dataIsEmpty,\n            .imageRequestMissing,\n            .pipelineInvalidated,\n            .dataDownloadExceededMaximumSize,\n        ]\n        for error in cases {\n            #expect(error.dataLoadingError == nil)\n        }\n    }\n\n    // MARK: - Descriptions\n\n    @Test func dataMissingInCacheDescription() {\n        let error = ImagePipeline.Error.dataMissingInCache\n        #expect(error.description.contains(\"cache\"))\n    }\n\n    @Test func dataLoadingFailedDescription() {\n        let underlying = URLError(.timedOut)\n        let error = ImagePipeline.Error.dataLoadingFailed(error: underlying)\n        #expect(error.description.contains(\"Failed to load image data\"))\n    }\n\n    @Test func dataIsEmptyDescription() {\n        let error = ImagePipeline.Error.dataIsEmpty\n        #expect(error.description.contains(\"empty\"))\n    }\n\n    @Test func decoderNotRegisteredDescription() {\n        let error = ImagePipeline.Error.decoderNotRegistered(context: .mock)\n        #expect(error.description.contains(\"No decoders\"))\n    }\n\n    @Test func decodingFailedWithImageDecodingError() {\n        let decoder = ImageDecoders.Default()\n        let error = ImagePipeline.Error.decodingFailed(\n            decoder: decoder,\n            context: .mock,\n            error: ImageDecodingError.unknown\n        )\n        // Should NOT contain \"Underlying error\" for ImageDecodingError\n        #expect(!error.description.contains(\"Underlying error\"))\n        #expect(error.description.contains(\"Failed to decode\"))\n    }\n\n    @Test func decodingFailedWithCustomError() {\n        struct CustomError: Error {}\n        let decoder = ImageDecoders.Default()\n        let error = ImagePipeline.Error.decodingFailed(\n            decoder: decoder,\n            context: .mock,\n            error: CustomError()\n        )\n        // Should contain \"Underlying error\" for non-ImageDecodingError\n        #expect(error.description.contains(\"Underlying error\"))\n    }\n\n    @Test func processingFailedWithImageProcessingError() {\n        let processor = ImageProcessors.Resize(width: 100)\n        let error = ImagePipeline.Error.processingFailed(\n            processor: processor,\n            context: .mock,\n            error: ImageProcessingError.unknown\n        )\n        // Should NOT contain \"Underlying error\" for ImageProcessingError\n        #expect(!error.description.contains(\"Underlying error\"))\n        #expect(error.description.contains(\"Failed to process\"))\n    }\n\n    @Test func processingFailedWithCustomError() {\n        struct CustomError: Error {}\n        let processor = ImageProcessors.Resize(width: 100)\n        let error = ImagePipeline.Error.processingFailed(\n            processor: processor,\n            context: .mock,\n            error: CustomError()\n        )\n        #expect(error.description.contains(\"Underlying error\"))\n    }\n\n    @Test func imageRequestMissingDescription() {\n        let error = ImagePipeline.Error.imageRequestMissing\n        #expect(error.description.contains(\"no image request\"))\n    }\n\n    @Test func pipelineInvalidatedDescription() {\n        let error = ImagePipeline.Error.pipelineInvalidated\n        #expect(error.description.contains(\"invalidated\"))\n    }\n\n    @Test func dataDownloadExceededMaximumSizeDescription() {\n        let error = ImagePipeline.Error.dataDownloadExceededMaximumSize\n        #expect(error.description.contains(\"exceeded\"))\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/DeprecatedTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct DeprecationTests {\n    private let pipeline: ImagePipeline\n    private let dataLoader: MockDataLoader\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    // MARK: - loadImage\n\n    @Test func loadImageWithURL() async {\n        dataLoader.isSuspended = true\n        let result: Result<ImageResponse, ImagePipeline.Error> = await withCheckedContinuation { continuation in\n            pipeline.loadImage(with: Test.url) { result in\n                continuation.resume(returning: result)\n            }\n            dataLoader.isSuspended = false\n        }\n        #expect(result.isSuccess)\n    }\n\n    @Test func loadImageWithRequest() async {\n        dataLoader.isSuspended = true\n        let taskRef = Ref<ImageTask?>(nil)\n        let stateInsideCallback = Ref<ImageTask.State>(.running)\n        let result: Result<ImageResponse, ImagePipeline.Error> = await withCheckedContinuation { continuation in\n            taskRef.value = pipeline.loadImage(with: Test.request) { result in\n                stateInsideCallback.value = taskRef.value!.state\n                continuation.resume(returning: result)\n            }\n            dataLoader.isSuspended = false\n        }\n        #expect(result.isSuccess)\n        #expect(stateInsideCallback.value == .completed)\n    }\n\n    @Test func loadImageCompletionOnMainThread() async {\n        dataLoader.isSuspended = true\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            pipeline.loadImage(with: Test.request) { _ in\n                #expect(Thread.isMainThread)\n                continuation.resume()\n            }\n            dataLoader.isSuspended = false\n        }\n    }\n\n    @Test func loadImageProgress() async {\n        dataLoader.results[Test.url] = .success(\n            (Data(count: 20), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        let progressValues = Ref<[(Int64, Int64)]>([])\n        dataLoader.isSuspended = true\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            pipeline.loadImage(\n                with: Test.request,\n                progress: { _, completed, total in\n                    #expect(Thread.isMainThread)\n                    progressValues.value.append((completed, total))\n                },\n                completion: { _ in continuation.resume() }\n            )\n            dataLoader.isSuspended = false\n        }\n        #expect(progressValues.value.count == 2)\n        #expect(progressValues.value[0] == (10, 20))\n        #expect(progressValues.value[1] == (20, 20))\n    }\n\n    @Test func loadImageCancellation() async {\n        dataLoader.isSuspended = true\n        let task = await withCheckedContinuation { (continuation: CheckedContinuation<ImageTask, Never>) in\n            let task = pipeline.loadImage(with: Test.request) { _ in\n                Issue.record(\"Should not be called\")\n            }\n            Task { @ImagePipelineActor in\n                task.cancel()\n                continuation.resume(returning: task)\n            }\n        }\n        #expect(task.state == .cancelled)\n    }\n\n    @Test func loadImageCancellationCompletionNotCalled() async {\n        dataLoader.isSuspended = true\n        let completionCalled = Ref(false)\n        let task = pipeline.loadImage(with: Test.request) { _ in\n            completionCalled.value = true\n        }\n        task.cancel()\n        // Wait for the pipeline actor to process cancellation and dispatch events\n        await Task { @ImagePipelineActor in }.value\n        // Simulate data arriving after cancellation\n        dataLoader.isSuspended = false\n        // Flush any DispatchQueue.main.async callbacks\n        await MainActor.run {}\n        #expect(!completionCalled.value)\n        #expect(task.state == .cancelled)\n    }\n\n    // MARK: - loadData\n\n    @Test func loadData() async {\n        dataLoader.isSuspended = true\n        let result: Result<(data: Data, response: URLResponse?), ImagePipeline.Error> = await withCheckedContinuation { continuation in\n            pipeline.loadData(with: Test.request) { result in\n                continuation.resume(returning: result)\n            }\n            dataLoader.isSuspended = false\n        }\n        #expect((try? result.get().data.count) == 22789)\n    }\n\n    @Test func loadDataCompletionOnMainThread() async {\n        dataLoader.isSuspended = true\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            pipeline.loadData(with: Test.request) { _ in\n                #expect(Thread.isMainThread)\n                continuation.resume()\n            }\n            dataLoader.isSuspended = false\n        }\n    }\n\n    @Test func loadDataProgress() async {\n        dataLoader.results[Test.url] = .success(\n            (Data(count: 20), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        let progressValues = Ref<[(Int64, Int64)]>([])\n        dataLoader.isSuspended = true\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            pipeline.loadData(\n                with: Test.request,\n                progress: { completed, total in\n                    #expect(Thread.isMainThread)\n                    progressValues.value.append((completed, total))\n                },\n                completion: { _ in continuation.resume() }\n            )\n            dataLoader.isSuspended = false\n        }\n        #expect(progressValues.value.count == 2)\n        #expect(progressValues.value[0] == (10, 20))\n        #expect(progressValues.value[1] == (20, 20))\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/DocumentationTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\nimport Nuke\n\n#if os(iOS) || os(visionOS)\n\nimport UIKit\n\nprivate let pipeline = ImagePipeline.shared\nprivate let url = URL(string: \"https://example.com/image.jpeg\")!\nprivate let image = Test.image\nprivate let data = Test.data\n@MainActor private let imageView = UIImageView()\n\n// MARK: - Getting Started\n\nprivate func checkGettingStarted01() async throws {\n    let image = try await ImagePipeline.shared.image(for: url)\n    _ = image\n}\n\nprivate final class CheckGettingStarted02 {\n    @MainActor\n    func loadImage() async throws {\n        let imageTask = ImagePipeline.shared.imageTask(with: url)\n        for await progress in imageTask.progress {\n            // Update progress\n            _ = progress\n        }\n        imageView.image = try await imageTask.image\n    }\n}\n\nprivate func checkGettingStarted03() async throws {\n    let request = ImageRequest(\n        url: URL(string: \"http://example.com/image.jpeg\"),\n        processors: [.resize(width: 320)],\n        priority: .high,\n        options: [.reloadIgnoringCachedData]\n    )\n    let image = try await pipeline.image(for: request)\n\n    _ = image\n}\n\nprivate func checkGettingStarted04() {\n    ImagePipeline.shared = ImagePipeline(configuration: .withDataCache)\n}\n\n// MARK: - Access Cached Images\n\nprivate func checkAccessCachedImages01() async throws {\n    let request = ImageRequest(url: url, options: [.returnCacheDataDontLoad])\n    let response = try await pipeline.imageTask(with: request).response\n    let cacheType = response.cacheType // .memory, .disk, or nil\n\n    _ = cacheType\n}\n\nprivate func checkAccessCachedImages02() {\n    let request = ImageRequest(url: url)\n    pipeline.cache.removeCachedImage(for: request)\n}\n\nprivate func checkAccessCachedImages03() async throws {\n    let request = ImageRequest(url: url, options: [ .reloadIgnoringCachedData])\n    let image = try await pipeline.image(for: request)\n\n    _ = image\n}\n\nprivate func checkAccessCachedImages04() {\n    let image = pipeline.cache[URL(string: \"https://example.com/image.jpeg\")!]\n    pipeline.cache[ImageRequest(url: url)] = nil\n\n    _ = image\n}\n\nprivate func checkAccessCachedImages05() {\n    let url = URL(string: \"https://example.com/image.jpeg\")!\n    pipeline.cache[url] = ImageContainer(image: image)\n\n    // Returns `nil` because memory cache reads are disabled\n    let request = ImageRequest(url: url, options: [.disableMemoryCacheWrites])\n    let image = pipeline.cache[request]\n\n    _ = image\n}\n\nprivate func checkAccessCachedImages06() {\n    let cache = pipeline.cache\n    let request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\")!)\n\n    _ = cache.cachedImage(for: request) // From any cache layer\n    _ = cache.cachedImage(for: request, caches: [.memory]) // Only memory\n    _ = cache.cachedImage(for: request, caches: [.disk]) // Only disk (decodes data)\n\n    let data = cache.cachedData(for: request)\n    _ = cache.containsData(for: request) // Fast contains check\n\n    // Stores image in the memory cache and stores an encoded\n    // image in the disk cache\n    cache.storeCachedImage(ImageContainer(image: image), for: request)\n\n    cache.removeCachedImage(for: request)\n    cache.removeAll()\n\n    _ = data\n}\n\nprivate func checkAccessCachedImages07() {\n    let request = ImageRequest(url: URL(string: \"https://example.com/image.jpeg\"))\n    _ = pipeline.cache.makeImageCacheKey(for: request)\n    _ = pipeline.cache.makeDataCacheKey(for: request)\n}\n\nprivate final class CheckAccessCachedImages08: ImagePipeline.Delegate {\n    func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? {\n        request.userInfo[\"imageId\"] as? String\n    }\n}\n\n// MARK: - Cache Configuration\n\nprivate func checkCacheConfiguration01() {\n    ImagePipeline.shared = ImagePipeline(configuration: .withDataCache)\n}\n\n// MARK: - Cache Layers\n\nprivate func checkCacheLayers01() {\n    // Configure cache\n    ImageCache.shared.costLimit = 1024 * 1024 * 100 // 100 MB\n    ImageCache.shared.countLimit = 100\n    ImageCache.shared.ttl = 120 // Invalidate image after 120 sec\n\n    // Read and write images\n    let request = ImageRequest(url: url)\n    ImageCache.shared[request] = ImageContainer(image: image)\n    let image = ImageCache.shared[request]\n\n    // Clear cache\n    ImageCache.shared.removeAll()\n\n    _ = image\n}\n\nprivate func checkCacheLayers02() {\n    // Configure cache\n    DataLoader.sharedUrlCache.diskCapacity = 100\n    DataLoader.sharedUrlCache.memoryCapacity = 0\n\n    // Read and write responses\n    let urlRequest = URLRequest(url: url)\n    _ = DataLoader.sharedUrlCache.cachedResponse(for: urlRequest)\n    DataLoader.sharedUrlCache.removeCachedResponse(for: urlRequest)\n\n    // Clear cache\n    DataLoader.sharedUrlCache.removeAllCachedResponses()\n}\n\nprivate func checkCacheLayers03() {\n    _ = ImagePipeline {\n        $0.dataCache = try? DataCache(name: \"com.myapp.datacache\")\n    }\n}\n\nprivate func checkCacheLayers04() throws {\n    let dataCache = try DataCache(name: \"my-cache\")\n\n    dataCache.sizeLimit = 1024 * 1024 * 100 // 100 MB\n\n    dataCache.storeData(data, for: \"key\")\n    if dataCache.containsData(for: \"key\") {\n        print(\"Data is cached\")\n    }\n    let data = dataCache.cachedData(for: \"key\")\n    // or let data = dataCache[\"key\"]\n    dataCache.removeData(for: \"key\")\n    dataCache.removeAll()\n\n    _ = data\n}\n\nprivate func checkCacheLayers05() throws {\n    let dataCache = try DataCache(name: \"my-cache\")\n\n    dataCache.storeData(data, for: \"key\")\n    dataCache.flush()\n    // or dataCache.flush(for: \"key\")\n\n    let url = dataCache.url(for: \"key\")\n    // Access file directly\n    _ = url\n}\n\n// MARK: - Performance\n\nprivate func checkPerformance01() {\n    // Target size is in points.\n    let request = ImageRequest(\n        url: URL(string: \"http://...\"),\n        processors: [.resize(width: 320)]\n    )\n\n    _ = request\n}\n\nprivate func checkPerformance02() {\n    final class ImageView: UIView {\n        private var task: ImageTask?\n\n        override func willMove(toWindow newWindow: UIWindow?) {\n            super.willMove(toWindow: newWindow)\n\n            task?.priority = newWindow == nil ? .low : .high\n        }\n    }\n}\n\nprivate func checkPerformance03() async throws {\n    let url = URL(string: \"http://example.com/image\")\n    async let first = pipeline.image(for: ImageRequest(url: url, processors: [\n        .resize(size: CGSize(width: 44, height: 44)),\n        .gaussianBlur(radius: 8)\n    ]))\n    async let second = pipeline.image(for: ImageRequest(url: url, processors: [\n        .resize(size: CGSize(width: 44, height: 44))\n    ]))\n    let images = try await (first, second)\n\n    _ = images\n}\n\n// MARK: - Prefetching\n\nprivate final class PrefetchingDemoViewController: UICollectionViewController {\n    private let prefetcher = ImagePrefetcher()\n    private var photos: [URL] = []\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        collectionView?.isPrefetchingEnabled = true\n        collectionView?.prefetchDataSource = self\n    }\n\n    override func viewWillAppear(_ animated: Bool) {\n        super.viewWillAppear(animated)\n\n        prefetcher.isPaused = false\n    }\n\n    override func viewWillDisappear(_ animated: Bool) {\n        super.viewWillDisappear(animated)\n\n        // When you pause, the prefetcher will finish outstanding tasks\n        // (by default, there are only 2 at a time), and pause the rest.\n        prefetcher.isPaused = true\n    }\n}\n\nextension PrefetchingDemoViewController: UICollectionViewDataSourcePrefetching {\n    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {\n        let urls = indexPaths.map { photos[$0.row] }\n        prefetcher.startPrefetching(with: urls)\n    }\n\n    func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {\n        let urls = indexPaths.map { photos[$0.row] }\n        prefetcher.startPrefetching(with: urls)\n    }\n}\n\n// MARK: - ImagePipeline (Extension)\n\nprivate func checkImagePipelineExtension01() {\n    _ = ImagePipeline {\n        $0.dataCache = try? DataCache(name: \"com.myapp.datacache\")\n        $0.dataCachePolicy = .automatic\n    }\n}\n\nprivate func checkImagePipelineExtension02() async throws {\n    let image = try await ImagePipeline.shared.image(for: url)\n    _ = image\n}\n\nprivate final class AsyncImageView: UIImageView {\n    func loadImage() async throws {\n        let imageTask = ImagePipeline.shared.imageTask(with: url)\n        for await progress in imageTask.progress {\n            // Update progress\n            _ = progress\n        }\n        imageView.image = try await imageTask.image\n    }\n}\n#endif\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineAsyncAwaitTests {\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n    let pipelineDelegate: ImagePipelineObserver\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let pipelineDelegate = ImagePipelineObserver()\n        self.dataLoader = dataLoader\n        self.pipelineDelegate = pipelineDelegate\n        self.pipeline = ImagePipeline(delegate: pipelineDelegate) {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    // MARK: - Basics\n\n    @Test func imageIsLoaded() async throws {\n        // WHEN\n        let image = try await pipeline.image(for: Test.request)\n\n        // THEN\n        #expect(image.sizeInPixels == CGSize(width: 640, height: 480))\n    }\n\n    // MARK: - Task-based API\n\n    @Test func taskBasedImageResponse() async throws {\n        // GIVEN\n        let task = pipeline.imageTask(with: Test.request)\n\n        // WHEN\n        let response = try await task.response\n\n        // THEN\n        #expect(response.image.sizeInPixels == CGSize(width: 640, height: 480))\n        #expect(task.state == .completed)\n    }\n\n    @Test func taskBasedImage() async throws {\n        // GIVEN\n        let task = pipeline.imageTask(with: Test.request)\n\n        // WHEN\n        let image = try await task.image\n\n        // THEN\n        #expect(image.sizeInPixels == CGSize(width: 640, height: 480))\n    }\n\n    // MARK: - Cancellation\n\n    @Test func cancellation() async throws {\n        dataLoader.queue.isSuspended = true\n\n        let pipeline = self.pipeline\n        let dataLoader = self.dataLoader\n        let task = Task {\n            try await pipeline.image(for: Test.url)\n        }\n\n        let observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in\n            task.cancel()\n        }\n\n        var caughtError: ImagePipeline.Error?\n        do {\n            _ = try await task.value\n        } catch let error as ImagePipeline.Error {\n            caughtError = error\n        }\n        #expect(caughtError == .cancelled)\n        NotificationCenter.default.removeObserver(observer)\n    }\n\n    @Test func cancelFromTaskCreated() async throws {\n        dataLoader.queue.isSuspended = true\n        pipelineDelegate.onTaskCreated = { $0.cancel() }\n\n        let pipeline = self.pipeline\n        let task = Task {\n            try await pipeline.image(for: Test.url)\n        }\n\n        var caughtError: ImagePipeline.Error?\n        do {\n            _ = try await task.value\n        } catch let error as ImagePipeline.Error {\n            caughtError = error\n        }\n        #expect(caughtError == .cancelled)\n    }\n\n    @Test func cancelImmediately() async throws {\n        dataLoader.queue.isSuspended = true\n\n        let pipeline = self.pipeline\n        let task = Task {\n            try await pipeline.image(for: Test.url)\n        }\n        task.cancel()\n\n        var caughtError: ImagePipeline.Error?\n        do {\n            _ = try await task.value\n        } catch let error as ImagePipeline.Error {\n            caughtError = error\n        }\n        #expect(caughtError == .cancelled)\n    }\n\n    @Test func cancelFromProgress() async throws {\n        dataLoader.queue.isSuspended = true\n\n        nonisolated(unsafe) var recordedProgress: [ImageTask.Progress] = []\n        let pipeline = self.pipeline\n        let task = Task { @Sendable in\n            let task = pipeline.imageTask(with: Test.url)\n            for await value in task.progress {\n                recordedProgress.append(value)\n            }\n        }\n\n        task.cancel()\n\n        _ = await task.value\n\n        // THEN nothing is recorded because the task is cancelled and\n        // stop observing the events\n        #expect(recordedProgress == [])\n    }\n\n    @Test func observeProgressAndCancelFromOtherTask() async throws {\n        dataLoader.queue.isSuspended = true\n\n        nonisolated(unsafe) var recordedProgress: [ImageTask.Progress] = []\n        let task = pipeline.imageTask(with: Test.url)\n\n        let task1 = Task { @Sendable in\n            for await event in task.progress {\n                recordedProgress.append(event)\n            }\n        }\n\n        let task2 = Task {\n            try await task.response\n        }\n\n        task2.cancel()\n\n        async let result1: () = task1.value\n        async let result2 = task2.value\n\n        // THEN you are able to observe `event` update because\n        // this task does no get cancelled\n        var caughtError: ImagePipeline.Error?\n        do {\n            _ = try await (result1, result2)\n        } catch let error as ImagePipeline.Error {\n            caughtError = error\n        }\n        #expect(caughtError == .cancelled)\n        #expect(recordedProgress == [])\n    }\n\n    @Test func cancelAsyncImageTask() async throws {\n        dataLoader.queue.isSuspended = true\n\n        let task = pipeline.imageTask(with: Test.url)\n        let observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in\n            task.cancel()\n        }\n        dataLoader.queue.isSuspended = false\n\n        var caughtError: ImagePipeline.Error?\n        do {\n            _ = try await task.image\n        } catch {\n            caughtError = error\n        }\n        #expect(caughtError == .cancelled)\n        #expect(task.state == .cancelled)\n        NotificationCenter.default.removeObserver(observer)\n    }\n\n    // MARK: - Load Data\n\n    @Test func loadData() async throws {\n        // GIVEN\n        dataLoader.results[Test.url] = .success((Test.data, Test.urlResponse))\n\n        // WHEN\n        let (data, response) = try await pipeline.data(for: Test.request)\n\n        // THEN\n        #expect(data.count == 22788)\n        #expect(response?.url != nil)\n    }\n\n    @Test func loadDataCancelImmediately() async throws {\n        dataLoader.queue.isSuspended = true\n\n        let pipeline = self.pipeline\n        let task = Task {\n            try await pipeline.data(for: Test.request)\n        }\n        task.cancel()\n\n        var caughtError: ImagePipeline.Error?\n        do {\n            _ = try await task.value\n        } catch let error as ImagePipeline.Error {\n            caughtError = error\n        }\n        #expect(caughtError == .cancelled)\n    }\n\n    @Test func imageTaskReturnedImmediately() async throws {\n        // GIVEN\n        nonisolated(unsafe) var imageTask: ImageTask?\n        pipelineDelegate.onTaskCreated = { imageTask = $0 }\n\n        // WHEN\n        _ = try await pipeline.image(for: Test.request)\n\n        // THEN\n        #expect(imageTask != nil)\n    }\n\n    @Test func progressUpdated() async throws {\n        // GIVEN\n        dataLoader.results[Test.url] = .success(\n            (Data(count: 20), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        // WHEN\n        var recordedProgress: [ImageTask.Progress] = []\n        do {\n            let task = pipeline.imageTask(with: Test.url)\n            for await progress in task.progress {\n                recordedProgress.append(progress)\n            }\n            _ = try await task.image\n        } catch {\n            // Do nothing\n        }\n\n        // THEN\n        #expect(recordedProgress == [\n            ImageTask.Progress(completed: 10, total: 20),\n            ImageTask.Progress(completed: 20, total: 20)\n        ])\n    }\n\n    @Test func thatProgressivePreviewsAreDelivered() async throws {\n        // GIVEN\n        let dataLoader = MockProgressiveDataLoader()\n        let pipeline = pipeline.reconfigured {\n            $0.dataLoader = dataLoader\n            $0.isProgressiveDecodingEnabled = true\n            $0.progressiveDecodingInterval = 0\n        }\n\n        // WHEN\n        var recordedPreviews: [ImageResponse] = []\n        let task = pipeline.imageTask(with: Test.url)\n        for try await preview in task.previews {\n            recordedPreviews.append(preview)\n            dataLoader.resume()\n        }\n        let response = try await task.response\n\n        // THEN\n        #expect(!response.container.isPreview)\n        #expect(recordedPreviews.count == 2)\n        #expect(recordedPreviews.allSatisfy { $0.container.isPreview })\n    }\n\n    // MARK: - Update Priority\n\n    @Test @ImagePipelineActor func updatePriority() async throws {\n        // GIVEN\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n\n        let request = Test.request\n        #expect(request.priority == .normal)\n\n        let expectation = TestExpectation(queue: queue, count: 1)\n        let imageTask = pipeline.imageTask(with: request)\n        Task.detached { try await imageTask.response }\n        await expectation.wait()\n\n        // WHEN/THEN\n        let operation = try #require(expectation.operations.first)\n        await queue.waitForPriorityChange(of: operation, to: .high) {\n            imageTask.priority = .high\n        }\n    }\n\n    // MARK: - ImageRequest with Async/Await (image container)\n\n    @Test func imageRequestWithAsyncImageSuccess() async throws {\n        // GIVEN\n        let image = PlatformImage(data: Test.data)!\n        let container = ImageContainer(image: image)\n\n        // WHEN\n        let request = ImageRequest(id: \"test\", image: { container })\n        let result = try await pipeline.image(for: request)\n\n        // THEN\n        #expect(result.sizeInPixels == CGSize(width: 640, height: 480))\n    }\n\n    @Test func imageRequestWithAsyncImageFailure() async throws {\n        // WHEN\n        let request = ImageRequest(id: \"test\", image: {\n            throw Foundation.URLError(.cancelled)\n        })\n\n        do {\n            _ = try await pipeline.image(for: request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            if case let .dataLoadingFailed(error) = error {\n                #expect((error as? Foundation.URLError)?.code == .cancelled)\n            } else {\n                Issue.record(\"Unexpected error type\")\n            }\n        }\n    }\n\n    @Test func imageRequestWithAsyncImageProcessorsApplied() async throws {\n        // GIVEN\n        let image = try #require(PlatformImage(data: Test.data))\n        let container = ImageContainer(image: image)\n\n        // WHEN\n        let request = ImageRequest(\n            id: \"test\",\n            image: { container },\n            processors: [.resize(size: CGSize(width: 160, height: 120), unit: .pixels)]\n        )\n        let result = try await pipeline.image(for: request)\n\n        // THEN - image is resized (original is 640x480)\n        #expect(result.sizeInPixels == CGSize(width: 160, height: 120))\n    }\n\n    // MARK: - ImageRequest with Async/Await\n\n    @Test func imageRequestWithAsyncAwaitSuccess() async throws {\n        // GIVEN\n        let localURL = Test.url(forResource: \"fixture\", extension: \"jpeg\")\n\n        // WHEN\n        let request = ImageRequest(id: \"test\", data: {\n            let (data, _) = try await URLSession.shared.data(for: URLRequest(url: localURL))\n            return data\n        })\n\n        let image = try await pipeline.image(for: request)\n\n        // THEN\n        #expect(image.sizeInPixels == CGSize(width: 640, height: 480))\n    }\n\n    @Test func imageRequestWithAsyncAwaitFailure() async throws {\n        // WHEN\n        let request = ImageRequest(id: \"test\", data: {\n            throw URLError(networkUnavailableReason: .cellular)\n        })\n\n        do {\n            _ = try await pipeline.image(for: request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            if case let .dataLoadingFailed(error) = error {\n                #expect((error as? URLError)?.networkUnavailableReason == .cellular)\n            } else {\n                Issue.record(\"Unexpected error type\")\n            }\n        }\n    }\n\n    // MARK: Common Use Cases\n\n    @Test func lowDataMode() async throws {\n        // GIVEN\n        let highQualityImageURL = URL(string: \"https://example.com/high-quality-image.jpeg\")!\n        let lowQualityImageURL = URL(string: \"https://example.com/low-quality-image.jpeg\")!\n\n        dataLoader.results[highQualityImageURL] = .failure(URLError(networkUnavailableReason: .constrained) as NSError)\n        dataLoader.results[lowQualityImageURL] = .success((Test.data, Test.urlResponse))\n\n        let pipeline = self.pipeline\n\n        // Create the default request to fetch the high quality image.\n        var urlRequest = URLRequest(url: highQualityImageURL)\n        urlRequest.allowsConstrainedNetworkAccess = false\n        let request = ImageRequest(urlRequest: urlRequest)\n\n        // WHEN\n        @Sendable func loadImage() async throws(ImagePipeline.Error) -> PlatformImage {\n            do {\n                return try await pipeline.image(for: request)\n            } catch {\n                guard (error.dataLoadingError as? URLError)?.networkUnavailableReason == .constrained else {\n                    throw error\n                }\n                return try await pipeline.image(for: lowQualityImageURL)\n            }\n        }\n\n        _ = try await loadImage()\n    }\n\n    // MARK: - ImageTask Integration\n\n    @Test func imageTaskEvents() async throws {\n        // GIVEN\n        let dataLoader = MockProgressiveDataLoader()\n        let pipeline = pipeline.reconfigured {\n            $0.dataLoader = dataLoader\n            $0.isProgressiveDecodingEnabled = true\n            $0.progressiveDecodingInterval = 0\n        }\n\n        // WHEN\n        var recordedPreviews: [ImageResponse] = []\n        var recordedResult: Result<ImageResponse, ImagePipeline.Error>?\n        var recordedEvents: [ImageTask.Event] = []\n\n        let task = pipeline.imageTask(with: Test.request)\n        for await event in task.events {\n            switch event {\n            case .preview(let response):\n                recordedPreviews.append(response)\n                dataLoader.resume()\n            case .finished(let result):\n                recordedResult = result\n            default:\n                break\n            }\n            recordedEvents.append(event)\n        }\n\n        // THEN\n        try #require(recordedPreviews.count == 2, \"Unexpected number of previews\")\n\n        let result = try #require(recordedResult)\n        #expect(recordedEvents.filter {\n            if case .progress = $0 {\n                return false // There is guarantee if all will arrive\n            }\n            return true\n        } == [\n            .preview(recordedPreviews[0]),\n            .preview(recordedPreviews[1]),\n            .finished(result)\n        ])\n    }\n}\n\n// MARK: - ImageTask State\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageTaskStateTests {\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    @Test func stateIsRunningWhileInFlight() async throws {\n        dataLoader.queue.isSuspended = true\n        let task = pipeline.imageTask(with: Test.request)\n        Task.detached { try? await task.response }\n\n        await notification(MockDataLoader.DidStartTask, object: dataLoader) {}\n\n        #expect(task.state == .running)\n        dataLoader.queue.isSuspended = false\n        _ = try await task.response\n    }\n\n    @Test func stateIsCompletedAfterSuccess() async throws {\n        let task = pipeline.imageTask(with: Test.request)\n        _ = try await task.response\n        #expect(task.state == .completed)\n    }\n\n    @Test func stateIsCancelledAfterCancel() async throws {\n        dataLoader.queue.isSuspended = true\n        let task = pipeline.imageTask(with: Test.request)\n        Task.detached { try? await task.response }\n        await notification(MockDataLoader.DidStartTask, object: dataLoader) {}\n        task.cancel()\n        await notification(MockDataLoader.DidCancelTask, object: dataLoader) {}\n        #expect(task.state == .cancelled)\n    }\n\n    @Test func stateIsFailedWhenErrorOccurs() async throws {\n        dataLoader.results[Test.url] = .failure(Foundation.URLError(.notConnectedToInternet) as NSError)\n        let task = pipeline.imageTask(with: Test.request)\n        _ = try? await task.response\n        #expect(task.state == .completed)\n    }\n}\n\n// MARK: - ImageTask.Progress\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageTaskProgressTests {\n\n    @Test func fractionIsZeroWhenTotalIsZero() {\n        let progress = ImageTask.Progress(completed: 0, total: 0)\n        #expect(progress.fraction == 0)\n    }\n\n    @Test func fractionIsCorrect() {\n        let progress = ImageTask.Progress(completed: 50, total: 100)\n        #expect(abs(progress.fraction - 0.5) < 0.001)\n    }\n\n    @Test func fractionIsClampedToOne() {\n        // completed > total can happen due to rounding; fraction must not exceed 1\n        let progress = ImageTask.Progress(completed: 150, total: 100)\n        #expect(progress.fraction == 1)\n    }\n\n    @Test func fractionIsOneWhenComplete() {\n        let progress = ImageTask.Progress(completed: 1000, total: 1000)\n        #expect(progress.fraction == 1)\n    }\n\n    @Test func progressEquality() {\n        let a = ImageTask.Progress(completed: 50, total: 100)\n        let b = ImageTask.Progress(completed: 50, total: 100)\n        #expect(a == b)\n        #expect(a.hashValue == b.hashValue)\n    }\n\n    @Test func progressInequality() {\n        let a = ImageTask.Progress(completed: 50, total: 100)\n        let b = ImageTask.Progress(completed: 60, total: 100)\n        let c = ImageTask.Progress(completed: 50, total: 200)\n        #expect(a != b)\n        #expect(a != c)\n    }\n}\n\n/// We have to mock it because there is no way to construct native `URLError`\n/// with a `networkUnavailableReason`.\nprivate struct URLError: Swift.Error {\n    var networkUnavailableReason: NetworkUnavailableReason?\n\n    enum NetworkUnavailableReason {\n        case cellular\n        case expensive\n        case constrained\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineCacheTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineCacheTests {\n    let memoryCache: MockImageCache\n    let diskCache: MockDataCache\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n    var cache: ImagePipeline.Cache { pipeline.cache }\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let diskCache = MockDataCache()\n        let memoryCache = MockImageCache()\n        self.dataLoader = dataLoader\n        self.diskCache = diskCache\n        self.memoryCache = memoryCache\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = memoryCache\n            $0.dataCache = diskCache\n        }\n    }\n\n    // MARK: Subscripts\n\n    @Test func `subscript`() {\n        // GIVEN\n        cache[Test.request] = Test.container\n\n        // THEN\n        #expect(cache[Test.request] != nil)\n    }\n\n    @Test func disableMemoryCacheRead() {\n        // GIVEN\n        cache[Test.request] = Test.container\n        let request = ImageRequest(url: Test.url, options: [.disableMemoryCacheReads])\n\n        // THEN\n        #expect(cache[request] == nil)\n    }\n\n    @Test func disableMemoryCacheWrite() {\n        // GIVEN\n        let request = ImageRequest(url: Test.url, options: [.disableMemoryCacheWrites])\n        cache[request] = Test.container\n\n        // THEN\n        #expect(cache[Test.request] == nil)\n    }\n\n    @Test func subscriptRemove() {\n        // GIVEN\n        cache[Test.request] = Test.container\n\n        // WHEN\n        cache[Test.request] = nil\n\n        // THEN\n        #expect(cache[Test.request] == nil)\n    }\n\n    @Test func subscriptStoringPreviewWhenDisabled() {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.isStoringPreviewsInMemoryCache = false\n        }\n\n        // WHEN\n        pipeline.cache[Test.request] = ImageContainer(image: Test.image, isPreview: true)\n\n        // THEN\n        #expect(pipeline.cache[Test.request] == nil)\n    }\n\n    @Test func subscriptStoringPreviewWhenEnabled() throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.isStoringPreviewsInMemoryCache = true\n        }\n\n        // WHEN\n        pipeline.cache[Test.request] = ImageContainer(image: Test.image, isPreview: true)\n\n        // THEN\n        let response = try #require(pipeline.cache[Test.request])\n        #expect(response.isPreview)\n    }\n\n    @Test func subscriptWhenNoImageCache() {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.imageCache = nil\n        }\n        pipeline.cache[Test.request] = Test.container\n\n        // THEN\n        #expect(pipeline.cache[Test.request] == nil)\n    }\n\n    @Test func subscriptWithRealImageCache() {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.imageCache = ImageCache()\n        }\n        pipeline.cache[Test.request] = Test.container\n\n        // THEN\n        #expect(pipeline.cache[Test.request] != nil)\n    }\n\n    // MARK: Cached Image\n\n    @Test func getCachedImageDefaultFromMemoryCache() {\n        // GIVEN\n        let request = Test.request\n        memoryCache[cache.makeImageCacheKey(for: request)] = Test.container\n\n        // WHEN\n        let image = cache.cachedImage(for: request)\n\n        // THEN\n        #expect(image != nil)\n    }\n\n    @Test func getCachedImageDefaultFromDiskCache() {\n        // GIVEN\n        let request = Test.request\n        diskCache.storeData(Test.data, for: cache.makeDataCacheKey(for: request))\n\n        // WHEN\n        let image = cache.cachedImage(for: request)\n\n        // THEN\n        #expect(image != nil)\n    }\n\n    @Test func getCachedImageDefaultFromDiskCacheWhenOptionEnabled() {\n        // GIVEN\n        let request = Test.request\n        diskCache.storeData(Test.data, for: cache.makeDataCacheKey(for: request))\n\n        // WHEN\n        let image = cache.cachedImage(for: request, caches: [.disk])\n\n        // THEN returns nil because queries only memory cache by default\n        #expect(image != nil)\n    }\n\n    @Test func getCachedImageDefaultNotStored() {\n        // GIVEN\n        let request = Test.request\n\n        // WHEN\n        let image = cache.cachedImage(for: request)\n\n        // THEN\n        #expect(image == nil)\n    }\n\n    @Test func getCachedImageDefaultFromMemoryCacheWhenCachePolicyPreventsLookup() {\n        // GIVEN\n        var request = Test.request\n        memoryCache[cache.makeImageCacheKey(for: request)] = Test.container\n\n        // WHEN\n        request.options = [.reloadIgnoringCachedData]\n        let image = cache.cachedImage(for: request)\n\n        // THEN\n        #expect(image == nil)\n    }\n\n    @Test func getCachedImageDefaultFromDiskCacheWhenCachePolicyPreventsLookup() {\n        // GIVEN\n        var request = Test.request\n        diskCache.storeData(Test.data, for: cache.makeDataCacheKey(for: request))\n\n        // WHEN\n        request.options = [.reloadIgnoringCachedData]\n        let image = cache.cachedImage(for: request, caches: [.disk])\n\n        // THEN\n        #expect(image == nil)\n    }\n\n    @Test func getCachedImageOnlyFromMemoryStoredInMemory() {\n        // GIVEN\n        let request = Test.request\n        memoryCache[cache.makeImageCacheKey(for: request)] = Test.container\n\n        // WHEN\n        let image = cache.cachedImage(for: request, caches: [.memory])\n\n        // THEN\n        #expect(image != nil)\n    }\n\n    @Test func getCachedImageOnlyFromMemoryStoredOnDisk() {\n        // GIVEN\n        let request = Test.request\n        diskCache.storeData(Test.data, for: cache.makeDataCacheKey(for: request))\n\n        // WHEN\n        let image = cache.cachedImage(for: request, caches: [.memory])\n\n        // THEN\n        #expect(image == nil)\n    }\n\n    @Test func disableDiskCacheReads() {\n        // GIVEN\n        cache.storeCachedData(Test.data, for: Test.request)\n        let request = ImageRequest(url: Test.url, options: [.disableDiskCacheReads])\n\n        // THEN\n        #expect(cache.cachedData(for: request) == nil)\n    }\n\n    @Test func disableDiskCacheWrites() {\n        // GIVEN\n        let request = ImageRequest(url: Test.url, options: [.disableDiskCacheWrites])\n        cache.storeCachedData(Test.data, for: request)\n\n        // THEN\n        #expect(cache.cachedData(for: Test.request) == nil)\n    }\n\n    // MARK: Store Cached Image\n\n    @Test func storeCachedImageMemoryCache() {\n        // WHEN\n        let request = Test.request\n        cache.storeCachedImage(Test.container, for: request)\n\n        // THEN\n        #expect(cache.cachedImage(for: request) != nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] != nil)\n\n        #expect(cache.cachedImage(for: request, caches: [.disk]) != nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) != nil)\n    }\n\n    @Test func storeCachedImageInDiskCache() {\n        // WHEN\n        let request = Test.request\n        cache.storeCachedImage(Test.container, for: request, caches: [.disk])\n\n        // THEN\n        #expect(cache.cachedImage(for: request) != nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] == nil)\n\n        #expect(cache.cachedImage(for: request, caches: [.disk]) != nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) != nil)\n    }\n\n    @Test func storeCachedImageInBothLayers() {\n        // WHEN\n        let request = Test.request\n        cache.storeCachedImage(Test.container, for: request, caches: [.memory, .disk])\n\n        // THEN\n        #expect(cache.cachedImage(for: request) != nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] != nil)\n\n        #expect(cache.cachedImage(for: request, caches: [.disk]) != nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) != nil)\n    }\n\n    // MARK: Cached Data\n\n    @Test func storeCachedData() {\n        // WHEN\n        let request = Test.request\n        cache.storeCachedData(Test.data, for: request)\n\n        // THEN\n        #expect(cache.cachedImage(for: request) != nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] == nil)\n\n        #expect(cache.cachedImage(for: request, caches: [.disk]) != nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) != nil)\n    }\n\n    @Test func storeCacheImageWhenMemoryCacheWriteDisabled() {\n        // WHEN\n        var request = Test.request\n        request.options.insert(.disableMemoryCacheWrites)\n        cache.storeCachedImage(Test.container, for: request, caches: [.memory])\n\n        // THEN\n        #expect(cache.cachedImage(for: request) == nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] == nil)\n\n        #expect(cache.cachedImage(for: request, caches: [.disk]) == nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) == nil)\n    }\n\n    @Test func storeCacheDataWhenNoDataCache() {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCache = nil\n        }\n\n        // WHEN\n        pipeline.cache.storeCachedData(Test.data, for: Test.request)\n\n        // THEN just make sure it doesn't do anything weird\n        #expect(pipeline.cache.cachedData(for: Test.request) == nil)\n    }\n\n    @Test func getCachedDataWhenNoDataCache() {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCache = nil\n        }\n\n        // THEN just make sure it doesn't do anything weird\n        #expect(pipeline.cache.cachedData(for: Test.request) == nil)\n        pipeline.cache.removeCachedData(for: Test.request)\n    }\n\n    // MARK: Contains\n\n    @Test func containsWhenStoredInMemoryCache() {\n        // GIVEN\n        cache.storeCachedImage(Test.container, for: Test.request, caches: [.memory])\n\n        // WHEN/THEN\n        #expect(cache.containsCachedImage(for: Test.request))\n        #expect(cache.containsCachedImage(for: Test.request, caches: [.all]))\n        #expect(cache.containsCachedImage(for: Test.request, caches: [.memory]))\n        #expect(!cache.containsCachedImage(for: Test.request, caches: [.disk]))\n    }\n\n    @Test func containsWhenStoredInDiskCache() {\n        // GIVEN\n        cache.storeCachedImage(Test.container, for: Test.request, caches: [.disk])\n\n        // WHEN/THEN\n        #expect(cache.containsCachedImage(for: Test.request))\n        #expect(cache.containsCachedImage(for: Test.request, caches: [.all]))\n        #expect(!cache.containsCachedImage(for: Test.request, caches: [.memory]))\n        #expect(cache.containsCachedImage(for: Test.request, caches: [.disk]))\n    }\n\n    @Test func containsStoredInBoth() {\n        // GIVEN\n        cache.storeCachedImage(Test.container, for: Test.request, caches: [.all])\n\n        // WHEN/THEN\n        #expect(cache.containsCachedImage(for: Test.request))\n        #expect(cache.containsCachedImage(for: Test.request, caches: [.all]))\n        #expect(cache.containsCachedImage(for: Test.request, caches: [.memory]))\n        #expect(cache.containsCachedImage(for: Test.request, caches: [.disk]))\n    }\n\n    @Test func containsData() {\n        // GIVEN\n        cache.storeCachedImage(Test.container, for: Test.request, caches: [.disk])\n\n        // WHEN/THEN\n        #expect(cache.containsData(for: Test.request))\n    }\n\n    @Test func containsDataWithNoDataCache() {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCache = nil\n        }\n\n        // WHEN/THEN\n        #expect(!pipeline.cache.containsData(for: Test.request))\n    }\n\n    // MARK: Remove\n\n    @Test func removeFromMemoryCache() {\n        // GIVEN\n        let request = Test.request\n        cache.storeCachedImage(Test.container, for: request)\n\n        // WHEN\n        cache.removeCachedImage(for: request)\n\n        // THEN\n        #expect(cache.cachedImage(for: request) == nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] == nil)\n    }\n\n    @Test func removeFromDiskCache() {\n        // GIVEN\n        let request = Test.request\n        cache.storeCachedImage(Test.container, for: request, caches: [.disk])\n\n        // WHEN\n        cache.removeCachedImage(for: request, caches: [.disk])\n\n        // THEN\n        #expect(cache.cachedImage(for: request, caches: [.disk]) == nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) == nil)\n    }\n\n    @Test func removeFromAllCaches() {\n        // GIVEN\n        let request = Test.request\n        cache.storeCachedImage(Test.container, for: request, caches: [.memory, .disk])\n\n        // WHEN\n        cache.removeCachedImage(for: request, caches: [.memory, .disk])\n\n        // THEN\n        #expect(cache.cachedImage(for: request) == nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] == nil)\n\n        #expect(cache.cachedImage(for: request, caches: [.disk]) == nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) == nil)\n    }\n\n    // MARK: Remove All\n\n    @Test func removeAll() {\n        // GIVEN\n        let request = Test.request\n        cache.storeCachedImage(Test.container, for: request, caches: [.memory, .disk])\n\n        // WHEN\n        cache.removeAll()\n\n        // THEN\n        #expect(cache.cachedImage(for: request) == nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] == nil)\n\n        #expect(cache.cachedImage(for: request, caches: [.disk]) == nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) == nil)\n    }\n\n    @Test func removeAllWithAllStatic() {\n        // GIVEN\n        let request = Test.request\n        cache.storeCachedImage(Test.container, for: request, caches: [.all])\n\n        // WHEN\n        cache.removeAll()\n\n        // THEN\n        #expect(cache.cachedImage(for: request) == nil)\n        #expect(memoryCache[cache.makeImageCacheKey(for: request)] == nil)\n\n        #expect(cache.cachedImage(for: request, caches: [.disk]) == nil)\n        #expect(diskCache.cachedData(for: cache.makeDataCacheKey(for: request)) == nil)\n    }\n\n    // MARK: - Image Orientation\n\n#if canImport(UIKit)\n    @Test func thatImageOrientationIsPreserved() throws {\n        // GIVEN opaque jpeg with orientation\n        let image = Test.image(named: \"right-orientation\", extension: \"jpeg\")\n        let cgImage = try #require(image.cgImage)\n        #expect(cgImage.isOpaque)\n        #expect(image.imageOrientation == .right)\n\n        // WHEN\n        let pipeline = ImagePipeline(configuration: .withDataCache)\n        pipeline.cache.storeCachedImage(ImageContainer(image: image), for: Test.request, caches: [.disk])\n        let cached = try #require(pipeline.cache.cachedImage(for: Test.request, caches: [.disk])?.image)\n\n        // THEN orientation is preserved\n        let cachedCGImage = try #require(cached.cgImage)\n        #expect(cachedCGImage.isOpaque)\n        #expect(cached.imageOrientation == .right)\n    }\n\n    @Test func thatImageOrientationIsPreservedForProcessedImages() throws {\n        // GIVEN opaque jpeg with orientation\n        let image = Test.image(named: \"right-orientation\", extension: \"jpeg\")\n        let cgImage = try #require(image.cgImage)\n        #expect(cgImage.isOpaque)\n        #expect(image.imageOrientation == .right)\n\n        let resized = try #require(ImageProcessors.Resize(width: 100).process(image))\n\n        // WHEN\n        let pipeline = ImagePipeline(configuration: .withDataCache)\n        pipeline.cache.storeCachedImage(ImageContainer(image: resized), for: Test.request, caches: [.disk])\n        let cached = try #require(pipeline.cache.cachedImage(for: Test.request, caches: [.disk])?.image)\n\n        // THEN orientation is preserved\n        let cachedCGImage = try #require(cached.cgImage)\n        #expect(cachedCGImage.isOpaque)\n        #expect(cached.imageOrientation == .right)\n    }\n#endif\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineCoalescingTests {\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    // MARK: - Deduplication\n\n    @Test func deduplicationGivenSameURLDifferentSameProcessors() async throws {\n        // Given requests with the same URLs and same processors\n        let processors = MockProcessorFactory()\n        let request1 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])\n        let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])\n\n        // When loading images for those requests\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n\n        let response1 = try await task1.response\n        #expect(response1.image.nk_test_processorIDs == [\"1\"])\n        _ = try await task2.response\n\n        // Then the original image is loaded once\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    @Test func deduplicationGivenSameURLDifferentProcessors() async throws {\n        // Given requests with the same URLs but different processors\n        let processors = MockProcessorFactory()\n        let request1 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])\n        let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: \"2\")])\n\n        // When loading images for those requests\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        // Then the original image is loaded once, but both processors are applied\n        #expect(processors.numberOfProcessorsApplied == 2)\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    @Test func noDeduplicationGivenNonEquivalentRequests() async throws {\n        let request1 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0))\n        let request2 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .returnCacheDataDontLoad, timeoutInterval: 0))\n\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        #expect(dataLoader.createdTaskCount == 2)\n    }\n\n    // MARK: - Scale\n\n#if !os(macOS)\n    @Test func overridingImageScale() async throws {\n        // GIVEN requests with the same URLs but different scale\n        let request1 = ImageRequest(url: Test.url).with { $0.scale = 2 }\n        let request2 = ImageRequest(url: Test.url).with { $0.scale = 3 }\n\n        // WHEN loading images for those requests\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n        let image1 = try await task1.response.image\n        let image2 = try await task2.response.image\n\n        // THEN\n        #expect(image1.scale == 2)\n        #expect(image2.scale == 3)\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n#endif\n\n    // MARK: - Thumbnail\n\n    @Test func deduplicationGivenSameURLButDifferentThumbnailOptions() async throws {\n        // GIVEN requests with the same URLs but one accesses thumbnail\n        let request1 = ImageRequest(url: Test.url).with { $0.thumbnail = .init(maxPixelSize: 400) }\n        let request2 = ImageRequest(url: Test.url)\n\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n        let image1 = try await task1.response.image\n        let image2 = try await task2.response.image\n\n        // THEN\n        #expect(image1.sizeInPixels == CGSize(width: 400, height: 300))\n        #expect(image2.sizeInPixels == CGSize(width: 640.0, height: 480.0))\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    @Test func deduplicationGivenSameURLButDifferentThumbnailOptionsReversed() async throws {\n        // GIVEN requests with the same URLs but one accesses thumbnail (reversed order)\n        let request1 = ImageRequest(url: Test.url)\n        let request2 = ImageRequest(url: Test.url).with {\n            $0.thumbnail = .init(maxPixelSize: 400)\n        }\n\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n        let image1 = try await task1.response.image\n        let image2 = try await task2.response.image\n\n        // THEN\n        #expect(image1.sizeInPixels == CGSize(width: 640.0, height: 480.0))\n        #expect(image2.sizeInPixels == CGSize(width: 400, height: 300))\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    // MARK: - Processing\n\n    @Test @ImagePipelineActor func processorsAreDeduplicated() async throws {\n        // Given\n        let processors = MockProcessorFactory()\n        let queueObserver = TaskQueueObserver(queue: pipeline.configuration.imageProcessingQueue)\n\n        // When\n        let (task1, task2, task3) = await withSuspendedDataLoading(for: pipeline, expectedCount: 3) {\n            (pipeline.imageTask(with: ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])),\n             pipeline.imageTask(with: ImageRequest(url: Test.url, processors: [processors.make(id: \"2\")])),\n             pipeline.imageTask(with: ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n        _ = try await task3.response\n\n        // Then\n        #expect(queueObserver.operations.count == 2)\n        #expect(processors.numberOfProcessorsApplied == 2)\n    }\n\n    @Test func correctImageIsStoredInMemoryCache() async throws {\n        let imageCache = MockImageCache()\n        let pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = imageCache\n        }\n\n        // Given requests with the same URLs but different processors\n        let processors = MockProcessorFactory()\n        let request1 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])\n        let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: \"2\")])\n\n        // When loading images for those requests\n        let response1 = try await pipeline.imageTask(with: request1).response\n        #expect(response1.image.nk_test_processorIDs == [\"1\"])\n\n        let response2 = try await pipeline.imageTask(with: request2).response\n        #expect(response2.image.nk_test_processorIDs == [\"2\"])\n\n        // Then\n        #expect(imageCache[request1] != nil)\n        #expect(imageCache[request1]?.image.nk_test_processorIDs == [\"1\"])\n        #expect(imageCache[request2] != nil)\n        #expect(imageCache[request2]?.image.nk_test_processorIDs == [\"2\"])\n    }\n\n    // MARK: - Cancellation\n\n    @Test func cancellation() async {\n        dataLoader.queue.isSuspended = true\n\n        // Given two equivalent requests\n        let startExpectation = TestExpectation(notification: MockDataLoader.DidStartTask, object: dataLoader)\n        let task1 = pipeline.imageTask(with: Test.request)\n        let task2 = pipeline.imageTask(with: Test.request)\n        Task.detached { try? await task1.response }\n        Task.detached { try? await task2.response }\n        await startExpectation.wait()\n\n        // When both tasks are cancelled the image loading session is cancelled\n        await notification(MockDataLoader.DidCancelTask, object: dataLoader) {\n            task1.cancel()\n            task2.cancel()\n        }\n    }\n\n    @Test func cancellationOnlyCancelOneTask() async throws {\n        dataLoader.queue.isSuspended = true\n\n        let task1 = pipeline.imageTask(with: Test.request)\n        let task2 = pipeline.imageTask(with: Test.request)\n\n        // Start both tasks\n        Task.detached { try? await task1.response }\n\n        // When cancelling only one of the tasks\n        task1.cancel()\n\n        // Then the image is still loaded via the second task\n        dataLoader.queue.isSuspended = false\n        _ = try await task2.response\n    }\n\n    // MARK: - Loading Data\n\n    @Test func loadsDataOnceWhenLoadingDataAndLoadingImage() async throws {\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: Test.request), pipeline.imageTask(with: Test.request))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    // MARK: - Misc\n\n    @Test func progressIsReported() async throws {\n        // Given\n        dataLoader.results[Test.url] = .success(\n            (Data(count: 20), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        // When/Then\n        let task = pipeline.imageTask(with: Test.url)\n        var progressValues: [ImageTask.Progress] = []\n        for await progress in task.progress {\n            progressValues.append(progress)\n        }\n        _ = try? await task.response\n\n        #expect(progressValues == [\n            ImageTask.Progress(completed: 10, total: 20),\n            ImageTask.Progress(completed: 20, total: 20)\n        ])\n    }\n\n    @Test func disablingDeduplication() async throws {\n        // Given\n        let pipeline = ImagePipeline {\n            $0.imageCache = nil\n            $0.dataLoader = dataLoader\n            $0.isTaskCoalescingEnabled = false\n        }\n\n        // When/Then\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: Test.request), pipeline.imageTask(with: Test.request))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        #expect(dataLoader.createdTaskCount == 2)\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineProcessingDeduplicationTests {\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    @Test func eachProcessingStepIsDeduplicated() async throws {\n        // Given requests with the same URLs but different processors\n        let processors = MockProcessorFactory()\n        let request1 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])\n        let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\"), processors.make(id: \"2\")])\n\n        // When\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        // Then the processor \"1\" is only applied once\n        #expect(processors.numberOfProcessorsApplied == 2)\n    }\n\n    @Test func eachFinalProcessedImageIsStoredInMemoryCache() async throws {\n        let cache = MockImageCache()\n        var conf = pipeline.configuration\n        conf.imageCache = cache\n        let pipeline = ImagePipeline(configuration: conf)\n\n        // Given requests with the same URLs but different processors\n        let processors = MockProcessorFactory()\n        let request1 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])\n        let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\"), processors.make(id: \"2\"), processors.make(id: \"3\")])\n\n        // When\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        // Then\n        #expect(cache[request1] != nil)\n        #expect(cache[request2] != nil)\n        #expect(cache[ImageRequest(url: Test.url, processors: [processors.make(id: \"1\"), processors.make(id: \"2\")])] == nil)\n    }\n\n    @Test func intermediateMemoryCachedResultsAreUsed() async throws {\n        let cache = MockImageCache()\n        var conf = pipeline.configuration\n        conf.imageCache = cache\n        let pipeline = ImagePipeline(configuration: conf)\n\n        let factory = MockProcessorFactory()\n\n        // Given\n        cache[ImageRequest(url: Test.url, processors: [factory.make(id: \"1\"), factory.make(id: \"2\")])] = Test.container\n\n        // When\n        let request = ImageRequest(url: Test.url, processors: [factory.make(id: \"1\"), factory.make(id: \"2\"), factory.make(id: \"3\")])\n        let response = try await pipeline.imageTask(with: request).response\n\n        // Then\n        #expect(response.image.nk_test_processorIDs == [\"3\"])\n        #expect(dataLoader.createdTaskCount == 0)\n        #expect(factory.numberOfProcessorsApplied == 1)\n    }\n\n    @Test func intermediateDataCacheResultsAreUsed() async throws {\n        // Given\n        let dataCache = MockDataCache()\n        dataCache.store[Test.url.absoluteString + \"12\"] = Test.data\n\n        let pipeline = pipeline.reconfigured {\n            $0.dataCache = dataCache\n        }\n\n        // When\n        let factory = MockProcessorFactory()\n        let request = ImageRequest(url: Test.url, processors: [factory.make(id: \"1\"), factory.make(id: \"2\"), factory.make(id: \"3\")])\n        let response = try await pipeline.imageTask(with: request).response\n\n        // Then\n        #expect(response.image.nk_test_processorIDs == [\"3\"])\n        #expect(dataLoader.createdTaskCount == 0)\n        #expect(factory.numberOfProcessorsApplied == 1)\n    }\n\n    @Test func processingDeduplicationCanBeDisabled() async throws {\n        // Given\n        let pipeline = pipeline.reconfigured {\n            $0.isTaskCoalescingEnabled = false\n        }\n\n        // Given requests with the same URLs but different processors\n        let processors = MockProcessorFactory()\n        let request1 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\")])\n        let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: \"1\"), processors.make(id: \"2\")])\n\n        // When\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: request1), pipeline.imageTask(with: request2))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        // Then the processor \"1\" is applied twice\n        #expect(processors.numberOfProcessorsApplied == 3)\n    }\n\n    // MARK: - Priority Escalation\n\n    @Test @ImagePipelineActor func lowPriorityRequestEscalatesWhenHigherPriorityJoins() async throws {\n        // GIVEN - a low-priority request already in flight\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n\n        var lowRequest = Test.request\n        lowRequest.priority = .low\n\n        let lowOperations = await queue.waitForOperations(count: 1) {\n            _ = pipeline.imageTask(with: lowRequest)\n        }\n        let operation = try #require(lowOperations.first)\n        #expect(operation.priority == .low)\n\n        // WHEN - a normal-priority request for the same URL joins the session\n        var highRequest = Test.request\n        highRequest.priority = .normal\n\n        // Priority should be escalated to the highest subscriber\n        await queue.waitForPriorityChange(of: operation, to: .normal) {\n            _ = pipeline.imageTask(with: highRequest)\n        }\n\n        // THEN\n        #expect(operation.priority == .normal)\n\n        // Cleanup\n        queue.isSuspended = false\n    }\n\n    @Test func dataOnlyLoadedOnceWithDifferentCachePolicyPassingURL() async throws {\n        // Given\n        let dataCache = MockDataCache()\n        let pipeline = pipeline.reconfigured {\n            $0.dataCache = dataCache\n        }\n\n        // When - One request reloading cache data, another one not\n        @Sendable func makeRequest(options: ImageRequest.Options) -> ImageRequest {\n            ImageRequest(urlRequest: URLRequest(url: Test.url), options: options)\n        }\n\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: makeRequest(options: [])),\n             pipeline.imageTask(with: makeRequest(options: [.reloadIgnoringCachedData])))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineConfigurationTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineConfigurationTests {\n\n    @Test func imageIsLoadedWithRateLimiterDisabled() async throws {\n        // Given\n        let dataLoader = MockDataLoader()\n        let pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n            $0.isRateLimiterEnabled = false\n        }\n\n        // When/Then\n        _ = try await pipeline.image(for: Test.request)\n    }\n\n    // MARK: DataCache\n\n    @Test func withDataCache() {\n        let pipeline = ImagePipeline(configuration: .withDataCache)\n        #expect(pipeline.configuration.dataCache != nil)\n    }\n\n    @Test func enablingSignposts() {\n        ImagePipeline.Configuration.isSignpostLoggingEnabled = false // Just padding\n        ImagePipeline.Configuration.isSignpostLoggingEnabled = true\n        ImagePipeline.Configuration.isSignpostLoggingEnabled = false\n    }\n\n    // MARK: - Default Values\n\n    @Test func isTaskCoalescingEnabledByDefault() {\n        let config = ImagePipeline.Configuration()\n        #expect(config.isTaskCoalescingEnabled == true)\n    }\n\n    @Test func isRateLimiterEnabledByDefault() {\n        let config = ImagePipeline.Configuration()\n        #expect(config.isRateLimiterEnabled == true)\n    }\n\n    @Test func isProgressiveDecodingDisabledByDefault() {\n        let config = ImagePipeline.Configuration()\n        #expect(config.isProgressiveDecodingEnabled == false)\n    }\n\n    @Test func dataCachePolicyDefaultsToStoreOriginalData() {\n        let config = ImagePipeline.Configuration()\n        #expect(config.dataCachePolicy == .storeOriginalData)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineDataCachingTests {\n    let dataLoader: MockDataLoader\n    let dataCache: MockDataCache\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataCache = MockDataCache()\n        let dataLoader = MockDataLoader()\n        self.dataCache = dataCache\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.dataCache = dataCache\n            $0.imageCache = nil\n        }\n    }\n\n    // MARK: - Basics\n\n    @Test func imageIsLoaded() async throws {\n        // Given\n        dataLoader.queue.isSuspended = true\n        dataCache.store[Test.url.absoluteString] = Test.data\n\n        // When/Then\n        _ = try await pipeline.image(for: Test.request)\n    }\n\n    @Test func dataIsStoredInCache() async throws {\n        // When\n        _ = try await pipeline.image(for: Test.request)\n\n        // Then\n        #expect(!dataCache.store.isEmpty)\n    }\n\n    @Test func thumbnailOptionsDataCacheStoresOriginalDataByDefault() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeOriginalData\n            $0.imageCache = MockImageCache()\n        }\n\n        // WHEN\n        var request = ImageRequest(url: Test.url)\n        request.thumbnail = .init(\n            size: CGSize(width: 400, height: 400),\n            unit: .pixels,\n            contentMode: .aspectFit\n        )\n\n        _ = try await pipeline.image(for: request)\n\n        // THEN\n        do { // Check memory cache\n            // Image does not exists for the original image\n            #expect(pipeline.cache.cachedImage(for: ImageRequest(url: Test.url), caches: [.memory]) == nil)\n\n            // Image exists for thumbnail\n            let thumbnail = try #require(pipeline.cache.cachedImage(for: request, caches: [.memory]))\n            #expect(thumbnail.image.sizeInPixels == CGSize(width: 400, height: 300))\n        }\n\n        do { // Check disk cache\n            // Data exists for the original image\n            let original = try #require(pipeline.cache.cachedImage(for: ImageRequest(url: Test.url), caches: [.disk]))\n            #expect(original.image.sizeInPixels == CGSize(width: 640, height: 480))\n\n            // Data does not exist for thumbnail\n            #expect(pipeline.cache.cachedData(for: request) == nil)\n        }\n    }\n\n    @Test func thumbnailOptionsDataCacheStoresOriginalDataWithStoreAllPolicy() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeAll\n            $0.imageCache = MockImageCache()\n        }\n\n        // WHEN\n        var request = ImageRequest(url: Test.url)\n        request.thumbnail = .init(\n            size: CGSize(width: 400, height: 400),\n            unit: .pixels,\n            contentMode: .aspectFit\n        )\n\n        _ = try await pipeline.image(for: request)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN\n        do { // Check memory cache\n            // Image does not exists for the original image\n            #expect(pipeline.cache.cachedImage(for: ImageRequest(url: Test.url), caches: [.memory]) == nil)\n\n            // Image exists for thumbnail\n            let thumbnail = try #require(pipeline.cache.cachedImage(for: request, caches: [.memory]))\n            #expect(thumbnail.image.sizeInPixels == CGSize(width: 400, height: 300))\n        }\n\n        do { // Check disk cache\n            // Data exists for the original image\n            let original = try #require(pipeline.cache.cachedImage(for: ImageRequest(url: Test.url), caches: [.disk]))\n            #expect(original.image.sizeInPixels == CGSize(width: 640, height: 480))\n\n            // Data exists for thumbnail\n            let thumbnail = try #require(pipeline.cache.cachedImage(for: request, caches: [.disk]))\n            #expect(thumbnail.image.sizeInPixels == CGSize(width: 400, height: 300))\n        }\n    }\n\n    // MARK: - Updating Priority\n\n    @Test func priorityUpdated() async throws {\n        // Given\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n\n        let request = Test.request\n        #expect(request.priority == .normal)\n\n        var task: ImageTask!\n        let operations = await queue.waitForOperations(count: 1) {\n            task = pipeline.imageTask(with: request)\n        }\n\n        // When/Then\n        let operation = try #require(operations.first)\n        await queue.waitForPriorityChange(of: operation, to: .high) {\n            task.priority = .high\n        }\n    }\n\n    // MARK: - Cancellation\n\n    @Test func operationCancelled() async throws {\n        // Given\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n        var task: ImageTask!\n        let operations = await queue.waitForOperations(count: 1) {\n            task = pipeline.imageTask(with: Test.request)\n        }\n\n        // When/Then\n        let operation = try #require(operations.first)\n        await queue.waitForCancellation(of: operation) {\n            task.cancel()\n        }\n    }\n\n    // MARK: ImageRequest.CachePolicy\n\n    @Test func reloadIgnoringCachedData() async throws {\n        // Given\n        dataCache.store[Test.url.absoluteString] = Test.data\n\n        var request = Test.request\n        request.options = [.reloadIgnoringCachedData]\n\n        // When\n        _ = try await pipeline.image(for: request)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    @Test func loadFromCacheOnlyDataCache() async throws {\n        // Given\n        dataCache.store[Test.url.absoluteString] = Test.data\n\n        var request = Test.request\n        request.options = [.returnCacheDataDontLoad]\n\n        // When\n        _ = try await pipeline.image(for: request)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func loadFromCacheOnlyMemoryCache() async throws {\n        // Given\n        let imageCache = MockImageCache()\n        imageCache[Test.request] = ImageContainer(image: Test.image)\n        let pipeline = pipeline.reconfigured {\n            $0.imageCache = imageCache\n        }\n\n        var request = Test.request\n        request.options = [.returnCacheDataDontLoad]\n\n        // When\n        _ = try await pipeline.image(for: request)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func loadImageFromCacheOnlyFailsIfNoCache() async {\n        // GIVEN no cached data and download disabled\n        var request = Test.request\n        request.options = [.returnCacheDataDontLoad]\n\n        // WHEN/THEN\n        await #expect {\n            _ = try await pipeline.image(for: request)\n        } throws: {\n            ($0 as? ImagePipeline.Error) == .dataMissingInCache\n        }\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func loadDataFromCacheOnlyFailsIfNoCache() async {\n        // GIVEN no cached data and download disabled\n        var request = Test.request\n        request.options = [.returnCacheDataDontLoad]\n\n        // WHEN/THEN\n        await #expect {\n            try await pipeline.data(for: request)\n        } throws: {\n            ($0 as? ImagePipeline.Error) == .dataMissingInCache\n        }\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineDataCachePolicyTests {\n    let dataLoader: MockDataLoader\n    let dataCache: MockDataCache\n    let pipeline: ImagePipeline\n    let encoder: MockImageEncoder\n    let processorFactory: MockProcessorFactory\n    let request: ImageRequest\n\n    init() {\n        let dataCache = MockDataCache()\n        let dataLoader = MockDataLoader()\n        let encoder = MockImageEncoder(result: Test.data(name: \"fixture-tiny\", extension: \"jpeg\"))\n        let processorFactory = MockProcessorFactory()\n        self.dataCache = dataCache\n        self.dataLoader = dataLoader\n        self.encoder = encoder\n        self.processorFactory = processorFactory\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.dataCache = dataCache\n            $0.imageCache = nil\n            $0.makeImageEncoder = { _ in encoder }\n        }\n        self.request = ImageRequest(url: Test.url, processors: [processorFactory.make(id: \"1\")])\n    }\n\n    // MARK: - Basics\n\n    @Test func processedImageLoadedFromDataCache() async throws {\n        // Given processed image data stored in data cache\n        dataLoader.queue.isSuspended = true\n        dataCache.store[Test.url.absoluteString + \"1\"] = Test.data\n\n        // When/Then\n        _ = try await pipeline.image(for: request)\n\n        // Then\n        #expect(processorFactory.numberOfProcessorsApplied == 0)\n    }\n\n#if !os(macOS)\n    @Test func processedImageIsDecompressed() async throws {\n        // Given processed image data stored in data cache\n        dataLoader.queue.isSuspended = true\n        dataCache.store[Test.url.absoluteString + \"1\"] = Test.data\n\n        // When/Then\n        let response = try await pipeline.imageTask(with: request).response\n        let image = response.image\n        #expect(ImageDecompression.isDecompressionNeeded(for: image) == nil)\n    }\n\n    @Test func processedImageIsStoredInMemoryCache() async throws {\n        // Given processed image data stored in data cache\n        let cache = MockImageCache()\n        let pipeline = pipeline.reconfigured {\n            $0.imageCache = cache\n        }\n        dataLoader.queue.isSuspended = true\n        dataCache.store[Test.url.absoluteString + \"1\"] = Test.data\n\n        // When\n        _ = try await pipeline.image(for: request)\n\n        // Then decompressed image is stored in disk cache\n        let container = cache[request]\n        #expect(container != nil)\n\n        let image = try #require(container?.image)\n        #expect(ImageDecompression.isDecompressionNeeded(for: image) == nil)\n    }\n\n    @Test func processedImageNotDecompressedWhenDecompressionDisabled() async throws {\n        // Given pipeline with decompression disabled\n        let pipeline = pipeline.reconfigured {\n            $0.isDecompressionEnabled = false\n        }\n\n        // Given processed image data stored in data cache\n        dataLoader.queue.isSuspended = true\n        dataCache.store[Test.url.absoluteString + \"1\"] = Test.data\n\n        // When/Then\n        let response = try await pipeline.imageTask(with: request).response\n        let image = response.image\n        let isDecompressionNeeded = ImageDecompression.isDecompressionNeeded(for: image)\n        #expect(isDecompressionNeeded == true)\n    }\n#endif\n\n    // MARK: DataCachPolicy.automatic\n\n    @Test func policyAutomaticGivenRequestWithProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN encoded processed image is stored in disk cache\n        #expect(encoder.encodeCount == 1)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyAutomaticGivenRequestWithoutProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url)\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyAutomaticGivenTwoRequests() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // WHEN\n        _ = try await pipeline.image(for: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")]))\n        _ = try await pipeline.image(for: ImageRequest(url: Test.url))\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN\n        // encoded processed image is stored in disk cache\n        // original image data is stored in disk cache\n        #expect(encoder.encodeCount == 1)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") != nil)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 2)\n        #expect(dataCache.store.count == 2)\n    }\n\n    @Test func policyAutomaticGivenOriginalImageInMemoryCache() async throws {\n        // GIVEN\n        let imageCache = MockImageCache()\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n            $0.imageCache = imageCache\n        }\n        imageCache[ImageRequest(url: Test.url)] = Test.container\n\n        // WHEN\n        _ = try await pipeline.image(for: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")]))\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN\n        // encoded processed image is stored in disk cache\n        #expect(encoder.encodeCount == 1)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    // MARK: DataCachPolicy.storeEncodedImages\n\n    @Test func policyStoreEncodedImagesGivenRequestWithProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeEncodedImages\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN encoded processed image is stored in disk cache\n        #expect(encoder.encodeCount == 1)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreEncodedImagesGivenRequestWithoutProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeEncodedImages\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url)\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN\n        #expect(encoder.encodeCount == 1)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreEncodedImagesGivenTwoRequests() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeEncodedImages\n        }\n\n        // WHEN\n        _ = try await pipeline.image(for: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")]))\n        _ = try await pipeline.image(for: ImageRequest(url: Test.url))\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN\n        // encoded processed image is stored in disk cache\n        // encoded original image is stored in disk cache\n        #expect(encoder.encodeCount == 2)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") != nil)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 2)\n        #expect(dataCache.store.count == 2)\n    }\n\n    // MARK: DataCachPolicy.storeOriginalData\n\n    @Test func policyStoreOriginalDataGivenRequestWithProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeOriginalData\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN encoded processed image is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreOriginalDataGivenRequestWithoutProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeOriginalData\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url)\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreOriginalDataGivenTwoRequests() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeOriginalData\n        }\n\n        // WHEN\n        let (task1, task2) = await withSuspendedDataLoading(for: pipeline, expectedCount: 2) {\n            (pipeline.imageTask(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])),\n             pipeline.imageTask(with: ImageRequest(url: Test.url)))\n        }\n        _ = try await task1.response\n        _ = try await task2.response\n\n        // THEN\n        // encoded processed image is stored in disk cache\n        // encoded original image is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    // MARK: DataCachPolicy.storeAll\n\n    @Test func policyStoreAllGivenRequestWithProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeAll\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN encoded processed image is stored in disk cache and\n        // original image data stored in disk cache\n        #expect(encoder.encodeCount == 1)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") != nil)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 2)\n        #expect(dataCache.store.count == 2)\n    }\n\n    @Test func policyStoreAllGivenRequestWithoutProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeAll\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url)\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreAllGivenTwoRequests() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeAll\n        }\n\n        // WHEN\n        _ = try await pipeline.image(for: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")]))\n        _ = try await pipeline.image(for: ImageRequest(url: Test.url))\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN\n        // encoded processed image is stored in disk cache\n        // original image data is stored in disk cache\n        #expect(encoder.encodeCount == 1)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") != nil)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 2)\n        #expect(dataCache.store.count == 2)\n    }\n\n    // MARK: Local Resources\n\n    @Test func imagesFromLocalStorageNotCached() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url(forResource: \"fixture\", extension: \"jpeg\"))\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataCache.store.count == 0)\n    }\n\n    @Test func processedImagesFromLocalStorageAreCached() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url(forResource: \"fixture\", extension: \"jpeg\"), processors: [.resize(width: 100)])\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN processed image is stored in disk cache\n        #expect(encoder.encodeCount == 1)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func imagesFromMemoryNotCached() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url(forResource: \"fixture\", extension: \"jpeg\"))\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataCache.store.count == 0)\n    }\n\n    @Test func imagesFromData() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // GIVEN request without a processor\n        let data = Test.data(name: \"fixture\", extension: \"jpeg\")\n        let url = URL(string: \"data:image/jpeg;base64,\\(data.base64EncodedString())\")\n        let request = ImageRequest(url: url)\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataCache.store.count == 0)\n    }\n\n    // MARK: Misc\n\n    @Test func setCustomImageEncoder() async throws {\n        struct MockImageEncoder: ImageEncoding, @unchecked Sendable {\n            let closure: (PlatformImage) -> Data?\n\n            func encode(_ image: PlatformImage) -> Data? {\n                return closure(image)\n            }\n        }\n\n        // Given\n        nonisolated(unsafe) var isCustomEncoderCalled = false\n        let encoder = MockImageEncoder { _ in\n            isCustomEncoderCalled = true\n            return nil\n        }\n\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n            $0.makeImageEncoder = { _ in\n                return encoder\n            }\n        }\n\n        // When\n        _ = try await pipeline.image(for: request)\n\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(isCustomEncoderCalled)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"1\") == nil)\n    }\n\n    // MARK: Integration with Thumbnail Feature\n\n    @Test func originalDataStoredWhenThumbnailRequested() async throws {\n        // GIVEN\n        var request = ImageRequest(url: Test.url)\n        request.thumbnail = .init(maxPixelSize: 400)\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN\n        #expect(dataCache.containsData(for: \"http://test.com/example.jpeg\"))\n    }\n\n    // MARK: - Thumbnail + Original Data Reuse\n\n    @Test func thumbnailRequestReusesOriginalDataFromDiskCache() async throws {\n        // GIVEN original image is loaded (no thumbnail), caching original data to disk\n        _ = try await pipeline.image(for: Test.request)\n        #expect(dataCache.containsData(for: Test.url.absoluteString))\n\n        // WHEN a thumbnail of the same URL is requested\n        var thumbnailRequest = ImageRequest(url: Test.url)\n        thumbnailRequest.thumbnail = .init(maxPixelSize: 400)\n\n        _ = try await pipeline.image(for: thumbnailRequest)\n\n        // THEN no additional network request is made — the original data from\n        // the disk cache should be reused to generate the thumbnail locally\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineDecodingTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineDecodingTests {\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    @Test func experimentalDecoder() async throws {\n        // Given\n        let decoder = MockExperimentalDecoder()\n\n        let dummyImage = PlatformImage()\n        let dummyData = \"123\".data(using: .utf8)\n        decoder._decode = { data in\n            return ImageContainer(image: dummyImage, data: dummyData, userInfo: [\"a\": 1])\n        }\n\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in decoder }\n        }\n\n        // When\n        let response = try await pipeline.imageTask(with: Test.request).response\n\n        // Then\n        let container = response.container\n        #expect(container.data == dummyData)\n        #expect(container.userInfo[\"a\"] as? Int == 1)\n    }\n\n    // MARK: - Decoder Errors\n\n    @Test func decoderReturningNilResultsInDecodingFailedError() async throws {\n        // GIVEN a decoder whose _decode closure returns nil (causes decode() to throw)\n        let decoder = MockExperimentalDecoder()\n        decoder._decode = { _ in nil }\n\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in decoder }\n        }\n\n        // WHEN\n        do {\n            _ = try await pipeline.imageTask(with: Test.request).response\n            Issue.record(\"Expected a decoding error\")\n        } catch {\n            // THEN the pipeline wraps it in a decodingFailed error\n            if case .decodingFailed = error {\n                // Expected\n            } else {\n                Issue.record(\"Expected decodingFailed, got \\(error)\")\n            }\n        }\n    }\n\n    @Test func whenDecoderFactoryReturnsNilPipelineErrors() async throws {\n        // GIVEN a pipeline where no decoder can handle the content\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in nil }\n        }\n\n        // WHEN\n        do {\n            _ = try await pipeline.imageTask(with: Test.request).response\n            Issue.record(\"Expected decoderNotRegistered error\")\n        } catch {\n            // THEN\n            if case .decoderNotRegistered = error {\n                // Expected\n            } else {\n                Issue.record(\"Expected decoderNotRegistered, got \\(error)\")\n            }\n        }\n    }\n}\n\nprivate final class MockExperimentalDecoder: ImageDecoding, @unchecked Sendable {\n    var _decode: ((Data) -> ImageContainer?)!\n\n    func decode(_ data: Data) throws -> ImageContainer {\n        guard let image = _decode(data) else {\n            throw ImageDecodingError.unknown\n        }\n        return image\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineDelegateTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineDelegateTests {\n    private let dataLoader: MockDataLoader\n    private let dataCache: MockDataCache\n    private let pipeline: ImagePipeline\n    private let delegate: MockImagePipelineDelegate\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let dataCache = MockDataCache()\n        let delegate = MockImagePipelineDelegate()\n        self.dataLoader = dataLoader\n        self.dataCache = dataCache\n        self.delegate = delegate\n        self.pipeline = ImagePipeline(delegate: delegate) {\n            $0.dataLoader = dataLoader\n            $0.dataCache = dataCache\n            $0.dataCachePolicy = .automatic\n            $0.imageCache = nil\n        }\n    }\n\n    @Test @MainActor func customizingDataCacheKey() async throws {\n        // GIVEN\n        let imageURLSmall = URL(string: \"https://example.com/image-01-small.jpeg\")!\n        let imageURLMedium = URL(string: \"https://example.com/image-01-medium.jpeg\")!\n\n        dataLoader.results[imageURLMedium] = .success(\n            (Test.data, URLResponse(url: imageURLMedium, mimeType: \"jpeg\", expectedContentLength: Test.data.count, textEncodingName: nil))\n        )\n\n        // GIVEN image is loaded from medium size URL and saved in cache using imageId \"image-01-small\"\n        let requestA = ImageRequest(\n            url: imageURLMedium,\n            processors: [.resize(width: 44)],\n            userInfo: [\"imageId\": \"image-01-small\"]\n        )\n        _ = try await pipeline.image(for: requestA)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        let data = try #require(dataCache.cachedData(for: \"image-01-small\"))\n        let image = try #require(PlatformImage(data: data))\n        #expect(image.sizeInPixels.width == 44 * Screen.scale)\n\n        // GIVEN a request for a small image\n        let requestB = ImageRequest(\n            url: imageURLSmall,\n            userInfo: [\"imageId\": \"image-01-small\"]\n        )\n\n        // WHEN/THEN the image is returned from the disk cache\n        let responseB = try await pipeline.imageTask(with: requestB).response\n        #expect(responseB.image.sizeInPixels.width == 44 * Screen.scale)\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    @Test func dataIsStoredInCache() async throws {\n        // WHEN\n        _ = try await pipeline.image(for: Test.request)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN\n        #expect(!dataCache.store.isEmpty)\n    }\n\n    @Test func dataIsStoredInCacheWhenCacheDisabled() async throws {\n        // WHEN\n        delegate.isCacheEnabled = false\n        _ = try await pipeline.image(for: Test.request)\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n\n        // THEN\n        #expect(dataCache.store.isEmpty)\n    }\n\n    // MARK: - willLoadData\n\n    @Test func willLoadDataIsCalled() async throws {\n        // WHEN\n        _ = try await pipeline.image(for: Test.request)\n\n        // THEN\n        #expect(delegate.willLoadDataCallCount == 1)\n        #expect(delegate.willLoadDataRequest?.url == Test.url)\n    }\n\n    @Test func willLoadDataCanModifyRequest() async throws {\n        // GIVEN\n        let trackingLoader = TrackingDataLoader(wrapping: dataLoader)\n        delegate.urlRequestModifier = { request in\n            var request = request\n            request.setValue(\"Bearer token123\", forHTTPHeaderField: \"Authorization\")\n            return request\n        }\n        let pipeline = ImagePipeline(delegate: delegate) {\n            $0.dataLoader = trackingLoader\n            $0.dataCache = dataCache\n            $0.imageCache = nil\n        }\n\n        // WHEN\n        _ = try await pipeline.image(for: Test.request)\n\n        // THEN the data loader received the modified request\n        #expect(trackingLoader.lastRequest?.value(forHTTPHeaderField: \"Authorization\") == \"Bearer token123\")\n    }\n\n    @Test func willLoadDataThrowingCancelsWithDataLoadingFailed() async throws {\n        // GIVEN\n        struct TokenRefreshError: Error {}\n        delegate.willLoadDataError = TokenRefreshError()\n\n        // WHEN\n        do {\n            _ = try await pipeline.image(for: Test.request)\n            Issue.record(\"Expected an error\")\n        } catch {\n            // THEN the error is wrapped in dataLoadingFailed\n            guard case .dataLoadingFailed(let underlying) = error else {\n                Issue.record(\"Expected dataLoadingFailed, got \\(error)\")\n                return\n            }\n            #expect(underlying is TokenRefreshError)\n        }\n    }\n\n    @Test func willLoadDataIsNotCalledForCustomDataFetch() async throws {\n        // GIVEN a request using a custom data fetch closure\n        let request = ImageRequest(id: \"test\", data: {\n            Test.data\n        })\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN willLoadData is NOT called (custom fetch bypasses URL loading)\n        #expect(delegate.willLoadDataCallCount == 0)\n    }\n}\n\nprivate final class MockImagePipelineDelegate: ImagePipeline.Delegate, @unchecked Sendable {\n    var isCacheEnabled = true\n\n    // willLoadData tracking\n    var willLoadDataCallCount = 0\n    var willLoadDataRequest: URLRequest?\n    var urlRequestModifier: ((URLRequest) -> URLRequest)?\n    var willLoadDataError: Error?\n\n    func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? {\n        request.userInfo[\"imageId\"] as? String\n    }\n\n    func willCache(data: Data, image: ImageContainer?, for request: ImageRequest, pipeline: ImagePipeline, completion: @escaping (Data?) -> Void) {\n        completion(isCacheEnabled ? data : nil)\n    }\n\n    func willLoadData(\n        for request: ImageRequest,\n        urlRequest: URLRequest,\n        pipeline: ImagePipeline\n    ) async throws -> URLRequest {\n        willLoadDataCallCount += 1\n        willLoadDataRequest = urlRequest\n        if let error = willLoadDataError { throw error }\n        return urlRequestModifier?(urlRequest) ?? urlRequest\n    }\n}\n\nprivate final class TrackingDataLoader: DataLoading, @unchecked Sendable {\n    private let wrapped: MockDataLoader\n    var lastRequest: URLRequest?\n\n    init(wrapping loader: MockDataLoader) {\n        self.wrapped = loader\n    }\n\n    func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse) {\n        lastRequest = request\n        return try await wrapped.loadData(with: request)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineFormatsTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineFormatsTests {\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    @Test func extendedColorSpaceSupport() async throws {\n        // Given\n        dataLoader.results[Test.url] = .success(\n            (Test.data(name: \"image-p3\", extension: \"jpg\"), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        // When\n        let response = try await pipeline.imageTask(with: Test.request).response\n\n        // Then\n        let image = response.image\n        let cgImage = try #require(image.cgImage)\n        let colorSpace = try #require(cgImage.colorSpace)\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n        #expect(colorSpace.isWideGamutRGB)\n#elseif os(watchOS)\n        #expect(!colorSpace.isWideGamutRGB)\n#endif\n    }\n\n    @Test func grayscaleSupport() async throws {\n        // Given\n        dataLoader.results[Test.url] = .success(\n            (Test.data(name: \"grayscale\", extension: \"jpeg\"), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        // When\n        let response = try await pipeline.imageTask(with: Test.request).response\n\n        // Then\n        let image = response.image\n        let cgImage = try #require(image.cgImage)\n        #expect(cgImage.bitsPerComponent == 8)\n    }\n\n    // MARK: - Image Formats\n\n    @Test func loadPNG() async throws {\n        // GIVEN a pipeline that returns PNG data\n        dataLoader.results[Test.url] = .success(\n            (Test.data(name: \"fixture\", extension: \"png\"),\n             URLResponse(url: Test.url, mimeType: \"png\", expectedContentLength: 0, textEncodingName: nil))\n        )\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: Test.request).response\n\n        // THEN the image is decoded correctly\n        #expect(response.container.type == .png)\n        #expect(response.image.sizeInPixels == CGSize(width: 640, height: 360))\n    }\n\n    @Test func loadGIF() async throws {\n        // GIVEN a pipeline that returns GIF data\n        dataLoader.results[Test.url] = .success(\n            (Test.data(name: \"cat\", extension: \"gif\"),\n             URLResponse(url: Test.url, mimeType: \"gif\", expectedContentLength: 0, textEncodingName: nil))\n        )\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: Test.request).response\n\n        // THEN GIF data is preserved in the container for animated playback\n        #expect(response.container.type == .gif)\n        #expect(response.container.data != nil)\n    }\n\n    @Test func loadHEIC() async throws {\n        // GIVEN a pipeline that returns HEIC data\n        dataLoader.results[Test.url] = .success(\n            (Test.data(name: \"img_751\", extension: \"heic\"),\n             URLResponse(url: Test.url, mimeType: \"heic\", expectedContentLength: 0, textEncodingName: nil))\n        )\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: Test.request).response\n\n        // THEN image is decoded correctly\n        #expect(response.container.type == .heic)\n        #expect(response.image.sizeInPixels != .zero)\n    }\n\n#if os(iOS) || os(macOS) || os(visionOS)\n    @Test func loadWebP() async throws {\n        // GIVEN a pipeline that returns WebP data\n        dataLoader.results[Test.url] = .success(\n            (Test.data(name: \"baseline\", extension: \"webp\"),\n             URLResponse(url: Test.url, mimeType: \"webp\", expectedContentLength: 0, textEncodingName: nil))\n        )\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: Test.request).response\n\n        // THEN image is decoded correctly\n        #expect(response.container.type == .webp)\n        #expect(response.image.sizeInPixels == CGSize(width: 550, height: 368))\n    }\n#endif\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineImageCacheTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n/// Test how well image pipeline interacts with memory cache.\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineImageCacheTests {\n    let dataLoader: MockDataLoader\n    let cache: MockImageCache\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let cache = MockImageCache()\n        self.dataLoader = dataLoader\n        self.cache = cache\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = cache\n        }\n    }\n\n    @Test func thatImageIsLoaded() async throws {\n        _ = try await pipeline.image(for: Test.request)\n    }\n\n    // MARK: Caching\n\n    @Test func cacheWrite() async throws {\n        // When\n        _ = try await pipeline.image(for: Test.request)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 1)\n        #expect(cache[Test.request] != nil)\n    }\n\n    @Test func cacheRead() async throws {\n        // Given\n        cache[Test.request] = ImageContainer(image: Test.image)\n\n        // When\n        _ = try await pipeline.image(for: Test.request)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 0)\n        #expect(cache[Test.request] != nil)\n    }\n\n    @Test func cacheWriteDisabled() async throws {\n        // Given\n        var request = Test.request\n        request.options.insert(.disableMemoryCacheWrites)\n\n        // When\n        _ = try await pipeline.image(for: request)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 1)\n        #expect(cache[Test.request] == nil)\n    }\n\n    @Test func memoryCacheReadDisabled() async throws {\n        // Given\n        cache[Test.request] = ImageContainer(image: Test.image)\n\n        var request = Test.request\n        request.options.insert(.disableMemoryCacheReads)\n\n        // When\n        _ = try await pipeline.image(for: request)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 1)\n        #expect(cache[Test.request] != nil)\n    }\n\n    @Test func reloadIgnoringCachedData() async throws {\n        // Given\n        cache[Test.request] = ImageContainer(image: Test.image)\n\n        var request = Test.request\n        request.options.insert(.reloadIgnoringCachedData)\n\n        // When\n        _ = try await pipeline.image(for: request)\n\n        // Then\n        #expect(dataLoader.createdTaskCount == 1)\n        #expect(cache[Test.request] != nil)\n    }\n\n    @Test func generatedThumbnailDataIsStoredInCache() async throws {\n        // When\n        let request = ImageRequest(url: Test.url).with { $0.thumbnail = .init(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFit) }\n        _ = try await pipeline.image(for: request)\n\n        // Then\n        let container = try #require(pipeline.cache[request])\n        #expect(container.image.sizeInPixels == CGSize(width: 400, height: 300))\n        #expect(pipeline.cache[ImageRequest(url: Test.url)] == nil)\n    }\n}\n\n/// Make sure that cache layers are checked in the correct order and the\n/// minimum necessary number of cache lookups are performed.\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineCacheLayerPriorityTests {\n    let pipeline: ImagePipeline\n    let dataLoader: MockDataLoader\n    let imageCache: MockImageCache\n    let dataCache: MockDataCache\n    let processorFactory: MockProcessorFactory\n\n    let request: ImageRequest\n    let intermediateRequest: ImageRequest\n    let processedImage: ImageContainer\n    let intermediateImage: ImageContainer\n    let originalRequest: ImageRequest\n    let originalImage: ImageContainer\n\n    init() {\n        let dataCache = MockDataCache()\n        let dataLoader = MockDataLoader()\n        let imageCache = MockImageCache()\n        let processorFactory = MockProcessorFactory()\n        self.dataCache = dataCache\n        self.dataLoader = dataLoader\n        self.imageCache = imageCache\n        self.processorFactory = processorFactory\n\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.dataCache = dataCache\n            $0.imageCache = imageCache\n        }\n\n        self.request = ImageRequest(url: Test.url, processors: [\n            processorFactory.make(id: \"1\"),\n            processorFactory.make(id: \"2\")\n        ])\n\n        self.intermediateRequest = ImageRequest(url: Test.url, processors: [\n            processorFactory.make(id: \"1\")\n        ])\n\n        self.originalRequest = ImageRequest(url: Test.url)\n\n        do {\n            let image = PlatformImage(data: Test.data)!\n            image.nk_test_processorIDs = [\"1\", \"2\"]\n            self.processedImage = ImageContainer(image: image)\n        }\n\n        do {\n            let image = PlatformImage(data: Test.data)!\n            image.nk_test_processorIDs = [\"1\"]\n            self.intermediateImage = ImageContainer(image: image)\n        }\n\n        self.originalImage = ImageContainer(image: PlatformImage(data: Test.data)!)\n    }\n\n    @Test func givenProcessedImageInMemoryCache() async throws {\n        // GIVEN\n        imageCache[request] = processedImage\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.image === processedImage.image)\n        #expect(response.cacheType == .memory)\n\n        // THEN\n        #expect(imageCache.readCount == 1)\n        #expect(imageCache.writeCount == 1) // Initial write\n        #expect(dataCache.readCount == 0)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func givenProcessedImageInBothMemoryAndDiskCache() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(processedImage, for: request, caches: [.all])\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.image === processedImage.image)\n        #expect(response.cacheType == .memory)\n\n        // THEN\n        #expect(imageCache.readCount == 1)\n        #expect(imageCache.writeCount == 1) // Initial write\n        #expect(dataCache.readCount == 0)\n        #expect(dataCache.writeCount == 1) // Initial write\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func givenProcessedImageInDiskCache() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(processedImage, for: request, caches: [.disk])\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.cacheType == .disk)\n\n        // THEN\n        #expect(imageCache.readCount == 1)\n        #expect(imageCache.writeCount == 1) // Initial write\n        #expect(dataCache.readCount == 1)\n        #expect(dataCache.writeCount == 1) // Initial write\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func givenProcessedImageInDiskCacheAndIntermediateImageInMemoryCache() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(processedImage, for: request, caches: [.disk])\n        pipeline.cache.storeCachedImage(intermediateImage, for: intermediateRequest, caches: [.memory])\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.cacheType == .disk)\n\n        // THEN\n        #expect(imageCache.readCount == 1)\n        #expect(imageCache.writeCount == 2) // Initial write\n        #expect(imageCache[request] != nil)\n        #expect(imageCache[intermediateRequest] != nil)\n        #expect(dataCache.readCount == 1)\n        #expect(dataCache.writeCount == 1) // Initial write\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func givenIntermediateImageInMemoryCache() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(intermediateImage, for: intermediateRequest, caches: [.memory])\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.image.nk_test_processorIDs == [\"1\", \"2\"])\n        #expect(response.cacheType == .memory)\n\n        // THEN\n        #expect(imageCache.readCount == 2) // Processed + intermediate\n        #expect(imageCache.writeCount == 2) // Initial write\n        #expect(imageCache[request] != nil)\n        #expect(imageCache[intermediateRequest] != nil)\n        #expect(dataCache.readCount == 1) // Check original image\n        #expect(dataCache.writeCount == 0)\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func givenOriginalAndIntermediateImageInMemoryCache() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(intermediateImage, for: intermediateRequest, caches: [.memory])\n        pipeline.cache.storeCachedImage(originalImage, for: originalRequest, caches: [.memory])\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.image.nk_test_processorIDs == [\"1\", \"2\"])\n        #expect(response.cacheType == .memory)\n\n        // THEN\n        #expect(imageCache.readCount == 2) // Processed + intermediate\n        #expect(imageCache.writeCount == 3) // Initial write + write processed\n        #expect(imageCache[originalRequest] != nil)\n        #expect(imageCache[request] != nil)\n        #expect(imageCache[intermediateRequest] != nil)\n        #expect(dataCache.readCount == 1) // Check original image\n        #expect(dataCache.writeCount == 0)\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func givenOriginalImageInBothCaches() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(originalImage, for: originalRequest, caches: [.all])\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.image.nk_test_processorIDs == [\"1\", \"2\"])\n        #expect(response.cacheType == .memory)\n\n        // THEN\n        #expect(imageCache.readCount == 3) // Processed + intermediate + original\n        #expect(imageCache.writeCount == 2) // Processed + original\n        #expect(imageCache[originalRequest] != nil)\n        #expect(imageCache[request] != nil)\n        #expect(dataCache.readCount == 2) // \"1\", \"2\"\n        #expect(dataCache.writeCount == 1) // Initial\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func givenOriginalImageInDiskCache() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(originalImage, for: originalRequest, caches: [.disk])\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.image.nk_test_processorIDs == [\"1\", \"2\"])\n        #expect(response.cacheType == .disk)\n\n        // THEN\n        #expect(imageCache.readCount == 3) // Processed + intermediate + original\n        #expect(imageCache.writeCount == 1) // Processed\n        #expect(imageCache[request] != nil)\n        #expect(dataCache.readCount == 3) // \"1\" + \"2\" + original\n        #expect(dataCache.writeCount == 1) // Initial\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func policyStoreEncodedImagesGivenDataAlreadyStored() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeEncodedImages\n        }\n\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n        pipeline.cache.storeCachedImage(Test.container, for: request, caches: [.disk])\n        dataCache.resetCounters()\n        imageCache.resetCounters()\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.cacheType == .disk)\n\n        // THEN\n        #expect(imageCache.readCount == 1)\n        #expect(imageCache.writeCount == 1)\n        #expect(dataCache.readCount == 1)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    // MARK: ImageRequest.Options\n\n    @Test func givenOriginalImageInDiskCacheAndDiskReadsDisabled() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(originalImage, for: originalRequest, caches: [.disk])\n\n        // WHEN\n        var request = request\n        request.options.insert(.disableDiskCacheReads)\n        let response = try await pipeline.imageTask(with: request).response\n        await pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished()\n        #expect(response.image.nk_test_processorIDs == [\"1\", \"2\"])\n        #expect(response.cacheType == nil)\n\n        // THEN\n        #expect(imageCache.readCount == 3) // Processed + intermediate + original\n        #expect(imageCache.writeCount == 1) // Processed\n        #expect(imageCache[request] != nil)\n        #expect(dataCache.readCount == 0) // Processed + original\n        #expect(dataCache.writeCount == 2) // Initial + processed\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    @Test func givenNoImageDataInDiskCacheAndDiskWritesDisabled() async throws {\n        // WHEN\n        var request = request\n        request.options.insert(.disableDiskCacheWrites)\n        let response = try await pipeline.imageTask(with: request).response\n        #expect(response.image.nk_test_processorIDs == [\"1\", \"2\"])\n        #expect(response.cacheType == nil)\n\n        // THEN\n        #expect(imageCache.readCount == 3) // Processed + intermediate + original\n        #expect(imageCache.writeCount == 1) // Processed\n        #expect(imageCache[request] != nil)\n        #expect(dataCache.readCount == 3) // \"1\" + \"2\" + original\n        #expect(dataCache.writeCount == 0)\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineLoadDataTests {\n    let dataLoader: MockDataLoader\n    let dataCache: MockDataCache\n    let pipeline: ImagePipeline\n    let encoder: MockImageEncoder\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let dataCache = MockDataCache()\n        let encoder = MockImageEncoder(result: Test.data)\n        self.dataLoader = dataLoader\n        self.dataCache = dataCache\n        self.encoder = encoder\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.dataCache = dataCache\n            $0.imageCache = nil\n            $0.makeImageEncoder = { _ in encoder }\n        }\n    }\n\n    @Test func loadDataDataLoaded() async throws {\n        let (data, _) = try await pipeline.data(for: Test.request)\n        #expect(data.count == 22789)\n    }\n\n    // MARK: - Progress Reporting\n\n    @Test func progressClosureIsCalled() async throws {\n        // Given\n        dataLoader.results[Test.url] = .success(\n            (Data(count: 20), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        // When\n        let task = pipeline.imageTask(with: Test.url)\n        var progressValues: [ImageTask.Progress] = []\n        for await progress in task.progress {\n            progressValues.append(progress)\n        }\n        _ = try? await task.response\n\n        // Then\n        #expect(progressValues == [\n            ImageTask.Progress(completed: 10, total: 20),\n            ImageTask.Progress(completed: 20, total: 20)\n        ])\n    }\n\n    // MARK: - Errors\n\n    @Test func loadWithInvalidURL() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataLoader = DataLoader()\n        }\n\n        // WHEN/THEN\n        do {\n            _ = try await pipeline.data(for: ImageRequest(url: URL(string: \"\")))\n            Issue.record(\"Expected failure\")\n        } catch {\n            // Expected\n        }\n    }\n\n    @Test func downloadExceedingMaximumResponseDataSize() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.maximumResponseDataSize = 1024\n        }\n\n        // WHEN/THEN\n        do {\n            _ = try await pipeline.image(for: Test.request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            guard case .dataDownloadExceededMaximumSize = error else {\n                Issue.record(\"Unexpected error: \\(error)\")\n                return\n            }\n        }\n    }\n\n    // MARK: - ImageRequest.CachePolicy\n\n    @Test func cacheLookupWithDefaultPolicyImageStored() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(Test.container, for: Test.request)\n\n        // WHEN\n        _ = try await pipeline.data(for: Test.request)\n\n        // THEN\n        #expect(dataCache.readCount == 1)\n        #expect(dataCache.writeCount == 1) // Initial write\n        #expect(dataLoader.createdTaskCount == 0)\n    }\n\n    @Test func cacheLookupWithReloadPolicyImageStored() async throws {\n        // GIVEN\n        pipeline.cache.storeCachedImage(Test.container, for: Test.request)\n\n        // WHEN\n        let request = ImageRequest(url: Test.url, options: [.reloadIgnoringCachedData])\n        _ = try await pipeline.data(for: request)\n\n        // THEN\n        #expect(dataCache.readCount == 0)\n        #expect(dataCache.writeCount == 2) // Initial write + write after fetch\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n\n    // MARK: - DataCachePolicy\n\n    // MARK: DataCachPolicy.automatic\n\n    @Test func policyAutomaticGivenRequestWithProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n\n        // WHEN\n        _ = try await pipeline.data(for: request)\n\n        // THEN nothing is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") == nil)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataCache.store.count == 0)\n    }\n\n    @Test func policyAutomaticGivenRequestWithoutProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url)\n\n        // WHEN\n        _ = try await pipeline.data(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyAutomaticGivenTwoRequests() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .automatic\n        }\n\n        // WHEN\n        _ = try await pipeline.data(for: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")]))\n        _ = try await pipeline.data(for: ImageRequest(url: Test.url))\n\n        // THEN\n        // only original image is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") == nil)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    // MARK: DataCachPolicy.storeOriginalData\n\n    @Test func policyStoreOriginalDataGivenRequestWithProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeOriginalData\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n\n        // WHEN\n        _ = try await pipeline.data(for: request)\n\n        // THEN nothing is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreOriginalDataGivenRequestWithoutProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeOriginalData\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url)\n\n        // WHEN\n        _ = try await pipeline.data(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreOriginalDataGivenTwoRequests() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeOriginalData\n        }\n\n        // WHEN\n        _ = try await pipeline.data(for: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")]))\n        _ = try await pipeline.data(for: ImageRequest(url: Test.url))\n\n        // THEN\n        // only original image is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") == nil)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    // MARK: DataCachPolicy.storeEncodedImages\n\n    @Test func policyStoreEncodedImagesGivenRequestWithProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeEncodedImages\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n\n        // WHEN\n        _ = try await pipeline.data(for: request)\n\n        // THEN nothing is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) == nil)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataCache.store.count == 0)\n    }\n\n    @Test func policyStoreEncodedImagesGivenRequestWithoutProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeEncodedImages\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url)\n\n        // WHEN\n        _ = try await pipeline.data(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) == nil)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataCache.store.count == 0)\n    }\n\n    @Test func policyStoreEncodedImagesGivenTwoRequests() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeEncodedImages\n        }\n\n        // WHEN\n        _ = try await pipeline.data(for: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")]))\n        _ = try await pipeline.data(for: ImageRequest(url: Test.url))\n\n        // THEN\n        // only original image is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") == nil)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) == nil)\n        #expect(dataCache.writeCount == 0)\n        #expect(dataCache.store.count == 0)\n    }\n\n    // MARK: DataCachPolicy.storeAll\n\n    @Test func policyStoreAllGivenRequestWithProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeAll\n        }\n\n        // GIVEN request with a processor\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")])\n\n        // WHEN\n        _ = try await pipeline.data(for: request)\n\n        // THEN nothing is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreAllGivenRequestWithoutProcessors() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeAll\n        }\n\n        // GIVEN request without a processor\n        let request = ImageRequest(url: Test.url)\n\n        // WHEN\n        _ = try await pipeline.data(for: request)\n\n        // THEN original image data is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n\n    @Test func policyStoreAllGivenTwoRequests() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataCachePolicy = .storeAll\n        }\n\n        // WHEN\n        _ = try await pipeline.data(for: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"p1\")]))\n        _ = try await pipeline.data(for: ImageRequest(url: Test.url))\n\n        // THEN\n        // only original image is stored in disk cache\n        #expect(encoder.encodeCount == 0)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString + \"p1\") == nil)\n        #expect(dataCache.cachedData(for: Test.url.absoluteString) != nil)\n        #expect(dataCache.writeCount == 1)\n        #expect(dataCache.store.count == 1)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelinePreviewPolicyTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelinePreviewPolicyTests {\n\n    // MARK: - Progressive JPEG (default policy = .incremental)\n\n    @Test func progressiveJPEGDeliversPreviews() async throws {\n        // GIVEN a progressive JPEG served in chunks with manual resume\n        let dataLoader = MockProgressiveDataLoader()\n        let pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.isProgressiveDecodingEnabled = true\n            $0.progressiveDecodingInterval = 0\n            $0.imageCache = nil\n        }\n\n        // WHEN loading the image and collecting previews\n        let task = pipeline.imageTask(with: Test.url)\n        var previews: [ImageResponse] = []\n        for try await preview in task.previews {\n            previews.append(preview)\n            dataLoader.resume()\n        }\n        let finalImage = try await task.image\n\n        // THEN previews are delivered (default policy is .incremental for progressive JPEG)\n        #expect(previews.count >= 1)\n        #expect(previews.allSatisfy { $0.container.isPreview })\n        #expect(finalImage.sizeInPixels == CGSize(width: 450, height: 300))\n    }\n\n    // MARK: - Progressive JPEG with .disabled policy\n\n    @Test func progressiveJPEGWithDisabledPolicyDeliversNoPreviews() async throws {\n        // GIVEN a delegate that disables previews and data sent automatically\n        let delegate = PreviewPolicyDelegate(policy: .disabled)\n        let dataLoader = MockAutoDataLoader(\n            data: Test.data(name: \"progressive\", extension: \"jpeg\")\n        )\n        let pipeline = ImagePipeline(delegate: delegate) {\n            $0.dataLoader = dataLoader\n            $0.isProgressiveDecodingEnabled = true\n            $0.imageCache = nil\n        }\n\n        // WHEN loading the image\n        let task = pipeline.imageTask(with: Test.url)\n        var previews: [ImageResponse] = []\n        for try await preview in task.previews {\n            previews.append(preview)\n        }\n        let finalImage = try await task.image\n\n        // THEN no previews are delivered\n        #expect(previews.isEmpty)\n        #expect(finalImage.sizeInPixels == CGSize(width: 450, height: 300))\n    }\n\n    // MARK: - Baseline JPEG (default policy = .disabled)\n\n    @Test func baselineJPEGDeliversNoPreviewsByDefault() async throws {\n        // GIVEN a baseline JPEG served incrementally (auto, no manual resume)\n        let dataLoader = MockAutoDataLoader(\n            data: Test.data(name: \"baseline\", extension: \"jpeg\")\n        )\n        let pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.isProgressiveDecodingEnabled = true\n            $0.imageCache = nil\n        }\n\n        // WHEN loading the image and collecting previews\n        let task = pipeline.imageTask(with: Test.url)\n        var previews: [ImageResponse] = []\n        for try await preview in task.previews {\n            previews.append(preview)\n        }\n        let finalImage = try await task.image\n\n        // THEN no previews because default policy for baseline JPEG is .disabled\n        #expect(previews.isEmpty)\n        #expect(finalImage.sizeInPixels.width > 0)\n    }\n\n    // MARK: - Baseline JPEG with .incremental policy\n\n    @Test func baselineJPEGWithIncrementalPolicyDeliversPreviews() async throws {\n        // GIVEN a delegate that forces .incremental for all images\n        let delegate = PreviewPolicyDelegate(policy: .incremental)\n        let dataLoader = MockBaselineDataLoader()\n        let pipeline = ImagePipeline(delegate: delegate) {\n            $0.dataLoader = dataLoader\n            $0.isProgressiveDecodingEnabled = true\n            $0.progressiveDecodingInterval = 0\n            $0.imageCache = nil\n        }\n\n        // WHEN loading the image\n        let task = pipeline.imageTask(with: Test.url)\n        var previews: [ImageResponse] = []\n        for try await preview in task.previews {\n            previews.append(preview)\n            dataLoader.resume()\n        }\n        let finalImage = try await task.image\n\n        // THEN previews are delivered because policy is .incremental\n        #expect(previews.count >= 1)\n        #expect(previews.allSatisfy { $0.container.isPreview })\n        #expect(finalImage.sizeInPixels.width > 0)\n    }\n}\n\n// MARK: - Helpers\n\n/// A delegate that returns a fixed preview policy for all requests.\nprivate final class PreviewPolicyDelegate: ImagePipeline.Delegate, @unchecked Sendable {\n    let policy: ImagePipeline.PreviewPolicy\n\n    init(policy: ImagePipeline.PreviewPolicy) {\n        self.policy = policy\n    }\n\n    func previewPolicy(for context: ImageDecodingContext, pipeline: ImagePipeline) -> ImagePipeline.PreviewPolicy {\n        policy\n    }\n}\n\n/// Serves data in chunks automatically without requiring manual resume calls.\nprivate final class MockAutoDataLoader: DataLoading, @unchecked Sendable {\n    let data: Data\n    let urlResponse: HTTPURLResponse\n\n    init(data: Data) {\n        self.data = data\n        self.urlResponse = HTTPURLResponse(\n            url: Test.url,\n            statusCode: 200,\n            httpVersion: \"HTTP/1.1\",\n            headerFields: [\"Content-Length\": \"\\(data.count)\"]\n        )!\n    }\n\n    func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse) {\n        let chunks = Array(_createChunks(for: data, size: data.count / 3))\n        let response = urlResponse\n        let stream = AsyncThrowingStream<Data, Error> { continuation in\n            DispatchQueue.main.async {\n                for chunk in chunks {\n                    continuation.yield(chunk)\n                }\n                continuation.finish()\n            }\n        }\n        return (stream, response)\n    }\n}\n\n/// Serves a baseline JPEG in chunks with manual resume control.\nprivate final class MockBaselineDataLoader: DataLoading, @unchecked Sendable {\n    let urlResponse: HTTPURLResponse\n    var chunks: [Data]\n    let data = Test.data(name: \"baseline\", extension: \"jpeg\")\n\n    private var streamContinuation: AsyncThrowingStream<Data, Error>.Continuation?\n\n    init() {\n        self.urlResponse = HTTPURLResponse(\n            url: Test.url,\n            statusCode: 200,\n            httpVersion: \"HTTP/1.1\",\n            headerFields: [\"Content-Length\": \"\\(data.count)\"]\n        )!\n        self.chunks = Array(_createChunks(for: data, size: data.count / 3))\n    }\n\n    func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse) {\n        let stream = AsyncThrowingStream<Data, Error> { continuation in\n            self.streamContinuation = continuation\n        }\n        // Serve the first chunk immediately\n        DispatchQueue.main.async {\n            self.resume()\n        }\n        return (stream, urlResponse)\n    }\n\n    func resume() {\n        DispatchQueue.main.async {\n            if let chunk = self.chunks.first {\n                self.chunks.removeFirst()\n                self.streamContinuation?.yield(chunk)\n                if self.chunks.isEmpty {\n                    self.streamContinuation?.finish()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineProcessorTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n#if !os(macOS)\nimport UIKit\n#endif\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineProcessorTests {\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    // MARK: - Applying Filters\n\n    @Test func thatImageIsProcessed() async throws {\n        // Given\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"processor1\")])\n\n        // When\n        let response = try await pipeline.imageTask(with: request).response\n\n        // Then\n        #expect(response.image.nk_test_processorIDs == [\"processor1\"])\n    }\n\n    // MARK: - Composing Filters\n\n    @Test func applyingMultipleProcessors() async throws {\n        // Given\n        let request = ImageRequest(\n            url: Test.url,\n            processors: [\n                MockImageProcessor(id: \"processor1\"),\n                MockImageProcessor(id: \"processor2\")\n            ]\n        )\n\n        // When\n        let response = try await pipeline.imageTask(with: request).response\n\n        // Then\n        #expect(response.image.nk_test_processorIDs == [\"processor1\", \"processor2\"])\n    }\n\n    @Test func performingRequestWithoutProcessors() async throws {\n        // Given\n        let request = ImageRequest(url: Test.url, processors: [])\n\n        // When\n        let response = try await pipeline.imageTask(with: request).response\n\n        // Then\n        #expect(response.image.nk_test_processorIDs == [])\n    }\n\n    // MARK: - Processor Failures\n\n    @Test func processorFailurePropagatesAsError() async throws {\n        // GIVEN a request with a processor that always returns nil\n        let request = ImageRequest(url: Test.url, processors: [MockFailingProcessor()])\n\n        // WHEN\n        do {\n            _ = try await pipeline.imageTask(with: request).response\n            Issue.record(\"Expected processing error\")\n        } catch {\n            // THEN the pipeline surfaces a processingFailed error\n            if case .processingFailed = error {\n                // Expected\n            } else {\n                Issue.record(\"Expected processingFailed, got \\(error)\")\n            }\n        }\n    }\n\n    @Test func firstProcessorSucceedsSecondFails() async throws {\n        // GIVEN a request where only the second processor fails\n        let request = ImageRequest(url: Test.url, processors: [\n            MockImageProcessor(id: \"ok\"),\n            MockFailingProcessor()\n        ])\n\n        // WHEN/THEN the error still surfaces even after the first processor succeeds\n        do {\n            _ = try await pipeline.imageTask(with: request).response\n            Issue.record(\"Expected processing error\")\n        } catch {\n            if case .processingFailed = error {\n                // Expected\n            } else {\n                Issue.record(\"Expected processingFailed, got \\(error)\")\n            }\n        }\n    }\n\n    // MARK: - Decompression\n\n#if !os(macOS)\n    @Test func decompressionSkippedIfProcessorsAreApplied() async throws {\n        // Given\n        let request = ImageRequest(url: Test.url, processors: [ImageProcessors.Anonymous(id: \"1\", { image in\n            #expect(ImageDecompression.isDecompressionNeeded(for: image) == true)\n            return image\n        })])\n\n        // When/Then\n        _ = try await pipeline.image(for: request)\n    }\n#endif\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineProgressiveDecodingTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineProgressiveDecodingTests {\n    private let dataLoader: MockProgressiveDataLoader\n    private let pipeline: ImagePipeline\n    private let cache: MockImageCache\n    private let processorsFactory: MockProcessorFactory\n\n    init() {\n        let dataLoader = MockProgressiveDataLoader()\n\n        let cache = MockImageCache()\n        let processorsFactory = MockProcessorFactory()\n\n        self.dataLoader = dataLoader\n        self.cache = cache\n        self.processorsFactory = processorsFactory\n\n        // We make two important assumptions with this setup:\n        //\n        // 1. Image processing is serial which means that all partial images are\n        // going to be processed and sent to the client before the final image is\n        // processed. So there's never going to be a situation where the final\n        // image is processed before one of the partial images.\n        //\n        // 2. Each data chunk produced by a data loader always results in a new\n        // scan. The way we split the data guarantees that.\n\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = cache\n            $0.isProgressiveDecodingEnabled = true\n            $0.isStoringPreviewsInMemoryCache = true\n            $0.progressiveDecodingInterval = 0\n            $0.imageProcessingQueue = TaskQueue(maxConcurrentOperationCount: 1)\n        }\n    }\n\n    // MARK: - Basics\n\n    @Test func progressiveDecoding() async throws {\n        // Given\n        // - An image which supports progressive decoding\n        // - A pipeline with progressive decoding enabled\n\n        // When\n        var recordedPreviews: [ImageResponse] = []\n        let task = pipeline.imageTask(with: Test.request)\n        for try await preview in task.previews {\n            // Then image previews are produced\n            #expect(preview.container.isPreview)\n\n            // Then the preview is stored in memory cache\n            let cached = cache[Test.request]\n            #expect(cached != nil)\n            #expect(cached?.isPreview == true)\n            #expect(cached?.image == preview.container.image)\n\n            recordedPreviews.append(preview)\n            dataLoader.resume()\n        }\n        let response = try await task.response\n\n        // Then two scans are produced\n        #expect(recordedPreviews.count == 2)\n\n        // Then the final image is produced\n        #expect(!response.container.isPreview)\n\n        // Then the preview is overwritten with the final image in memory cache\n        let cached = cache[Test.request]\n        #expect(cached != nil)\n        #expect(cached?.isPreview == false)\n        #expect(cached?.image == response.image)\n    }\n\n    @Test func failedPartialImagesAreIgnored() async throws {\n        // Given\n        class FailingPartialsDecoder: ImageDecoding, @unchecked Sendable {\n            func decode(_ data: Data) throws -> ImageContainer {\n                try ImageDecoders.Default().decode(data)\n            }\n        }\n\n        let registry = ImageDecoderRegistry()\n        registry.register { _ in\n            FailingPartialsDecoder()\n        }\n\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { registry.decoder(for: $0) }\n        }\n\n        // When/Then\n        let task = pipeline.imageTask(with: Test.request)\n\n        // Subscribe to both streams synchronously before suspending to avoid a\n        // race where the background task starts too late and misses the first\n        // progress event (which is served automatically on the main queue).\n        let dataLoader = self.dataLoader\n        let progressEvents = task.progress\n        let previewEvents = task.previews\n\n        Task {\n            for await _ in progressEvents {\n                dataLoader.resume()\n            }\n        }\n\n        var recordedPreviews: [ImageResponse] = []\n        for try await preview in previewEvents {\n            recordedPreviews.append(preview)\n            dataLoader.resume()\n        }\n        let response = try await task.response\n\n        // Then partial images are never produced\n        #expect(recordedPreviews.isEmpty)\n        // Then the final image is produced\n        #expect(!response.isPreview)\n    }\n\n    // MARK: - Image Processing\n\n#if !os(macOS)\n    @Test func partialImagesAreResized() async throws {\n        // Given\n        let image = PlatformImage(data: dataLoader.data)\n        #expect(image?.cgImage?.width == 450)\n        #expect(image?.cgImage?.height == 300)\n\n        let request = ImageRequest(\n            url: Test.url,\n            processors: [ImageProcessors.Resize(size: CGSize(width: 45, height: 30), unit: .pixels)]\n        )\n\n        // When\n        var recordedPreviews: [ImageResponse] = []\n        let task = pipeline.imageTask(with: request)\n        for try await preview in task.previews {\n            #expect(preview.image.cgImage?.width == 45)\n            #expect(preview.image.cgImage?.height == 30)\n            recordedPreviews.append(preview)\n            dataLoader.resume()\n        }\n        let response = try await task.response\n\n        // Then\n        #expect(recordedPreviews.count == 2)\n        #expect(response.image.cgImage?.width == 45)\n        #expect(response.image.cgImage?.height == 30)\n    }\n#endif\n\n    @Test func partialImagesAreProcessed() async throws {\n        // Given\n        let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"_image_processor\")])\n\n        // When\n        var recordedPreviews: [ImageResponse] = []\n        let task = pipeline.imageTask(with: request)\n        for try await preview in task.previews {\n            #expect(preview.image.nk_test_processorIDs.count == 1)\n            #expect(preview.image.nk_test_processorIDs.first == \"_image_processor\")\n            recordedPreviews.append(preview)\n            dataLoader.resume()\n        }\n        let response = try await task.response\n\n        // Then\n        #expect(recordedPreviews.count == 2)\n        #expect(response.image.nk_test_processorIDs.count == 1)\n        #expect(response.image.nk_test_processorIDs.first == \"_image_processor\")\n    }\n\n    @Test func progressiveDecodingDisabled() async throws {\n        // Given\n        var configuration = pipeline.configuration\n        configuration.isProgressiveDecodingEnabled = false\n        let pipeline = ImagePipeline(configuration: configuration)\n\n        // When\n        let task = pipeline.imageTask(with: Test.request)\n\n        // Subscribe to both streams synchronously before suspending to avoid a\n        // race where the background task starts too late and misses the first\n        // progress event (which is served automatically on the main queue).\n        let dataLoader = self.dataLoader\n        let progressEvents = task.progress\n        let previewEvents = task.previews\n        Task {\n            for await _ in progressEvents {\n                dataLoader.resume()\n            }\n        }\n\n        var recordedPreviews: [ImageResponse] = []\n        for try await preview in previewEvents {\n            recordedPreviews.append(preview)\n            dataLoader.resume()\n        }\n        let response = try await task.response\n\n        // Then partial images are never produced\n        #expect(recordedPreviews.isEmpty)\n        #expect(!response.isPreview)\n    }\n\n    // MARK: Back Pressure\n\n    @Test @ImagePipelineActor func backpressureImageDecoding() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in MockImageDecoder(name: \"a\") }\n        }\n\n        let queue = pipeline.configuration.imageDecodingQueue\n        queue.isSuspended = true\n\n        let dataLoader = dataLoader\n        let expectation = TestExpectation(queue: queue, count: 2)\n        let task = pipeline.imageTask(with: ImageRequest(url: Test.url, processors: [ImageProcessors.Anonymous(id: \"1\", { $0 })]))\n        let previewEvents = task.previews\n        let progressEvents = task.progress\n        Task {\n            for try await _ in previewEvents {\n                dataLoader.resume()\n            }\n        }\n        Task {\n            for await _ in progressEvents {\n                dataLoader.resume()\n            }\n        }\n        await expectation.wait()\n\n        // Then only 2 operations: 1 partial, 1 final\n        #expect(expectation.operations.count == 2)\n\n        queue.isSuspended = false\n        let response = try await task.response\n        #expect(!response.isPreview)\n    }\n\n    // MARK: Memory Cache\n\n    @Test func intermediateMemoryCachedResultsAreDelivered() async throws {\n        // GIVEN intermediate result stored in memory cache\n        let request = ImageRequest(url: Test.url, processors: [\n            processorsFactory.make(id: \"1\"),\n            processorsFactory.make(id: \"2\")\n        ])\n        let intermediateRequest = ImageRequest(url: Test.url, processors: [\n            processorsFactory.make(id: \"1\")\n        ])\n        cache[intermediateRequest] = ImageContainer(image: Test.image, isPreview: true)\n\n        pipeline.configuration.dataLoadingQueue.isSuspended = true // Make sure no data is loaded\n\n        // WHEN/THEN the pipeline finds the first preview in the memory cache,\n        // applies the remaining processors and delivers it\n        let task = pipeline.imageTask(with: request)\n        var recordedPreviews: [ImageResponse] = []\n        for try await preview in task.previews {\n            recordedPreviews.append(preview)\n            break // Only need the first preview; data loading is suspended\n        }\n\n        #expect(recordedPreviews.count == 1)\n        #expect(recordedPreviews.first?.image.nk_test_processorIDs == [\"2\"])\n        #expect(recordedPreviews.first?.container.isPreview == true)\n    }\n\n    // MARK: - Cancellation\n\n    @Test func cancelBeforeLoadingStartedIsHandledGracefully() async {\n        // GIVEN a task that is cancelled synchronously before the response is awaited\n        let task = pipeline.imageTask(with: Test.request)\n        task.cancel()\n\n        // THEN awaiting the response either throws (most likely) or succeeds\n        // without crashing. We do NOT record a failure if it succeeds, since\n        // timing means the loading may have already completed.\n        _ = try? await task.response\n    }\n\n    // MARK: Scale\n\n#if os(iOS) || os(visionOS)\n    @Test func overridingImageScaleWithFloat() async throws {\n        // GIVEN\n        let request = ImageRequest(url: Test.url).with { $0.scale = 7.0 }\n\n        // WHEN/THEN\n        let task = pipeline.imageTask(with: request)\n        var previewScale: CGFloat?\n        for try await preview in task.previews {\n            previewScale = preview.image.scale\n            dataLoader.resume()\n        }\n        _ = try await task.response\n\n        #expect(previewScale == 7)\n    }\n#endif\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelinePublisherTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelinePublisherTests {\n    let dataLoader: MockDataLoader\n    let imageCache: MockImageCache\n    let dataCache: MockDataCache\n    let observer: ImagePipelineObserver\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let imageCache = MockImageCache()\n        let dataCache = MockDataCache()\n        let observer = ImagePipelineObserver()\n        self.dataLoader = dataLoader\n        self.imageCache = imageCache\n        self.dataCache = dataCache\n        self.observer = observer\n        self.pipeline = ImagePipeline(delegate: observer) {\n            $0.dataLoader = dataLoader\n            $0.imageCache = imageCache\n            $0.dataCache = dataCache\n        }\n    }\n\n    @Test func loadWithPublisher() async throws {\n        // GIVEN\n        let request = ImageRequest(id: \"a\", data: { Test.data })\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n\n        // THEN\n        #expect(response.image.sizeInPixels == CGSize(width: 640, height: 480))\n    }\n\n    @Test func loadWithPublisherAndApplyProcessor() async throws {\n        // GIVEN\n        var request = ImageRequest(id: \"a\", data: { Test.data })\n        request.processors = [MockImageProcessor(id: \"1\")]\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n\n        // THEN\n        #expect(response.image.sizeInPixels == CGSize(width: 640, height: 480))\n        #expect(response.image.nk_test_processorIDs == [\"1\"])\n    }\n\n    @Test func imageRequestWithPublisher() {\n        // GIVEN\n        let request = ImageRequest(id: \"a\", data: { Test.data })\n\n        // THEN\n        #expect(request.urlRequest == nil)\n        #expect(request.url == nil)\n    }\n\n    @Test func cancellation() async {\n        // GIVEN\n        dataLoader.isSuspended = true\n\n        // WHEN\n        let cancellable = pipeline\n            .imagePublisher(with: Test.request)\n            .sink(receiveCompletion: { _ in }, receiveValue: { _ in })\n\n        await notification(ImagePipelineObserver.didCancelTask, object: observer) {\n            cancellable.cancel()\n        }\n    }\n\n    @Test func dataIsStoredInDataCache() async throws {\n        // GIVEN\n        let request = ImageRequest(id: \"a\", data: { Test.data })\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN\n        #expect(!dataCache.store.isEmpty)\n    }\n\n    @Test func initWithURL() {\n        _ = pipeline.imagePublisher(with: URL(string: \"https://example.com/image.jpeg\")!)\n    }\n\n    @Test func initWithImageRequest() {\n        _ = pipeline.imagePublisher(with: ImageRequest(url: URL(string: \"https://example.com/image.jpeg\")))\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelinePublisherProgressiveDecodingTests {\n    private let dataLoader: MockProgressiveDataLoader\n    private let imageCache: MockImageCache\n    private let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockProgressiveDataLoader()\n        let imageCache = MockImageCache()\n\n        self.dataLoader = dataLoader\n        self.imageCache = imageCache\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = imageCache\n            $0.isResumableDataEnabled = false\n            $0.isProgressiveDecodingEnabled = true\n            $0.progressiveDecodingInterval = 0\n            $0.isStoringPreviewsInMemoryCache = true\n        }\n    }\n\n    @Test func imagePreviewsAreDelivered() async {\n        let expectation = TestExpectation()\n        nonisolated(unsafe) var previewsCount = 0\n        nonisolated(unsafe) var isCompleted = false\n\n        // WHEN\n        let publisher = pipeline.imagePublisher(with: Test.url)\n        let cancellable = publisher.sink(receiveCompletion: { completion in\n            switch completion {\n            case .failure:\n                Issue.record(\"Unexpected failure\")\n            case .finished:\n                isCompleted = true\n                expectation.fulfill()\n            }\n        }, receiveValue: { response in\n            previewsCount += 1\n            if previewsCount == 3 {\n                #expect(!response.container.isPreview)\n            } else {\n                #expect(response.container.isPreview)\n            }\n            self.dataLoader.resume()\n        })\n        await expectation.wait()\n        withExtendedLifetime(cancellable) {}\n\n        #expect(previewsCount == 3) // 2 partial + 1 final\n        #expect(isCompleted)\n    }\n\n    @Test func imagePreviewsAreDeliveredFromMemoryCacheSynchronously() async {\n        // GIVEN\n        pipeline.cache[Test.request] = ImageContainer(image: Test.image, isPreview: true)\n\n        let expectation = TestExpectation()\n        nonisolated(unsafe) var previewsCount = 0\n        nonisolated(unsafe) var isFirstPreviewProduced = false\n\n        // WHEN\n        let publisher = pipeline.imagePublisher(with: Test.url)\n        let cancellable = publisher.sink(receiveCompletion: { completion in\n            switch completion {\n            case .failure:\n                Issue.record(\"Unexpected failure\")\n            case .finished:\n                expectation.fulfill()\n            }\n        }, receiveValue: { response in\n            previewsCount += 1\n            if previewsCount == 5 {\n                #expect(!response.container.isPreview)\n            } else {\n                #expect(response.container.isPreview)\n                if previewsCount >= 3 {\n                    self.dataLoader.resume()\n                } else {\n                    isFirstPreviewProduced = true\n                }\n            }\n        })\n        #expect(isFirstPreviewProduced)\n        await expectation.wait()\n        withExtendedLifetime(cancellable) {}\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineResumableDataTests {\n    private let dataLoader: _MockResumableDataLoader\n    private let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = _MockResumableDataLoader()\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    @Test func thatProgressIsReported() async throws {\n        // Given an initial request failed mid download\n\n        // Expect the progress for the first part of the download to be reported.\n        var initialProgress: [ImageTask.Progress] = []\n        do {\n            let task = pipeline.imageTask(with: Test.request)\n            for await progress in task.progress {\n                initialProgress.append(progress)\n            }\n            _ = try await task.response\n        } catch {\n            // Expected failure\n        }\n\n        #expect(initialProgress == [\n            ImageTask.Progress(completed: 3799, total: 22789),\n            ImageTask.Progress(completed: 7598, total: 22789),\n            ImageTask.Progress(completed: 11397, total: 22789)\n        ])\n\n        // Expect progress closure to continue reporting the progress of the\n        // entire download\n        var remainingProgress: [ImageTask.Progress] = []\n        let task2 = pipeline.imageTask(with: Test.request)\n        for await progress in task2.progress {\n            remainingProgress.append(progress)\n        }\n        _ = try await task2.response\n\n        #expect(remainingProgress == [\n            ImageTask.Progress(completed: 15196, total: 22789),\n            ImageTask.Progress(completed: 18995, total: 22789),\n            ImageTask.Progress(completed: 22789, total: 22789)\n        ])\n    }\n\n    @Test func thatResumableDataIsntSavedIfCancelledWhenDownloadIsCompleted() async throws {\n        // GIVEN an initial partial download that fails and stores resumable data\n        _ = try? await pipeline.imageTask(with: Test.request).response\n\n        // WHEN the download is resumed and completes successfully (all bytes delivered)\n        _ = try await pipeline.imageTask(with: Test.request).response\n\n        // THEN no resumable data remains in storage: the completed download doesn't\n        // produce a partial entry (ResumableData init requires data.count < Content-Length).\n        let stored = await ResumableDataStorage.shared.removeResumableData(\n            for: ImageRequest(url: Test.url),\n            pipeline: pipeline\n        )\n        #expect(stored == nil)\n    }\n}\n\nprivate class _MockResumableDataLoader: DataLoading, @unchecked Sendable {\n    let data: Data = Test.data(name: \"fixture\", extension: \"jpeg\")\n    let eTag: String = \"img_01\"\n\n    func loadData(with request: URLRequest) async throws -> (AsyncThrowingStream<Data, Error>, URLResponse) {\n        let headers = request.allHTTPHeaderFields\n        let data = self.data\n        let eTag = self.eTag\n\n        func sendChunk(_ chunk: Data, of data: Data, statusCode: Int) -> (Data, URLResponse) {\n            let response = HTTPURLResponse(\n                url: request.url!,\n                statusCode: statusCode,\n                httpVersion: \"HTTP/1.2\",\n                headerFields: [\n                    \"Accept-Ranges\": \"bytes\",\n                    \"ETag\": eTag,\n                    \"Content-Range\": \"bytes \\(chunk.startIndex)-\\(chunk.endIndex)/\\(data.count)\",\n                    \"Content-Length\": \"\\(data.count)\"\n                ]\n            )!\n            return (chunk, response)\n        }\n\n        // Check if the client already has some resumable data available.\n        if let range = headers?[\"Range\"], let validator = headers?[\"If-Range\"] {\n            let offset = _groups(regex: \"bytes=(\\\\d*)-\", in: range)[0]\n            guard validator == eTag else {\n                throw URLError(.cancelled)\n            }\n            let remainingData = data[Int(offset)!...]\n            let chunks = Array(_createChunks(for: remainingData, size: data.count / 6 + 1))\n            let firstResult = sendChunk(chunks[0], of: remainingData, statusCode: 206)\n            let stream = AsyncThrowingStream<Data, Error> { continuation in\n                continuation.yield(firstResult.0)\n                for chunk in chunks.dropFirst() {\n                    continuation.yield(sendChunk(chunk, of: remainingData, statusCode: 206).0)\n                }\n                continuation.finish()\n            }\n            return (stream, firstResult.1)\n        } else {\n            var chunks = Array(_createChunks(for: data, size: data.count / 6 + 1))\n            chunks.removeLast(chunks.count / 2)\n            let firstResult = sendChunk(chunks[0], of: data, statusCode: 200)\n            let stream = AsyncThrowingStream<Data, Error> { continuation in\n                continuation.yield(firstResult.0)\n                for chunk in chunks.dropFirst() {\n                    continuation.yield(sendChunk(chunk, of: data, statusCode: 200).0)\n                }\n                continuation.finish(throwing: NSError(domain: NSURLErrorDomain, code: Foundation.URLError.networkConnectionLost.rawValue, userInfo: [:]))\n            }\n            return (stream, firstResult.1)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineTaskDelegateTests {\n    private let dataLoader: MockDataLoader\n    private let pipeline: ImagePipeline\n    private let delegate: ImagePipelineObserver\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let delegate = ImagePipelineObserver()\n        self.dataLoader = dataLoader\n        self.delegate = delegate\n        self.pipeline = ImagePipeline(delegate: delegate) {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    @Test func startAndCompletedEvents() async throws {\n        let completed = TestExpectation(notification: ImagePipelineObserver.didCompleteTask, object: delegate)\n        let response = try await pipeline.imageTask(with: Test.request).response\n        await completed.wait()\n\n        // Then\n        #expect(delegate.events == [\n            ImageTaskEvent.created,\n            .started,\n            .progressUpdated(completedUnitCount: 22789, totalUnitCount: 22789),\n            .completed(result: .success(response))\n        ])\n    }\n\n    @Test func progressUpdateEvents() async throws {\n        let request = ImageRequest(url: Test.url)\n        dataLoader.results[Test.url] = .success(\n            (Data(count: 20), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        let completed = TestExpectation(notification: ImagePipelineObserver.didCompleteTask, object: delegate)\n        var result: Result<ImageResponse, ImagePipeline.Error>?\n        do {\n            let response = try await pipeline.imageTask(with: request).response\n            result = .success(response)\n        } catch {\n            result = .failure(error)\n        }\n        await completed.wait()\n\n        // Then\n        #expect(delegate.events == [\n            ImageTaskEvent.created,\n            .started,\n            .progressUpdated(completedUnitCount: 10, totalUnitCount: 20),\n            .progressUpdated(completedUnitCount: 20, totalUnitCount: 20),\n            .completed(result: try #require(result))\n        ])\n    }\n\n    @Test func cancellationEvents() async {\n        dataLoader.queue.isSuspended = true\n\n        let startExpectation = TestExpectation(notification: MockDataLoader.DidStartTask, object: dataLoader)\n        let task = pipeline.imageTask(with: Test.request)\n        Task.detached { try? await task.response }\n        await startExpectation.wait()\n\n        await notification(ImagePipelineObserver.didCancelTask, object: delegate) {\n            task.cancel()\n        }\n        await Task.yield()\n\n        // Then\n        #expect(delegate.events == [\n            ImageTaskEvent.created,\n            .started,\n            .cancelled\n        ])\n    }\n\n    @Test func errorCompletionEventDelivered() async throws {\n        // GIVEN a data loader that fails\n        let error = URLError(.notConnectedToInternet)\n        dataLoader.results[Test.url] = .failure(error as NSError)\n\n        // WHEN\n        let completed = TestExpectation(notification: ImagePipelineObserver.didCompleteTask, object: delegate)\n        _ = try? await pipeline.imageTask(with: Test.request).response\n        await completed.wait()\n\n        // THEN the delegate receives a completed(.failure(...)) event\n        let events = delegate.events\n        guard case .completed(let result) = events.last else {\n            Issue.record(\"Expected completed event, got \\(events)\")\n            return\n        }\n        if case .success = result {\n            Issue.record(\"Expected failure result\")\n        }\n    }\n\n    @Test func intermediateResponseEventsDelivered() async throws {\n        // GIVEN a pipeline with progressive decoding\n        let dataLoader = MockProgressiveDataLoader()\n        let pipeline = ImagePipeline(delegate: delegate) {\n            $0.dataLoader = dataLoader\n            $0.isProgressiveDecodingEnabled = true\n            $0.progressiveDecodingInterval = 0\n            $0.imageCache = nil\n        }\n\n        // WHEN\n        let task = pipeline.imageTask(with: Test.url)\n        for try await _ in task.previews {\n            dataLoader.resume()\n        }\n        _ = try await task.response\n\n        // THEN intermediate response events are recorded\n        let previews = delegate.events.filter {\n            if case .intermediateResponseReceived = $0 { return true }\n            return false\n        }\n        #expect(previews.count >= 1)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePipelineTests {\n    let dataLoader: MockDataLoader\n    let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.dataLoader = dataLoader\n        self.pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    // MARK: - Progress\n\n    @Test func progressUpdated() async throws {\n        // Given\n        dataLoader.results[Test.url] = .success(\n            (Data(count: 20), URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 20, textEncodingName: nil))\n        )\n\n        // When\n        let task = pipeline.imageTask(with: Test.url)\n        var progressValues: [ImageTask.Progress] = []\n        for await progress in task.progress {\n            progressValues.append(progress)\n        }\n        _ = try? await task.response\n\n        // Then\n        #expect(progressValues == [\n            ImageTask.Progress(completed: 10, total: 20),\n            ImageTask.Progress(completed: 20, total: 20)\n        ])\n    }\n\n    // MARK: - Updating Priority\n\n    @Test @ImagePipelineActor func dataLoadingPriorityUpdated() async throws {\n        // Given\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n\n        let request = Test.request\n        #expect(request.priority == .normal)\n\n        let expectation = TestExpectation(queue: queue, count: 1)\n        let imageTask = pipeline.imageTask(with: request)\n        Task.detached { try await imageTask.response }\n        await expectation.wait()\n\n        // When/Then\n        let operation = try #require(expectation.operations.first)\n        await queue.waitForPriorityChange(of: operation, to: .high) {\n            imageTask.priority = .high\n        }\n    }\n\n    @Test @ImagePipelineActor func decodingPriorityUpdated() async throws {\n        // Given\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in MockImageDecoder(name: \"test\") }\n        }\n\n        let queue = pipeline.configuration.imageDecodingQueue\n        queue.isSuspended = true\n\n        let request = Test.request\n        #expect(request.priority == .normal)\n\n        let expectation = TestExpectation(queue: queue, count: 1)\n        let imageTask = pipeline.imageTask(with: request)\n        Task.detached { try await imageTask.response }\n        await expectation.wait()\n\n        // When/Then\n        let operation = try #require(expectation.operations.first)\n        await queue.waitForPriorityChange(of: operation, to: .high) {\n            imageTask.priority = .high\n        }\n    }\n\n    @Test @ImagePipelineActor func processingPriorityUpdated() async throws {\n        // Given\n        let queue = pipeline.configuration.imageProcessingQueue\n        queue.isSuspended = true\n\n        let request = ImageRequest(url: Test.url, processors: [ImageProcessors.Anonymous(id: \"1\", { $0 })])\n        #expect(request.priority == .normal)\n\n        let expectation = TestExpectation(queue: queue, count: 1)\n        let imageTask = pipeline.imageTask(with: request)\n        Task.detached { try await imageTask.response }\n        await expectation.wait()\n\n        // When/Then\n        let operation = try #require(expectation.operations.first)\n        await queue.waitForPriorityChange(of: operation, to: .high) {\n            imageTask.priority = .high\n        }\n    }\n\n    // MARK: - Cancellation\n\n    @Test func dataLoadingOperationCancelled() async {\n        dataLoader.queue.isSuspended = true\n\n        let startExpectation = TestExpectation(notification: MockDataLoader.DidStartTask, object: dataLoader)\n        let task = pipeline.imageTask(with: Test.request)\n        Task.detached { try? await task.response }\n        await startExpectation.wait()\n\n        await notification(MockDataLoader.DidCancelTask, object: dataLoader) {\n            task.cancel()\n        }\n    }\n\n    @Test @ImagePipelineActor func decodingOperationCancelled() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in MockImageDecoder(name: \"test\") }\n        }\n\n        let queue = pipeline.configuration.imageDecodingQueue\n        queue.isSuspended = true\n\n        let expectation = TestExpectation(queue: queue, count: 1)\n        let task = pipeline.imageTask(with: Test.request)\n        Task.detached { try? await task.response }\n        await expectation.wait()\n\n        // When/Then\n        let operation = try #require(expectation.operations.first)\n        await queue.waitForCancellation(of: operation) {\n            task.cancel()\n        }\n    }\n\n    @Test @ImagePipelineActor func processingOperationCancelled() async throws {\n        // Given\n        let queue = pipeline.configuration.imageProcessingQueue\n        queue.isSuspended = true\n\n        let request = ImageRequest(url: Test.url, processors: [ImageProcessors.Anonymous(id: \"1\", { $0 })])\n\n        let expectation = TestExpectation(queue: queue, count: 1)\n        let task = pipeline.imageTask(with: request)\n        Task.detached { try? await task.response }\n        await expectation.wait()\n\n        // When/Then\n        let operation = try #require(expectation.operations.first)\n        await queue.waitForCancellation(of: operation) {\n            task.cancel()\n        }\n    }\n\n    // MARK: Decompression\n\n#if !os(macOS)\n\n    @Test func disablingDecompression() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.isDecompressionEnabled = false\n        }\n\n        // WHEN\n        let image = try await pipeline.image(for: Test.url)\n\n        // THEN\n        #expect(ImageDecompression.isDecompressionNeeded(for: image) == true)\n    }\n\n    @Test func disablingDecompressionForIndividualRequest() async throws {\n        // GIVEN\n        let request = ImageRequest(url: Test.url, options: [.skipDecompression])\n\n        // WHEN\n        let image = try await pipeline.image(for: request)\n\n        // THEN\n        #expect(ImageDecompression.isDecompressionNeeded(for: image) == true)\n    }\n\n    @Test func decompressionPerformed() async throws {\n        // WHEN\n        let image = try await pipeline.image(for: Test.request)\n\n        // THEN\n        #expect(ImageDecompression.isDecompressionNeeded(for: image) == nil)\n    }\n\n    @Test func decompressionNotPerformedWhenProcessorWasApplied() async throws {\n        // GIVEN request with scaling processor\n        let input = Test.image\n        let pipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in MockAnonymousImageDecoder(output: input) }\n        }\n\n        let request = ImageRequest(url: Test.url, processors: [\n            .resize(size: CGSize(width: 40, height: 40))\n        ])\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN\n        #expect(ImageDecompression.isDecompressionNeeded(for: input) == true)\n    }\n\n    @Test func decompressionPerformedWhenProcessorIsAppliedButDoesNothing() async throws {\n        // Given request with scaling processor\n        let request = ImageRequest(url: Test.url, processors: [MockEmptyImageProcessor()])\n\n        // When\n        let response = try await pipeline.imageTask(with: request).response\n\n        // Then - Expect decompression to be performed (processor was applied but it did nothing)\n        #expect(ImageDecompression.isDecompressionNeeded(for: response.image) == nil)\n    }\n\n#endif\n\n    // MARK: - Thumbnail\n\n    @Test func thumbnailIsGenerated() async throws {\n        // GIVEN\n        let request = ImageRequest(url: Test.url).with { $0.thumbnail = .init(maxPixelSize: 400) }\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n\n        // THEN\n        #expect(response.image.sizeInPixels == CGSize(width: 400, height: 300))\n    }\n\n    @Test @ImagePipelineActor func thumbnailIsGeneratedOnDecodingQueue() async throws {\n        // GIVEN\n        let request = ImageRequest(url: Test.url).with { $0.thumbnail = .init(maxPixelSize: 400) }\n        let observer = TaskQueueObserver(queue: pipeline.configuration.imageDecodingQueue)\n\n        // WHEN\n        _ = try await pipeline.image(for: request)\n\n        // THEN\n        #expect(observer.operations.count >= 1)\n    }\n\n#if os(iOS) || os(visionOS)\n    @Test @ImagePipelineActor func thumbnailIsntDecompressed() async throws {\n        pipeline.configuration.imageDecompressingQueue.isSuspended = true\n\n        // GIVEN\n        let request = ImageRequest(url: Test.url).with { $0.thumbnail = .init(maxPixelSize: 400) }\n\n        // WHEN/THEN - image loads even though decompression queue is suspended\n        _ = try await pipeline.image(for: request)\n    }\n#endif\n\n    // MARK: - CacheKey\n\n    @Test func cacheKeyForRequest() {\n        let request = Test.request\n        #expect(pipeline.cache.makeDataCacheKey(for: request) == \"http://test.com/example.jpeg\")\n    }\n\n    @Test func cacheKeyForRequestWithProcessors() {\n        var request = Test.request\n        request.processors = [ImageProcessors.Anonymous(id: \"1\", { $0 })]\n        #expect(pipeline.cache.makeDataCacheKey(for: request) == \"http://test.com/example.jpeg1\")\n    }\n\n    @Test func cacheKeyForRequestWithThumbnail() {\n        let request = ImageRequest(url: Test.url).with {\n            $0.thumbnail = .init(maxPixelSize: 400)\n        }\n        #expect(pipeline.cache.makeDataCacheKey(for: request) == \"http://test.com/example.jpegcom.github/kean/nuke/thumbnail?maxPixelSize=400.0,options=truetruetruetrue\")\n    }\n\n    @Test func cacheKeyForRequestWithThumbnailFlexibleSize() {\n        let request = ImageRequest(url: Test.url).with {\n            $0.thumbnail = .init(\n                size: CGSize(width: 400, height: 400),\n                unit: .pixels,\n                contentMode: .aspectFit\n            )\n        }\n        #expect(pipeline.cache.makeDataCacheKey(for: request) == \"http://test.com/example.jpegcom.github/kean/nuke/thumbnail?width=400.0,height=400.0,contentMode=.aspectFit,options=truetruetruetrue\")\n    }\n\n    // MARK: - Invalidate\n\n    @Test func whenInvalidatedTasksAreCancelled() async {\n        dataLoader.queue.isSuspended = true\n\n        let startExpectation = TestExpectation(notification: MockDataLoader.DidStartTask, object: dataLoader)\n        let task = pipeline.imageTask(with: Test.request)\n        Task.detached { try? await task.response }\n        await startExpectation.wait()\n\n        await notification(MockDataLoader.DidCancelTask, object: dataLoader) {\n            pipeline.invalidate()\n        }\n    }\n\n    @Test func invalidatedTasksFailWithError() async throws {\n        // WHEN\n        pipeline.invalidate()\n\n        // THEN\n        do {\n            _ = try await pipeline.image(for: Test.request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            #expect(error == .pipelineInvalidated)\n        }\n    }\n\n    // MARK: Error Handling\n\n    @Test func dataLoadingFailedErrorReturned() async {\n        // Given\n        let expectedError = NSError(domain: \"t\", code: 23, userInfo: nil)\n        dataLoader.results[Test.url] = .failure(expectedError)\n\n        // When/Then\n        do {\n            _ = try await pipeline.image(for: Test.request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            #expect(error == .dataLoadingFailed(error: expectedError))\n        }\n    }\n\n    @Test func dataLoaderReturnsEmptyData() async {\n        // Given\n        dataLoader.results[Test.url] = .success((Data(), Test.urlResponse))\n\n        // When/Then\n        do {\n            _ = try await pipeline.image(for: Test.request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            #expect(error == .dataIsEmpty)\n        }\n    }\n\n    @Test func decoderNotRegistered() async {\n        // Given\n        let pipeline = ImagePipeline {\n            $0.dataLoader = MockDataLoader()\n            $0.makeImageDecoder = { _ in nil }\n            $0.imageCache = nil\n        }\n\n        do {\n            _ = try await pipeline.image(for: Test.request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            guard case let .decoderNotRegistered(context) = error else {\n                Issue.record(\"Expected .decoderNotRegistered\")\n                return\n            }\n            #expect(context.request.url == Test.request.url)\n            #expect(context.data.count == 22789)\n            #expect(context.isCompleted)\n            #expect(context.urlResponse?.url == Test.url)\n        }\n    }\n\n    @Test func decodingFailedErrorReturned() async {\n        // Given\n        let decoder = MockFailingDecoder()\n        let pipeline = ImagePipeline {\n            $0.dataLoader = MockDataLoader()\n            $0.makeImageDecoder = { _ in decoder }\n            $0.imageCache = nil\n        }\n\n        // When/Then\n        do {\n            _ = try await pipeline.image(for: Test.request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            if case let .decodingFailed(failedDecoder, context, error) = error {\n                #expect((failedDecoder as? MockFailingDecoder) === decoder)\n                #expect(context.request.url == Test.request.url)\n                #expect(context.data == Test.data)\n                #expect(context.isCompleted)\n                #expect(context.urlResponse?.url == Test.url)\n                #expect(error as? MockError == MockError(description: \"decoder-failed\"))\n            } else {\n                Issue.record(\"Unexpected error: \\(error)\")\n            }\n        }\n    }\n\n    @Test func processingFailedErrorReturned() async {\n        // GIVEN\n        let request = ImageRequest(url: Test.url, processors: [MockFailingProcessor()])\n\n        // WHEN/THEN\n        do {\n            _ = try await pipeline.image(for: request)\n            Issue.record(\"Expected failure\")\n        } catch {\n            guard case let .processingFailed(processor, context, underlyingError) = error else {\n                Issue.record(\"Expected .processingFailed\")\n                return\n            }\n            #expect(processor is MockFailingProcessor)\n            #expect(context.request.url == Test.url)\n            #expect(context.response.container.image.sizeInPixels == CGSize(width: 640, height: 480))\n            #expect(context.response.cacheType == nil)\n            #expect(context.isCompleted == true)\n            #expect(underlyingError as? ImageProcessingError == .unknown)\n        }\n    }\n\n    @Test func imageContainerUserInfo() {\n        // WHEN\n        let container = ImageContainer(image: Test.image, type: nil, isPreview: false, data: nil, userInfo: [.init(\"a\"): 1])\n\n        // THEN\n        #expect(container.userInfo[\"a\"] as? Int == 1)\n    }\n\n    @Test func errorDescription() {\n        #expect(!ImagePipeline.Error.dataLoadingFailed(error: Foundation.URLError(.unknown)).description.isEmpty)\n\n        #expect(!ImagePipeline.Error.decodingFailed(decoder: MockImageDecoder(name: \"test\"), context: .mock, error: MockError(description: \"decoding-failed\")).description.isEmpty)\n\n        let processor = ImageProcessors.Resize(width: 100, unit: .pixels)\n        let error = ImagePipeline.Error.processingFailed(processor: processor, context: .mock, error: MockError(description: \"processing-failed\"))\n        let expected = \"Failed to process the image using processor Resize(size: (100.0, 9999.0) pixels, contentMode: .aspectFit, crop: false, upscale: false). Underlying error: MockError(description: \\\"processing-failed\\\").\"\n        #expect(error.description == expected)\n        #expect(\"\\(error)\" == expected)\n\n        #expect(error.dataLoadingError == nil)\n    }\n\n    // MARK: Skip Data Loading Queue Option\n\n    @Test @ImagePipelineActor func skipDataLoadingQueuePerRequestWithURL() async throws {\n        // Given\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n\n        let request = ImageRequest(url: Test.url, options: [\n            .skipDataLoadingQueue\n        ])\n\n        // Then image is still loaded\n        _ = try await pipeline.image(for: request)\n    }\n\n    @Test @ImagePipelineActor func skipDataLoadingQueuePerRequestWithPublisher() async throws {\n        // Given\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n\n        let request = ImageRequest(id: \"a\", data: { Test.data }, options: [\n            .skipDataLoadingQueue\n        ])\n\n        // Then image is still loaded\n        _ = try await pipeline.image(for: request)\n    }\n\n    // MARK: Misc\n\n    @Test func loadWithStringLiteral() async throws {\n        let image = try await pipeline.image(for: \"https://example.com/image.jpeg\")\n        #expect(image.size != .zero)\n    }\n\n    @Test func loadWithInvalidURL() async throws {\n        // GIVEN\n        let pipeline = pipeline.reconfigured {\n            $0.dataLoader = DataLoader()\n        }\n\n        // WHEN\n        for _ in 0...10 {\n            do {\n                _ = try await pipeline.image(for: ImageRequest(url: URL(string: \"\")))\n                Issue.record(\"Expected failure\")\n            } catch {\n                // Expected\n            }\n        }\n    }\n\n#if !os(macOS)\n    @Test func overridingImageScale() async throws {\n        // GIVEN\n        let request = ImageRequest(url: Test.url).with { $0.scale = 7 }\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n\n        // THEN\n        #expect(response.image.scale == 7)\n    }\n\n    @Test func overridingImageScaleWithFloat() async throws {\n        // GIVEN\n        let request = ImageRequest(url: Test.url).with { $0.scale = 7.0 }\n\n        // WHEN\n        let response = try await pipeline.imageTask(with: request).response\n\n        // THEN\n        #expect(response.image.scale == 7)\n    }\n#endif\n\n    // MARK: - Error Propagation\n\n    @Test func errorPropagatedWhenDataLoadingFails() async {\n        // GIVEN - data loader configured to fail\n        let error = NSError(domain: \"test\", code: -1)\n        dataLoader.results[Test.url] = .failure(error)\n\n        // WHEN\n        do {\n            _ = try await pipeline.imageTask(with: Test.request).response\n            Issue.record(\"Expected error to be thrown\")\n        } catch {\n            // THEN - the underlying error is wrapped in a pipeline error\n            #expect(error.dataLoadingError != nil)\n        }\n    }\n\n    @Test func errorPropagatedToBothCoalescedSubscribers() async {\n        // GIVEN - two tasks for the same URL, data loader will fail\n        let error = NSError(domain: \"test\", code: -1)\n        dataLoader.results[Test.url] = .failure(error)\n\n        // WHEN - both tasks are started concurrently\n        async let result1: ImageResponse = pipeline.imageTask(with: Test.request).response\n        async let result2: ImageResponse = pipeline.imageTask(with: Test.request).response\n\n        var errorCount = 0\n        do { _ = try await result1 } catch { errorCount += 1 }\n        do { _ = try await result2 } catch { errorCount += 1 }\n\n        // THEN - both subscribers receive an error\n        #expect(errorCount == 2)\n        // Only one network request was made (coalesced)\n        #expect(dataLoader.createdTaskCount == 1)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePrefetcherTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePrefetcherTests {\n    private let pipeline: ImagePipeline\n    private let dataLoader: MockDataLoader\n    private let dataCache: MockDataCache\n    private let imageCache: MockImageCache\n    private let observer: ImagePipelineObserver\n    private let prefetcher: ImagePrefetcher\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let dataCache = MockDataCache()\n        let imageCache = MockImageCache()\n        let observer = ImagePipelineObserver()\n        self.dataLoader = dataLoader\n        self.dataCache = dataCache\n        self.imageCache = imageCache\n        self.observer = observer\n        let pipeline = ImagePipeline(delegate: observer) {\n            $0.dataLoader = dataLoader\n            $0.imageCache = imageCache\n            $0.dataCache = dataCache\n        }\n        self.pipeline = pipeline\n        prefetcher = ImagePrefetcher(pipeline: pipeline)\n    }\n\n    // MARK: Basics\n\n    /// Start prefetching for the request and then request an image separately.\n    @Test @ImagePipelineActor func basicScenario() async {\n        dataLoader.isSuspended = true\n\n        _ = await prefetcher.queue.waitForOperations(count: 1) {\n            prefetcher.startPrefetching(with: [Test.request])\n        }\n\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            pipeline.loadImage(with: Test.request, progress: nil) { _ in\n                continuation.resume()\n            }\n            Task { @ImagePipelineActor in\n                dataLoader.isSuspended = false\n            }\n        }\n\n        // THEN\n        #expect(dataLoader.createdTaskCount == 1)\n        #expect(observer.startedTaskCount == 2)\n    }\n\n    // MARK: Start Prefetching\n\n    @Test func startPrefetching() async {\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            prefetcher.didComplete = { @MainActor @Sendable in\n                continuation.resume()\n            }\n            prefetcher.startPrefetching(with: [Test.url])\n        }\n\n        // THEN image saved in both caches\n        #expect(pipeline.cache[Test.request] != nil)\n        #expect(pipeline.cache.cachedData(for: Test.request) != nil)\n    }\n\n    @Test func startPrefetchingWithTwoEquivalentURLs() async {\n        dataLoader.isSuspended = true\n\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            prefetcher.didComplete = { @MainActor @Sendable in\n                continuation.resume()\n            }\n            prefetcher.startPrefetching(with: [Test.url])\n            prefetcher.startPrefetching(with: [Test.url])\n\n            Task { @ImagePipelineActor in\n                dataLoader.isSuspended = false\n            }\n        }\n\n        // THEN only one task is started\n        #expect(observer.startedTaskCount == 1)\n    }\n\n    @Test func whenImageIsInMemoryCacheNoTaskStarted() async {\n        // GIVEN\n        pipeline.cache[Test.request] = Test.container\n\n        // WHEN\n        await withCheckedContinuation { continuation in\n            prefetcher.didComplete = {\n                continuation.resume()\n            }\n            prefetcher.startPrefetching(with: [Test.url])\n        }\n\n        // THEN\n        #expect(observer.startedTaskCount == 0)\n    }\n\n    // MARK: Stop Prefetching\n\n    @Test func stopPrefetching() async {\n        dataLoader.isSuspended = true\n\n        let url = Test.url\n\n        // Wait for start notification\n        await notification(ImagePipelineObserver.didStartTask, object: observer) {\n            prefetcher.startPrefetching(with: [url])\n        }\n\n        // Wait for cancel notification\n        await notification(ImagePipelineObserver.didCancelTask, object: observer) {\n            prefetcher.stopPrefetching(with: [url])\n        }\n    }\n\n    // MARK: Destination\n\n    @Test func startPrefetchingDestinationDisk() async {\n        // GIVEN\n        let localPipeline = pipeline.reconfigured {\n            $0.makeImageDecoder = { _ in\n                Issue.record(\"Expect image not to be decoded\")\n                return nil\n            }\n        }\n        let localPrefetcher = ImagePrefetcher(pipeline: localPipeline, destination: .diskCache)\n\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            localPrefetcher.didComplete = { @MainActor @Sendable in\n                continuation.resume()\n            }\n            localPrefetcher.startPrefetching(with: [Test.url])\n        }\n\n        // THEN image saved in both caches\n        #expect(localPipeline.cache[Test.request] == nil)\n        #expect(localPipeline.cache.cachedData(for: Test.request) != nil)\n    }\n\n    // MARK: Pause\n\n    @Test @ImagePipelineActor func pausingPrefetcher() async {\n        // WHEN\n        prefetcher.isPaused = true\n\n        _ = await prefetcher.queue.waitForOperations(count: 1) {\n            prefetcher.startPrefetching(with: [Test.url])\n        }\n\n        // THEN\n        #expect(observer.startedTaskCount == 0)\n    }\n\n    // MARK: Priority\n\n    @Test @ImagePipelineActor func defaultPrioritySetToLow() async {\n        // WHEN start prefetching with URL\n        pipeline.configuration.dataLoadingQueue.isSuspended = true\n\n        let operations = await pipeline.configuration.dataLoadingQueue.waitForOperations(count: 1) {\n            prefetcher.startPrefetching(with: [Test.url])\n        }\n\n        // THEN priority is set to .low\n        guard let operation = operations.first else {\n            Issue.record(\"Failed to find operation\")\n            return\n        }\n        #expect(operation.priority == .low)\n\n        // Cleanup\n        prefetcher.stopPrefetching()\n    }\n\n    @Test @ImagePipelineActor func defaultPriorityAffectsRequests() async {\n        // WHEN start prefetching with ImageRequest\n        pipeline.configuration.dataLoadingQueue.isSuspended = true\n        let request = Test.request\n        #expect(request.priority == .normal) // Default is .normal\n\n        let operations = await pipeline.configuration.dataLoadingQueue.waitForOperations(count: 1) {\n            prefetcher.startPrefetching(with: [request])\n        }\n\n        // THEN priority is set to .low\n        guard let operation = operations.first else {\n            Issue.record(\"Failed to find operation\")\n            return\n        }\n        #expect(operation.priority == .low)\n    }\n\n    @Test @ImagePipelineActor func lowerPriorityThanDefaultNotAffected() async {\n        // WHEN start prefetching with ImageRequest with .veryLow priority\n        pipeline.configuration.dataLoadingQueue.isSuspended = true\n        var request = Test.request\n        request.priority = .veryLow\n\n        let operations = await pipeline.configuration.dataLoadingQueue.waitForOperations(count: 1) {\n            prefetcher.startPrefetching(with: [request])\n        }\n        await Task.yield()\n\n        // THEN priority is set to .low (prefetcher priority)\n        guard let operation = operations.first else {\n            Issue.record(\"Failed to find operation\")\n            return\n        }\n        #expect(operation.priority == .low)\n    }\n\n    @Test @ImagePipelineActor func changePriority() async {\n        // GIVEN\n        prefetcher.priority = .veryHigh\n\n        // WHEN\n        pipeline.configuration.dataLoadingQueue.isSuspended = true\n\n        let operations = await pipeline.configuration.dataLoadingQueue.waitForOperations(count: 1) {\n            prefetcher.startPrefetching(with: [Test.url])\n        }\n\n        // THEN\n        guard let operation = operations.first else {\n            Issue.record(\"Failed to find operation\")\n            return\n        }\n        #expect(operation.priority == .veryHigh)\n    }\n\n    @Test @ImagePipelineActor func changePriorityOfOutstandingTasks() async {\n        // WHEN\n        pipeline.configuration.dataLoadingQueue.isSuspended = true\n\n        let operations = await pipeline.configuration.dataLoadingQueue.waitForOperations(count: 1) {\n            prefetcher.startPrefetching(with: [Test.url])\n        }\n\n        guard let operation = operations.first else {\n            Issue.record(\"Failed to find operation\")\n            return\n        }\n\n        // WHEN/THEN\n        #expect(operation.priority == .low)\n\n        await pipeline.configuration.dataLoadingQueue.waitForPriorityChange(of: operation, to: .veryLow) {\n            prefetcher.priority = .veryLow\n        }\n        #expect(operation.priority == .veryLow)\n    }\n\n    // MARK: DidComplete\n\n    @Test func didCompleteIsCalled() async {\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            prefetcher.didComplete = { @MainActor @Sendable in\n                continuation.resume()\n            }\n            prefetcher.startPrefetching(with: [Test.url])\n        }\n    }\n\n    @Test func didCompleteIsCalledWhenImageCached() async {\n        imageCache[Test.request] = Test.container\n\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            prefetcher.didComplete = { @MainActor @Sendable in\n                continuation.resume()\n            }\n            prefetcher.startPrefetching(with: [Test.request])\n        }\n    }\n\n    // MARK: Empty Inputs\n\n    @Test func startPrefetchingWithEmptyURLArray() {\n        // WHEN - prefetching is started with an empty URL list\n        prefetcher.startPrefetching(with: [URL]())\n\n        // THEN - no tasks are created and nothing crashes\n        #expect(observer.startedTaskCount == 0)\n    }\n\n    @Test func startPrefetchingWithEmptyRequestArray() {\n        // WHEN - prefetching is started with an empty request list\n        prefetcher.startPrefetching(with: [ImageRequest]())\n\n        // THEN - no tasks are created and nothing crashes\n        #expect(observer.startedTaskCount == 0)\n    }\n\n    @Test func stopPrefetchingWithEmptyArrayIsNoOp() {\n        // GIVEN - nothing is currently being prefetched\n        // WHEN - stopping with an empty list\n        prefetcher.stopPrefetching(with: [URL]())\n\n        // THEN - no crash, no state change\n        #expect(observer.startedTaskCount == 0)\n    }\n\n    // MARK: Misc\n\n    @ImagePipelineActor\n    @Test func allPrefetchingRequestsAreStoppedWhenPrefetcherIsDeallocated() async {\n        pipeline.configuration.dataLoadingQueue.isSuspended = true\n\n        var localPrefetcher: ImagePrefetcher? = ImagePrefetcher(pipeline: pipeline)\n        let request = Test.request\n\n        // Wait for start notification\n        await notification(ImagePipelineObserver.didStartTask, object: observer) {\n            localPrefetcher?.startPrefetching(with: [request])\n        }\n\n        // Wait for cancel notification when prefetcher is deallocated\n        await notification(ImagePipelineObserver.didCancelTask, object: observer) {\n            autoreleasepool {\n                localPrefetcher = nil\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessingOptionsTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessingOptionsTests {\n\n    // MARK: - Unit\n\n    @Test func unitPointsDescription() {\n        let unit = ImageProcessingOptions.Unit.points\n        #expect(unit.description == \"points\")\n    }\n\n    @Test func unitPixelsDescription() {\n        let unit = ImageProcessingOptions.Unit.pixels\n        #expect(unit.description == \"pixels\")\n    }\n\n    // MARK: - ContentMode\n\n    @Test func contentModeAspectFillDescription() {\n        let mode = ImageProcessingOptions.ContentMode.aspectFill\n        #expect(mode.description == \".aspectFill\")\n    }\n\n    @Test func contentModeAspectFitDescription() {\n        let mode = ImageProcessingOptions.ContentMode.aspectFit\n        #expect(mode.description == \".aspectFit\")\n    }\n\n    // MARK: - Border\n\n    @Test func borderDescription() {\n#if canImport(UIKit)\n        let border = ImageProcessingOptions.Border(color: .red, width: 2, unit: .pixels)\n#else\n        let border = ImageProcessingOptions.Border(color: .red, width: 2, unit: .pixels)\n#endif\n        #expect(border.description.contains(\"Border\"))\n        #expect(border.description.contains(\"pixels\"))\n    }\n\n    @Test func borderDefaultWidth() {\n#if canImport(UIKit)\n        let border = ImageProcessingOptions.Border(color: .blue)\n#else\n        let border = ImageProcessingOptions.Border(color: .blue)\n#endif\n        #expect(border.width > 0)\n    }\n\n    @Test func bordersWithSameColorAndWidthAreEqual() {\n        let a = ImageProcessingOptions.Border(color: .red, width: 2, unit: .pixels)\n        let b = ImageProcessingOptions.Border(color: .red, width: 2, unit: .pixels)\n        #expect(a == b)\n        #expect(a.hashValue == b.hashValue)\n    }\n\n    @Test func bordersWithDifferentColorsAreNotEqual() {\n        let a = ImageProcessingOptions.Border(color: .red, width: 2, unit: .pixels)\n        let b = ImageProcessingOptions.Border(color: .blue, width: 2, unit: .pixels)\n        #expect(a != b)\n    }\n\n    @Test func bordersWithDifferentWidthsAreNotEqual() {\n        let a = ImageProcessingOptions.Border(color: .red, width: 2, unit: .pixels)\n        let b = ImageProcessingOptions.Border(color: .red, width: 4, unit: .pixels)\n        #expect(a != b)\n    }\n\n    // MARK: - ContentMode\n\n    @Test func contentModeEquality() {\n        #expect(ImageProcessingOptions.ContentMode.aspectFill == .aspectFill)\n        #expect(ImageProcessingOptions.ContentMode.aspectFit == .aspectFit)\n        #expect(ImageProcessingOptions.ContentMode.aspectFill != .aspectFit)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/AnonymousTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n#if !os(macOS)\n    import UIKit\n#endif\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessorsAnonymousTests {\n\n    @Test func anonymousProcessorsHaveDifferentIdentifiers() {\n        #expect(\n            ImageProcessors.Anonymous(id: \"1\", { $0 }).identifier ==\n            ImageProcessors.Anonymous(id: \"1\", { $0 }).identifier\n        )\n        #expect(\n            ImageProcessors.Anonymous(id: \"1\", { $0 }).identifier !=\n            ImageProcessors.Anonymous(id: \"2\", { $0 }).identifier\n        )\n    }\n\n    @Test func anonymousProcessorsHaveDifferentHashableIdentifiers() {\n        #expect(\n            ImageProcessors.Anonymous(id: \"1\", { $0 }).hashableIdentifier ==\n            ImageProcessors.Anonymous(id: \"1\", { $0 }).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Anonymous(id: \"1\", { $0 }).hashableIdentifier !=\n            ImageProcessors.Anonymous(id: \"2\", { $0 }).hashableIdentifier\n        )\n    }\n\n    @Test func anonymousProcessorDescription() {\n        let processor = ImageProcessors.Anonymous(id: \"my-processor\", { $0 })\n        #expect(processor.description.contains(\"my-processor\"))\n    }\n\n    @Test func anonymousProcessorReturnsNil() {\n        let processor = ImageProcessors.Anonymous(id: \"nil-processor\") { _ in nil }\n        let result = processor.process(Test.image)\n        #expect(result == nil)\n    }\n\n    @Test func anonymousProcessorIsApplied() throws {\n        // Given\n        let processor = ImageProcessors.Anonymous(id: \"1\") {\n            $0.nk_test_processorIDs = [\"1\"]\n            return $0\n        }\n\n        // When\n        let image = try #require(processor.process(Test.image))\n\n        // Then\n        #expect(image.nk_test_processorIDs == [\"1\"])\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/CircleTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n#if !os(macOS)\n    import UIKit\n#endif\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessorsCircleTests {\n\n    @Test(.disabled()) func thatImageIsCroppedToSquareAutomatically() throws {\n        // Given\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let processor = ImageProcessors.Circle()\n\n        // When\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 150, height: 150))\n        #expect(isEqualImages(output, Test.image(named: \"s-circle.png\")))\n    }\n\n    @Test(.disabled()) func thatBorderIsAdded() throws {\n        // Given\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let border = ImageProcessingOptions.Border(color: .red, width: 4, unit: .pixels)\n        let processor = ImageProcessors.Circle(border: border)\n\n        // When\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 150, height: 150))\n        #expect(isEqualImages(output, Test.image(named: \"s-circle-border.png\")))\n    }\n\n    @Test func extendedColorSpaceSupport() throws {\n        // Given\n        let input = Test.image(named: \"image-p3\", extension: \"jpg\")\n        let border = ImageProcessingOptions.Border(color: .red, width: 4, unit: .pixels)\n        let processor = ImageProcessors.Circle(border: border)\n\n        // When\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then image is resized but isn't cropped\n        let colorSpace = try #require(output.cgImage?.colorSpace)\n        #expect(colorSpace.isWideGamutRGB)\n    }\n\n    @Test @MainActor func identifierEqual() throws {\n        #expect(\n            ImageProcessors.Circle().identifier ==\n            ImageProcessors.Circle().identifier\n        )\n        #expect(\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).identifier ==\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).identifier\n        )\n        #expect(\n            ImageProcessors.Circle(border: .init(color: .red, width: 4, unit: .pixels)).identifier ==\n            ImageProcessors.Circle(border: .init(color: .red, width: 4 / Screen.scale, unit: .points)).identifier\n        )\n    }\n\n    @Test func identifierNotEqual() throws {\n        #expect(\n            ImageProcessors.Circle().identifier !=\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).identifier\n        )\n        #expect(\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).identifier !=\n            ImageProcessors.Circle(border: .init(color: .red, width: 4, unit: .pixels)).identifier\n        )\n        #expect(\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).identifier !=\n            ImageProcessors.Circle(border: .init(color: .blue, width: 2, unit: .pixels)).identifier\n        )\n    }\n\n    @Test @MainActor func hashableIdentifierEqual() throws {\n        #expect(\n            ImageProcessors.Circle().hashableIdentifier ==\n            ImageProcessors.Circle().hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).hashableIdentifier ==\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Circle(border: .init(color: .red, width: 4, unit: .pixels)).hashableIdentifier ==\n            ImageProcessors.Circle(border: .init(color: .red, width: 4 / Screen.scale, unit: .points)).hashableIdentifier\n        )\n    }\n\n    @Test func hashableNotEqual() throws {\n        #expect(\n            AnyHashable(ImageProcessors.Circle().identifier) !=\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).hashableIdentifier !=\n            ImageProcessors.Circle(border: .init(color: .red, width: 4, unit: .pixels)).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Circle(border: .init(color: .red, width: 2, unit: .pixels)).hashableIdentifier !=\n            ImageProcessors.Circle(border: .init(color: .blue, width: 2, unit: .pixels)).hashableIdentifier\n        )\n    }\n\n    @Test func description() {\n        // Given\n        let processor = ImageProcessors.Circle(border: .init(color: .blue, width: 2, unit: .pixels))\n\n        // Then\n        #expect(processor.description == \"Circle(border: Border(color: #0000FF, width: 2.0 pixels))\")\n    }\n\n    @Test func descriptionWithoutBorder() {\n        // Given\n        let processor = ImageProcessors.Circle()\n\n        // Then\n        #expect(processor.description == \"Circle(border: nil)\")\n    }\n\n    @Test func colorToHex() {\n        // Given\n        let color = UIColor.red\n\n        // Then\n        #expect(color.hex == \"#FF0000\")\n    }\n}\n#endif\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/CompositionTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n#if !os(macOS)\n    import UIKit\n#endif\n\n// MARK: - ImageProcessors.Composition\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessorsCompositionTests {\n\n    @Test func appliesAllProcessors() throws {\n        // GIVEN\n        let processor = ImageProcessors.Composition([\n            MockImageProcessor(id: \"1\"),\n            MockImageProcessor(id: \"2\")]\n        )\n\n        // WHEN\n        let image = try #require(processor.process(Test.image))\n\n        // THEN\n        #expect(image.nk_test_processorIDs == [\"1\", \"2\"])\n    }\n\n    @Test func appliesAllProcessorsWithContext() throws {\n        // GIVEN\n        let processor = ImageProcessors.Composition([\n            MockImageProcessor(id: \"1\"),\n            MockImageProcessor(id: \"2\")]\n        )\n\n        // WHEN\n        let context = ImageProcessingContext(request: Test.request, response: ImageResponse(container: Test.container, request: Test.request), isCompleted: true)\n        let output = try processor.process(Test.container, context: context)\n\n        // THEN\n        #expect(output.image.nk_test_processorIDs == [\"1\", \"2\"])\n    }\n\n    @Test func identifiers() {\n        // GIVEN different processors\n        let lhs = ImageProcessors.Composition([MockImageProcessor(id: \"1\")])\n        let rhs = ImageProcessors.Composition([MockImageProcessor(id: \"2\")])\n\n        // THEN\n        #expect(lhs != rhs)\n        #expect(lhs.identifier != rhs.identifier)\n        #expect(lhs.hashableIdentifier != rhs.hashableIdentifier)\n    }\n\n    @Test func identifiersDifferentProcessorCount() {\n        // GIVEN processors with different processor count\n        let lhs = ImageProcessors.Composition([MockImageProcessor(id: \"1\")])\n        let rhs = ImageProcessors.Composition([MockImageProcessor(id: \"1\"), MockImageProcessor(id: \"2\")])\n\n        // THEN\n        #expect(lhs != rhs)\n        #expect(lhs.identifier != rhs.identifier)\n        #expect(lhs.hashableIdentifier != rhs.hashableIdentifier)\n    }\n\n    @Test func identifiersEqualProcessors() {\n        // GIVEN processors with equal processors\n        let lhs = ImageProcessors.Composition([MockImageProcessor(id: \"1\"), MockImageProcessor(id: \"2\")])\n        let rhs = ImageProcessors.Composition([MockImageProcessor(id: \"1\"), MockImageProcessor(id: \"2\")])\n\n        // THEN\n        #expect(lhs == rhs)\n        #expect(lhs.hashValue == rhs.hashValue)\n        #expect(lhs.identifier == rhs.identifier)\n        #expect(lhs.hashableIdentifier == rhs.hashableIdentifier)\n    }\n\n    @Test func identifiersWithSameProcessorsButInDifferentOrder() {\n        // GIVEN processors with equal processors but in different order\n        let lhs = ImageProcessors.Composition([MockImageProcessor(id: \"2\"), MockImageProcessor(id: \"1\")])\n        let rhs = ImageProcessors.Composition([MockImageProcessor(id: \"1\"), MockImageProcessor(id: \"2\")])\n\n        // THEN\n        #expect(lhs != rhs)\n        #expect(lhs.identifier != rhs.identifier)\n        #expect(lhs.hashableIdentifier != rhs.hashableIdentifier)\n    }\n\n    @Test func identifiersEmptyProcessors() {\n        // GIVEN empty processors\n        let lhs = ImageProcessors.Composition([])\n        let rhs = ImageProcessors.Composition([])\n\n        // THEN\n        #expect(lhs == rhs)\n        #expect(lhs.hashValue == rhs.hashValue)\n        #expect(lhs.identifier == rhs.identifier)\n        #expect(lhs.hashableIdentifier == rhs.hashableIdentifier)\n    }\n\n    @Test func thatIdentifiesAreFlattened() {\n        let lhs = ImageProcessors.Composition([\n            ImageProcessors.Composition([MockImageProcessor(id: \"1\"), MockImageProcessor(id: \"2\")]),\n            ImageProcessors.Composition([MockImageProcessor(id: \"3\"), MockImageProcessor(id: \"4\")])]\n        )\n        let rhs = ImageProcessors.Composition([\n            MockImageProcessor(id: \"1\"), MockImageProcessor(id: \"2\"),\n            MockImageProcessor(id: \"3\"), MockImageProcessor(id: \"4\")]\n        )\n\n        // THEN\n        #expect(lhs.identifier == rhs.identifier)\n    }\n\n    @Test func description() {\n        // GIVEN\n        let processor = ImageProcessors.Composition([\n            ImageProcessors.Circle()\n        ])\n\n        // THEN\n        #expect(\"\\(processor)\" == \"Composition(processors: [Circle(border: nil)])\")\n    }\n\n    // MARK: Edge Cases\n\n    @Test func singleProcessorInCompositionIsApplied() throws {\n        // GIVEN - composition wrapping a single processor\n        let processor = ImageProcessors.Composition([MockImageProcessor(id: \"solo\")])\n\n        // WHEN\n        let image = try #require(processor.process(Test.image))\n\n        // THEN - the sole processor is still applied\n        #expect(image.nk_test_processorIDs == [\"solo\"])\n    }\n\n    @Test func emptyCompositionPassesThroughImage() throws {\n        // GIVEN - composition with no processors\n        let processor = ImageProcessors.Composition([])\n\n        // WHEN\n        let image = try #require(processor.process(Test.image))\n\n        // THEN - original image passes through with no processor IDs\n        #expect(image.nk_test_processorIDs == [])\n    }\n\n    @Test func whenOneProcessorReturnsNilCompositionReturnsNil() {\n        // GIVEN - a composition where the second step fails\n        let processor = ImageProcessors.Composition([\n            MockImageProcessor(id: \"1\"),\n            MockFailingProcessor()\n        ])\n\n        // WHEN/THEN - the entire composition yields nil\n        #expect(processor.process(Test.image) == nil)\n    }\n\n    @Test func remainingProcessorsSkippedAfterFailure() {\n        // GIVEN - a composition where the first step fails\n        let processor = ImageProcessors.Composition([\n            MockFailingProcessor(),\n            MockImageProcessor(id: \"shouldNotRun\")\n        ])\n\n        // WHEN/THEN - composition short-circuits at the first failure\n        #expect(processor.process(Test.image) == nil)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/CoreImageFilterTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n#if !os(macOS)\nimport UIKit\n#else\nimport CoreImage\n#endif\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessorsCoreImageFilterTests {\n    @Test func applySepia() throws {\n        // GIVEN\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let processor = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\")\n\n        // WHEN\n        let output = try #require(processor.process(input))\n\n        // THEN\n        _ = output // image was produced successfully\n    }\n\n    @Test func applySepiaWithParameters() throws {\n        // GIVEN\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let processor = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\", parameters: [\"inputIntensity\": 0.5], identifier: \"CISepiaTone-75\")\n\n        // WHEN\n        let output = try #require(processor.process(input))\n\n        // THEN\n        _ = output // image was produced successfully\n    }\n\n    @Test func applyFilterWithInvalidName() throws {\n        // GIVEN\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let processor = ImageProcessors.CoreImageFilter(name: \"yo\", parameters: [\"inputIntensity\": 0.5], identifier: \"CISepiaTone-75\")\n\n        // THEN\n        #expect(throws: ImageProcessors.CoreImageFilter.Error.self) {\n            try processor.processThrowing(input)\n        }\n    }\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n    @Test func applyFilterToCIImage() throws {\n        // GIVEN image backed by CIImage\n        let input = PlatformImage(ciImage: CIImage(cgImage: Test.image.cgImage!))\n        let processor = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\", parameters: [\"inputIntensity\": 0.5], identifier: \"CISepiaTone-75\")\n\n        // WHEN\n        let output = try #require(processor.process(input))\n\n        // THEN\n        _ = output // image was produced successfully\n    }\n#endif\n\n    @Test func applyFilterBackedByNothing() throws {\n        // GIVEN empty image\n        let input = PlatformImage()\n        let processor = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\", parameters: [\"inputIntensity\": 0.5], identifier: \"CISepiaTone-75\")\n\n        // THEN\n        #expect(throws: ImageProcessors.CoreImageFilter.Error.self) {\n            try processor.processThrowing(input)\n        }\n    }\n\n    @Test func description() {\n        // GIVEN\n        let processor = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\", parameters: [\"inputIntensity\": 0.5], identifier: \"CISepiaTone-75\")\n\n        // THEN\n        #expect(\"\\(processor)\" == \"CoreImageFilter(name: CISepiaTone, parameters: [\\\"inputIntensity\\\": 0.5])\")\n    }\n\n    @Test func applyCustomFilter() throws {\n        // GIVEN\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let filter = try #require(CIFilter(name: \"CISepiaTone\", parameters: nil))\n        let processor = ImageProcessors.CoreImageFilter(filter, identifier: \"test\")\n\n        // WHEN\n        let output = try #require(processor.process(input))\n\n        // THEN\n        _ = output // image was produced successfully\n    }\n\n    // MARK: - Composition\n\n    @Test func compositionOfTwoCIFiltersProducesOutput() throws {\n        // GIVEN two CoreImage filters composed in sequence\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let filter1 = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\")\n        let filter2 = ImageProcessors.CoreImageFilter(name: \"CIColorInvert\")\n        let composition = ImageProcessors.Composition([filter1, filter2])\n\n        // WHEN\n        let output = try #require(composition.process(input))\n\n        // THEN a valid image is produced\n        _ = output\n    }\n\n    // MARK: - Identifiers\n\n    @Test func identifiersAreDistinctForDifferentFilterNames() {\n        // GIVEN two filters with different names (using the name-only initializer)\n        let sepia = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\")\n        let bloom = ImageProcessors.CoreImageFilter(name: \"CIBloom\")\n\n        // THEN their identifiers differ\n        #expect(sepia.identifier != bloom.identifier)\n    }\n\n    @Test func identifiersAreEqualForSameFilterAndParameters() {\n        // GIVEN two identically-configured filters\n        let a = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\", parameters: [\"inputIntensity\": 0.8], identifier: \"sepia-80\")\n        let b = ImageProcessors.CoreImageFilter(name: \"CISepiaTone\", parameters: [\"inputIntensity\": 0.8], identifier: \"sepia-80\")\n\n        // THEN their identifiers are equal\n        #expect(a.identifier == b.identifier)\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/DecompressionTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageDecompressionTests {\n\n    @Test func decompressionNotNeededFlagSet() throws {\n        // Given\n        let input = Test.image\n        ImageDecompression.setDecompressionNeeded(true, for: input)\n\n        // When\n        let output = ImageDecompression.decompress(image: input)\n\n        // Then\n        #expect(ImageDecompression.isDecompressionNeeded(for: output) != true)\n    }\n\n    @Test func grayscalePreserved() throws {\n        // Given\n        let input = Test.image(named: \"grayscale\", extension: \"jpeg\")\n        #expect(input.cgImage?.bitsPerComponent == 8)\n        #expect(input.cgImage?.bitsPerPixel == 8)\n\n        // When\n        let output = ImageDecompression.decompress(image: input, isUsingPrepareForDisplay: true)\n\n        // Then\n        #expect(output.cgImage?.bitsPerPixel == 8)\n        #expect(output.cgImage?.bitsPerComponent == 8)\n    }\n\n    @Test func isDecompressionNeededReturnsFalseForUntaggedImage() {\n        // GIVEN a freshly created image with no decompression tag\n        let image = Test.image\n\n        // THEN flag is unset (nil), treated as not needing decompression\n        #expect(ImageDecompression.isDecompressionNeeded(for: image) != true)\n    }\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n    @Test func wideGamutColorSpaceIsPreservedAfterDecompression() throws {\n        // GIVEN a wide-gamut (P3) image\n        let input = Test.image(named: \"image-p3\", extension: \"jpg\")\n        let inputColorSpace = try #require(input.cgImage?.colorSpace)\n        #expect(inputColorSpace.isWideGamutRGB)\n\n        // WHEN decompressed\n        let output = ImageDecompression.decompress(image: input)\n\n        // THEN the wide-gamut color space is preserved\n        let outputColorSpace = try #require(output.cgImage?.colorSpace)\n        #expect(outputColorSpace.isWideGamutRGB)\n    }\n#endif\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/GaussianBlurTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n#if !os(macOS)\n    import UIKit\n#endif\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessorsGaussianBlurTests {\n    @Test func applyBlur() {\n        // Given\n        let image = Test.image\n        let processor = ImageProcessors.GaussianBlur()\n        #expect(!processor.description.isEmpty)\n\n        // When\n        #expect(processor.process(image) != nil)\n    }\n\n    @Test func applyBlurProducesImagesBackedByCoreGraphics() {\n        // Given\n        let image = Test.image\n        let processor = ImageProcessors.GaussianBlur()\n\n        // When\n        #expect(processor.process(image) != nil)\n    }\n\n    @Test func applyBlurProducesTransparentImages() throws {\n        // Given\n        let image = Test.image\n        let processor = ImageProcessors.GaussianBlur()\n\n        // When\n        let processed = try #require(processor.process(image))\n\n        // Then\n        #expect(processed.cgImage?.isOpaque == false)\n    }\n\n    @Test func imagesWithSameRadiusHasSameIdentifiers() {\n        #expect(\n            ImageProcessors.GaussianBlur(radius: 2).identifier ==\n            ImageProcessors.GaussianBlur(radius: 2).identifier\n        )\n    }\n\n    @Test func imagesWithDifferentRadiusHasDifferentIdentifiers() {\n        #expect(\n            ImageProcessors.GaussianBlur(radius: 2).identifier !=\n            ImageProcessors.GaussianBlur(radius: 3).identifier\n        )\n    }\n\n    @Test func imagesWithSameRadiusHasSameHashableIdentifiers() {\n        #expect(\n            ImageProcessors.GaussianBlur(radius: 2).hashableIdentifier ==\n            ImageProcessors.GaussianBlur(radius: 2).hashableIdentifier\n        )\n    }\n\n    @Test func imagesWithDifferentRadiusHasDifferentHashableIdentifiers() {\n        #expect(\n            ImageProcessors.GaussianBlur(radius: 2).hashableIdentifier !=\n            ImageProcessors.GaussianBlur(radius: 3).hashableIdentifier\n        )\n    }\n\n    // MARK: - Output Dimensions\n\n    @Test func blurDoesNotChangeImageDimensions() throws {\n        // GIVEN\n        let image = Test.image\n        let inputSize = image.sizeInPixels\n        let processor = ImageProcessors.GaussianBlur(radius: 8)\n\n        // WHEN\n        let output = try #require(processor.process(image))\n\n        // THEN - blurring must not alter the canvas size\n        #expect(output.sizeInPixels == inputSize)\n    }\n\n    @Test func blurWithMinimumRadiusProducesOutput() throws {\n        // GIVEN - radius of 1 is the smallest non-trivial blur\n        let processor = ImageProcessors.GaussianBlur(radius: 1)\n\n        // WHEN / THEN - must not crash and must return a valid image\n        let output = try #require(processor.process(Test.image))\n        #expect(output.sizeInPixels == Test.image.sizeInPixels)\n    }\n\n    @Test func differentRadiiProduceDifferentDescriptions() {\n        #expect(\n            ImageProcessors.GaussianBlur(radius: 4).description !=\n            ImageProcessors.GaussianBlur(radius: 16).description\n        )\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/ImageDownsampleTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n#if !os(macOS)\n    import UIKit\n#endif\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageThumbnailTests {\n\n    @Test func thatImageIsResized() throws {\n        // WHEN\n        let options = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        let output = try #require(options.makeThumbnail(with: Test.data))\n\n        // THEN\n        #expect(output.sizeInPixels == CGSize(width: 400, height: 300))\n    }\n\n    @Test func thatImageIsResizedToFill() throws {\n        // Given\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFill)\n\n        // When\n        let output = try #require(options.makeThumbnail(with: Test.data))\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 533, height: 400))\n    }\n\n    @Test func thatImageIsResizedToFillPNG() throws {\n        // Given\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 180, height: 180), unit: .pixels, contentMode: .aspectFill)\n\n        // When\n        // Input: 640 x 360\n        let output = try #require(makeThumbnail(data: Test.data(name: \"fixture\", extension: \"png\"), options: options))\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 320, height: 180))\n    }\n\n    @Test func thatImageIsResizedToFit() throws {\n        // Given\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFit)\n\n        // When\n        let output = try #require(options.makeThumbnail(with: Test.data))\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 400, height: 300))\n    }\n\n    @Test func thatImageIsResizedToFitPNG() throws {\n        // Given\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 160, height: 160), unit: .pixels, contentMode: .aspectFit)\n\n        // When\n        // Input: 640 x 360\n        let output = try #require(options.makeThumbnail(with: Test.data(name: \"fixture\", extension: \"png\")))\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 160, height: 90))\n    }\n\n#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)\n    @Test func resizeImageWithOrientationRight() throws {\n        // Given an image with `right` orientation. From the user perspective,\n        // the image a landscape image with s size 640x480px. The raw pixel\n        // data, on the other hand, is 480x640px.\n        let input = Test.data(name: \"right-orientation\", extension: \"jpeg\")\n        #expect(PlatformImage(data: input)?.imageOrientation == .right)\n\n        // When we resize the image to fit 320x480px frame, we expect the processor\n        // to take image orientation into the account and produce a 320x240px.\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 320, height: 1000), unit: .pixels, contentMode: .aspectFit)\n        let output = try #require(options.makeThumbnail(with: input))\n\n        // Then the output has orientation of the original image\n        #expect(output.imageOrientation == .right)\n\n        //verify size of the image in points and pixels (using scale)\n        #expect(output.sizeInPixels == CGSize(width: 320, height: 240))\n    }\n\n    @Test func resizeImageWithOrientationUp() throws {\n        let input = Test.data(name: \"baseline\", extension: \"jpeg\")\n        #expect(PlatformImage(data: input)?.imageOrientation == .up)\n\n        let options = ImageRequest.ThumbnailOptions(maxPixelSize: 300)\n        let output = try #require(options.makeThumbnail(with: input))\n\n        // Then the output has orientation of the original image\n        #expect(output.imageOrientation == .up)\n\n        //verify size of the image in points and pixels (using scale)\n        #expect(output.sizeInPixels == CGSize(width: 300, height: 200))\n    }\n#endif\n\n    // MARK: No-op / small-image edge cases\n\n    @Test func thatImageSmallerThanMaxPixelSizeIsNotUpscaled() throws {\n        // GIVEN - test image is 640×480; request a thumbnail larger than that\n        let options = ImageRequest.ThumbnailOptions(maxPixelSize: 2000)\n\n        // WHEN\n        let output = try #require(options.makeThumbnail(with: Test.data))\n\n        // THEN - the image is returned at its native size, not upscaled\n        let size = output.sizeInPixels\n        #expect(size.width <= 640)\n        #expect(size.height <= 480)\n    }\n\n    @Test func thatInvalidDataReturnsNil() {\n        // GIVEN - completely random bytes that don't form a valid image\n        let data = Data(repeating: 0xAB, count: 256)\n        let options = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n\n        // WHEN / THEN\n        #expect(options.makeThumbnail(with: data) == nil)\n    }\n\n    @Test func thatEmptyDataReturnsNil() {\n        let options = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        #expect(options.makeThumbnail(with: Data()) == nil)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/ImageProcessorsProtocolExtensionsTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\nimport Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessorsProtocolExtensionsTests {\n\n    @Test func passingProcessorsUsingProtocolExtensionsResize() throws {\n        let size = CGSize(width: 100, height: 100)\n        let processor = ImageProcessors.Resize(size: size)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.resize(size: size)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsResizeWidthOnly() throws {\n        let processor = ImageProcessors.Resize(width: 100)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.resize(width: 100)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsResizeHeightOnly() throws {\n        let processor = ImageProcessors.Resize(height: 100)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.resize(height: 100)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsCircleEmpty() throws {\n        let processor = ImageProcessors.Circle()\n\n        let request = try #require(ImageRequest(url: nil, processors: [.circle()]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsCircle() throws {\n        let border = ImageProcessingOptions.Border.init(color: .red)\n        let processor = ImageProcessors.Circle(border: border)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.circle(border: border)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsRoundedCorners() throws {\n        let radius: CGFloat = 10\n        let processor = ImageProcessors.RoundedCorners(radius: radius)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.roundedCorners(radius: radius)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsAnonymous() throws {\n        let id = UUID().uuidString\n        let closure: (@Sendable (PlatformImage) -> PlatformImage?) = { _ in nil }\n        let processor = ImageProcessors.Anonymous(id: id, closure)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.process(id: id, closure)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n    @Test func passingProcessorsUsingProtocolExtensionsCoreImageFilterWithNameOnly() throws {\n        let name = \"CISepiaTone\"\n        let processor = ImageProcessors.CoreImageFilter(name: name)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.coreImageFilter(name: name)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsCoreImageFilter() throws {\n        let name = \"CISepiaTone\"\n        let id = UUID().uuidString\n        let processor = ImageProcessors.CoreImageFilter(name: name, parameters: [:], identifier: id)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.coreImageFilter(name: name, parameters: [:], identifier: id)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsGaussianBlurEmpty() throws {\n        let processor = ImageProcessors.GaussianBlur()\n\n        let request = try #require(ImageRequest(url: nil, processors: [.gaussianBlur()]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n\n    @Test func passingProcessorsUsingProtocolExtensionsGaussianBlur() throws {\n        let radius = 10\n        let processor = ImageProcessors.GaussianBlur(radius: radius)\n\n        let request = try #require(ImageRequest(url: nil, processors: [.gaussianBlur(radius: radius)]))\n\n        #expect(request.processors.first?.identifier == processor.identifier)\n    }\n#endif\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/ResizeTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n#if !os(macOS)\nimport UIKit\n#endif\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessorsResizeTests {\n\n    @Test func thatImageIsResizedToFill() throws {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFill)\n\n        // When\n        let output = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 533, height: 400))\n    }\n\n    @Test func thatImageIsntUpscaledByDefault() throws {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 960, height: 960), unit: .pixels, contentMode: .aspectFill)\n\n        // When\n        let output = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 640, height: 480))\n    }\n\n    @Test func resizeToFitHeight() throws {\n        // Given\n        let processor = ImageProcessors.Resize(height: 300, unit: .pixels)\n\n        // When\n        let output = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 400, height: 300))\n    }\n\n    @Test func resizeToFitWidth() throws {\n        // Given\n        let processor = ImageProcessors.Resize(width: 400, unit: .pixels)\n\n        // When\n        let output = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 400, height: 300))\n    }\n\n    @Test func thatImageIsUpscaledIfOptionIsEnabled() throws {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 960, height: 960), unit: .pixels, contentMode: .aspectFill, upscale: true)\n\n        // When\n        let output = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 1280, height: 960))\n    }\n\n    @Test func thatContentModeCanBeChangeToAspectFit() throws {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 480, height: 480), unit: .pixels, contentMode: .aspectFit)\n\n        // When\n        let output = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 480, height: 360))\n    }\n\n    @Test func thatImageIsCropped() throws {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 400, height: 400), unit: .pixels, crop: true)\n\n        // When\n        let output = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 400, height: 400))\n    }\n\n    @Test func thatImageIsntCroppedWithAspectFitMode() throws {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 480, height: 480), unit: .pixels, contentMode: .aspectFit, crop: true)\n\n        // When\n        let output = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then image is resized but isn't cropped\n        #expect(output.sizeInPixels == CGSize(width: 480, height: 360))\n    }\n\n    @Test func extendedColorSpaceSupport() throws {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 480, height: 480), unit: .pixels, contentMode: .aspectFit, crop: true)\n\n        // When\n        let output = try #require(processor.process(Test.image(named: \"image-p3\", extension: \"jpg\")), \"Failed to process an image\")\n\n        // Then image is resized but isn't cropped\n        #expect(output.sizeInPixels == CGSize(width: 480, height: 320))\n        let colorSpace = try #require(output.cgImage?.colorSpace)\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n        #expect(colorSpace.isWideGamutRGB)\n#elseif os(watchOS)\n        #expect(!colorSpace.isWideGamutRGB)\n#endif\n    }\n\n#if os(macOS)\n    @Test @MainActor func resizeImageWithOrientationLeft() throws {\n        // Given an image with `left` orientation. From the user perspective,\n        // the image a landscape image with s size 640x480px. The raw pixel\n        // data, on the other hand, is 480x640px. macOS, however, automatically\n        // changes image orientaiton to `up` so that you don't have to worry about it\n        let input = Test.image(named: \"right-orientation.jpeg\")\n\n        // When we resize the image to fit 320x480px frame, we expect the processor\n        // to take image orientation into the account and produce a 320x240px.\n        let processor = ImageProcessors.Resize(size: CGSize(width: 320, height: 1000), unit: .pixels, contentMode: .aspectFit)\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then the image orientation is still `.left`\n        #expect(output.sizeInPixels == CGSize(width: 320, height: 240))\n\n        // Then the image is resized according to orientation\n        #expect(output.size == CGSize(width: 320 / Screen.scale, height: 240 / Screen.scale))\n    }\n#endif\n\n#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)\n    @Test func resizeImageWithOrientationLeft() throws {\n        // Given an image with `right` orientation. From the user perspective,\n        // the image a landscape image with s size 640x480px. The raw pixel\n        // data, on the other hand, is 480x640px.\n        let input = Test.image(named: \"right-orientation.jpeg\")\n        #expect(input.imageOrientation == .right)\n\n        // When we resize the image to fit 320x480px frame, we expect the processor\n        // to take image orientation into the account and produce a 320x240px.\n        let processor = ImageProcessors.Resize(size: CGSize(width: 320, height: 1000), unit: .pixels, contentMode: .aspectFit)\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then the image orientation is still `.right`\n        #expect(output.sizeInPixels == CGSize(width: 240, height: 320))\n        #expect(output.imageOrientation == .right)\n        // Then the image is resized according to orientation\n        #expect(output.size == CGSize(width: 320, height: 240))\n    }\n\n    @Test func resizeAndCropWithOrientationLeft() throws {\n        // Given an image with `right` orientation. From the user perspective,\n        // the image a landscape image with s size 640x480px. The raw pixel\n        // data, on the other hand, is 480x640px.\n        let input = Test.image(named: \"right-orientation.jpeg\")\n        #expect(input.imageOrientation == .right)\n\n        // When\n        let processor = ImageProcessors.Resize(size: CGSize(width: 320, height: 80), unit: .pixels, contentMode: .aspectFill, crop: true)\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then\n        #expect(output.sizeInPixels == CGSize(width: 80, height: 320))\n        #expect(output.imageOrientation == .right)\n        // Then\n        #expect(output.size == CGSize(width: 320, height: 80))\n    }\n#endif\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n    @Test func thatScalePreserved() throws {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFill)\n\n        // When\n        let image = try #require(processor.process(Test.image), \"Failed to process an image\")\n\n        // Then\n        #expect(image.scale == Test.image.scale)\n    }\n#endif\n\n    @Test @MainActor func thatIdentifiersAreEqualWithSameParameters() {\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30)).identifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30)).identifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), unit: .pixels).identifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30 / Screen.scale, height: 30 / Screen.scale), unit: .points).identifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), crop: true).identifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), crop: true).identifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), upscale: true).identifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), upscale: true).identifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), contentMode: .aspectFit).identifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), contentMode: .aspectFit).identifier\n        )\n    }\n\n    @Test func thatIdentifiersAreNotEqualWithDifferentParameters() {\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30)).identifier !=\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 40)).identifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), crop: true).identifier !=\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), crop: false).identifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), upscale: true).identifier !=\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), upscale: false).identifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), contentMode: .aspectFit).identifier !=\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), contentMode: .aspectFill).identifier\n        )\n    }\n\n    @Test @MainActor func thatHashableIdentifiersAreEqualWithSameParameters() {\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30)).hashableIdentifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30)).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), unit: .pixels).hashableIdentifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30 / Screen.scale, height: 30 / Screen.scale), unit: .points).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), crop: true).hashableIdentifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), crop: true).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), upscale: true).hashableIdentifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), upscale: true).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), contentMode: .aspectFit).hashableIdentifier ==\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), contentMode: .aspectFit).hashableIdentifier\n        )\n    }\n\n    @Test func thatHashableIdentifiersAreNotEqualWithDifferentParameters() {\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30)).hashableIdentifier !=\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 40)).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), crop: true).hashableIdentifier !=\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), crop: false).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), upscale: true).hashableIdentifier !=\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), upscale: false).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), contentMode: .aspectFit).hashableIdentifier !=\n            ImageProcessors.Resize(size: CGSize(width: 30, height: 30), contentMode: .aspectFill).hashableIdentifier\n        )\n    }\n\n    @Test func description() {\n        // Given\n        let processor = ImageProcessors.Resize(size: CGSize(width: 30, height: 30), unit: .pixels, contentMode: .aspectFit)\n\n        // Then\n        #expect(processor.description == \"Resize(size: (30.0, 30.0) pixels, contentMode: .aspectFit, crop: false, upscale: false)\")\n    }\n\n    // MARK: - Identity / Edge Cases\n\n    @Test func thatAspectFitImageIsNotUpscaledByDefault() throws {\n        // GIVEN a processor with a target larger than the image — aspect fit, no upscale\n        let processor = ImageProcessors.Resize(size: CGSize(width: 960, height: 960), unit: .pixels, contentMode: .aspectFit)\n\n        // WHEN\n        let output = try #require(processor.process(Test.image))\n\n        // THEN the image is not enlarged beyond its original size\n        #expect(output.sizeInPixels == CGSize(width: 640, height: 480))\n    }\n\n    @Test func thatAspectFitCorrectlySelectsLimitingDimension() throws {\n        // GIVEN a processor where the width is the limiting dimension\n        // (target 100×200: 640/100=6.4x, 480/200=2.4x — height scale is smaller, so height is limiting)\n        let processor = ImageProcessors.Resize(size: CGSize(width: 200, height: 100), unit: .pixels, contentMode: .aspectFit)\n\n        // WHEN\n        let output = try #require(processor.process(Test.image))\n\n        // THEN fits within 200×100 while maintaining aspect ratio 4:3 → 100 high → 133 wide\n        // scale = 100/480 ≈ 0.208, width = 640 * 0.208 = ~133\n        let size = output.sizeInPixels\n        #expect(size.height == 100)\n        #expect(size.width <= 200)\n        #expect(size.width > 0)\n    }\n\n    // Just make sure these initializers are still available.\n    @Test func initializer() {\n        _ = ImageProcessors.Resize(height: 10)\n        _ = ImageProcessors.Resize(width: 10)\n        _ = ImageProcessors.Resize(width: 10, upscale: true)\n        _ = ImageProcessors.Resize(width: 10, unit: .pixels, upscale: true)\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct CoreGraphicsExtensionsTests {\n    @Test func scaleToFill() {\n        #expect(1 == CGSize(width: 10, height: 10).scaleToFill(CGSize(width: 10, height: 10)))\n        #expect(0.5 == CGSize(width: 20, height: 20).scaleToFill(CGSize(width: 10, height: 10)))\n        #expect(2 == CGSize(width: 5, height: 5).scaleToFill(CGSize(width: 10, height: 10)))\n\n        #expect(1 == CGSize(width: 20, height: 10).scaleToFill(CGSize(width: 10, height: 10)))\n        #expect(1 == CGSize(width: 10, height: 20).scaleToFill(CGSize(width: 10, height: 10)))\n        #expect(0.5 == CGSize(width: 30, height: 20).scaleToFill(CGSize(width: 10, height: 10)))\n        #expect(0.5 == CGSize(width: 20, height: 30).scaleToFill(CGSize(width: 10, height: 10)))\n\n        #expect(2 == CGSize(width: 5, height: 10).scaleToFill(CGSize(width: 10, height: 10)))\n        #expect(2 == CGSize(width: 10, height: 5).scaleToFill(CGSize(width: 10, height: 10)))\n        #expect(2 == CGSize(width: 5, height: 8).scaleToFill(CGSize(width: 10, height: 10)))\n        #expect(2 == CGSize(width: 8, height: 5).scaleToFill(CGSize(width: 10, height: 10)))\n\n        #expect(2 == CGSize(width: 30, height: 10).scaleToFill(CGSize(width: 10, height: 20)))\n        #expect(2 == CGSize(width: 10, height: 30).scaleToFill(CGSize(width: 20, height: 10)))\n    }\n\n    @Test func scaleToFit() {\n        #expect(1 == CGSize(width: 10, height: 10).scaleToFit(CGSize(width: 10, height: 10)))\n        #expect(0.5 == CGSize(width: 20, height: 20).scaleToFit(CGSize(width: 10, height: 10)))\n        #expect(2 == CGSize(width: 5, height: 5).scaleToFit(CGSize(width: 10, height: 10)))\n\n        #expect(0.5 == CGSize(width: 20, height: 10).scaleToFit(CGSize(width: 10, height: 10)))\n        #expect(0.5 == CGSize(width: 10, height: 20).scaleToFit(CGSize(width: 10, height: 10)))\n        #expect(0.25 == CGSize(width: 40, height: 20).scaleToFit(CGSize(width: 10, height: 10)))\n        #expect(0.25 == CGSize(width: 20, height: 40).scaleToFit(CGSize(width: 10, height: 10)))\n\n        #expect(1 == CGSize(width: 5, height: 10).scaleToFit(CGSize(width: 10, height: 10)))\n        #expect(1 == CGSize(width: 10, height: 5).scaleToFit(CGSize(width: 10, height: 10)))\n        #expect(2 == CGSize(width: 2, height: 5).scaleToFit(CGSize(width: 10, height: 10)))\n        #expect(2 == CGSize(width: 5, height: 2).scaleToFit(CGSize(width: 10, height: 10)))\n\n        #expect(0.25 == CGSize(width: 40, height: 10).scaleToFit(CGSize(width: 10, height: 20)))\n        #expect(0.25 == CGSize(width: 10, height: 40).scaleToFit(CGSize(width: 20, height: 10)))\n    }\n\n    @Test func centeredInRectWithSize() {\n        #expect(\n            CGSize(width: 10, height: 10).centeredInRectWithSize(CGSize(width: 10, height: 10)) ==\n            CGRect(x: 0, y: 0, width: 10, height: 10)\n        )\n        #expect(\n            CGSize(width: 20, height: 20).centeredInRectWithSize(CGSize(width: 10, height: 10)) ==\n            CGRect(x: -5, y: -5, width: 20, height: 20)\n        )\n        #expect(\n            CGSize(width: 20, height: 10).centeredInRectWithSize(CGSize(width: 10, height: 10)) ==\n            CGRect(x: -5, y: 0, width: 20, height: 10)\n        )\n        #expect(\n            CGSize(width: 10, height: 20).centeredInRectWithSize(CGSize(width: 10, height: 10)) ==\n            CGRect(x: 0, y: -5, width: 10, height: 20)\n        )\n        #expect(\n            CGSize(width: 10, height: 20).centeredInRectWithSize(CGSize(width: 10, height: 20)) ==\n            CGRect(x: 0, y: 0, width: 10, height: 20)\n        )\n        #expect(\n            CGSize(width: 10, height: 40).centeredInRectWithSize(CGSize(width: 10, height: 20)) ==\n            CGRect(x: 0, y: -10, width: 10, height: 40)\n        )\n    }\n}\n\nprivate extension CGSize {\n    func scaleToFill(_ targetSize: CGSize) -> CGFloat {\n        getScale(targetSize: targetSize, contentMode: .aspectFill)\n    }\n\n    func scaleToFit(_ targetSize: CGSize) -> CGFloat {\n        getScale(targetSize: targetSize, contentMode: .aspectFit)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageProcessorsTests/RoundedCornersTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n#if !os(macOS)\n    import UIKit\n#endif\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageProcessorsRoundedCornersTests {\n\n    @Test(.disabled()) func thatCornerRadiusIsAdded() throws {\n        // Given\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let processor = ImageProcessors.RoundedCorners(radius: 12, unit: .pixels)\n\n        // When\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then\n        let expected = Test.image(named: \"s-rounded-corners.png\")\n        #expect(isEqualImages(output, expected))\n        #expect(output.sizeInPixels == CGSize(width: 200, height: 150))\n    }\n\n    @Test(.disabled()) func thatBorderIsAdded() throws {\n        // Given\n        let input = Test.image(named: \"fixture-tiny.jpeg\")\n        let border = ImageProcessingOptions.Border(color: .red, width: 4, unit: .pixels)\n        let processor = ImageProcessors.RoundedCorners(radius: 12, unit: .pixels, border: border)\n\n        // When\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then\n        let expected = Test.image(named: \"s-rounded-corners-border.png\")\n        #expect(isEqualImages(output, expected))\n    }\n\n    @Test func extendedColorSpaceSupport() throws {\n        // Given\n        let input = Test.image(named: \"image-p3\", extension: \"jpg\")\n        let processor = ImageProcessors.RoundedCorners(radius: 12, unit: .pixels)\n\n        // When\n        let output = try #require(processor.process(input), \"Failed to process an image\")\n\n        // Then image is resized but isn't cropped\n        let colorSpace = try #require(output.cgImage?.colorSpace)\n#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)\n        #expect(colorSpace.isWideGamutRGB)\n#elseif os(watchOS)\n        #expect(!colorSpace.isWideGamutRGB)\n#endif\n    }\n\n    @Test @MainActor func equalIdentifiers() {\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16).identifier ==\n            ImageProcessors.RoundedCorners(radius: 16).identifier\n        )\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels).identifier ==\n            ImageProcessors.RoundedCorners(radius: 16 / Screen.scale, unit: .points).identifier\n        )\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).identifier ==\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).identifier\n        )\n    }\n\n    @Test @MainActor func notEqualIdentifiers() {\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16).identifier !=\n            ImageProcessors.RoundedCorners(radius: 8).identifier\n        )\n        if Screen.scale == 1 {\n            #expect(\n                ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).identifier ==\n                ImageProcessors.RoundedCorners(radius: 16, unit: .points, border: .init(color: .red)).identifier\n            )\n            #expect(\n                ImageProcessors.RoundedCorners(radius: 32, unit: .pixels, border: .init(color: .red)).identifier !=\n                ImageProcessors.RoundedCorners(radius: 16, unit: .points, border: .init(color: .red)).identifier\n            )\n        } else {\n            #expect(\n                ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).identifier !=\n                ImageProcessors.RoundedCorners(radius: 16, unit: .points, border: .init(color: .red)).identifier\n            )\n        }\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).identifier !=\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .blue)).identifier\n        )\n    }\n\n    @Test @MainActor func equalHashableIdentifiers() {\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16).hashableIdentifier ==\n            ImageProcessors.RoundedCorners(radius: 16).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels).hashableIdentifier ==\n            ImageProcessors.RoundedCorners(radius: 16 / Screen.scale, unit: .points).hashableIdentifier\n        )\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).hashableIdentifier ==\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).hashableIdentifier\n        )\n    }\n\n    @Test @MainActor func notEqualHashableIdentifiers() {\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16).hashableIdentifier !=\n            ImageProcessors.RoundedCorners(radius: 8).hashableIdentifier\n        )\n        if Screen.scale == 1 {\n            #expect(\n                ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).hashableIdentifier ==\n                ImageProcessors.RoundedCorners(radius: 16, unit: .points, border: .init(color: .red)).hashableIdentifier\n            )\n            #expect(\n                ImageProcessors.RoundedCorners(radius: 32, unit: .pixels, border: .init(color: .red)).hashableIdentifier !=\n                ImageProcessors.RoundedCorners(radius: 16, unit: .points, border: .init(color: .red)).hashableIdentifier\n            )\n        } else {\n            #expect(\n                ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).hashableIdentifier !=\n                ImageProcessors.RoundedCorners(radius: 16, unit: .points, border: .init(color: .red)).hashableIdentifier\n            )\n        }\n        #expect(\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red)).hashableIdentifier !=\n            ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .blue)).hashableIdentifier\n        )\n    }\n\n    @Test func description() {\n        // Given\n        let processor = ImageProcessors.RoundedCorners(radius: 16, unit: .pixels)\n\n        // Then\n        #expect(processor.description == \"RoundedCorners(radius: 16.0 pixels, border: nil)\")\n    }\n\n    @Test func descriptionWithBorder() {\n        // Given\n        let processor = ImageProcessors.RoundedCorners(radius: 16, unit: .pixels, border: .init(color: .red, width: 2, unit: .pixels))\n\n        // Then\n        #expect(processor.description == \"RoundedCorners(radius: 16.0 pixels, border: Border(color: #FF0000, width: 2.0 pixels))\")\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImagePublisherTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\nimport Combine\nimport Foundation\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImagePublisherTests {\n    private let dataLoader: MockDataLoader\n    private let pipeline: ImagePipeline\n\n    init() {\n        let dataLoader = MockDataLoader()\n        self.dataLoader = dataLoader\n        pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n    }\n\n    // MARK: Common Use Cases\n\n    @Test func lowDataMode() async {\n        // GIVEN\n        let highQualityImageURL = URL(string: \"https://example.com/high-quality-image.jpeg\")!\n        let lowQualityImageURL = URL(string: \"https://example.com/low-quality-image.jpeg\")!\n\n        dataLoader.results[highQualityImageURL] = .failure(URLError(networkUnavailableReason: .constrained) as NSError)\n        dataLoader.results[lowQualityImageURL] = .success((Test.data, Test.urlResponse))\n\n        // WHEN\n        let pipeline = self.pipeline\n\n        // Create the default request to fetch the high quality image.\n        var urlRequest = Foundation.URLRequest(url: highQualityImageURL)\n        urlRequest.allowsConstrainedNetworkAccess = false\n        let request = ImageRequest(urlRequest: urlRequest)\n\n        // WHEN\n        let publisher = pipeline.imagePublisher(with: request).tryCatch { error -> AnyPublisher<ImageResponse, ImagePipeline.Error> in\n            guard (error.dataLoadingError as? URLError)?.networkUnavailableReason == .constrained else {\n                throw error\n            }\n            return pipeline.imagePublisher(with: lowQualityImageURL)\n        }\n\n        var cancellable: AnyCancellable?\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            cancellable = publisher.sink(receiveCompletion: { result in\n                switch result {\n                case .finished:\n                    break // Expected result\n                case .failure:\n                    Issue.record(\"Expected success\")\n                }\n            }, receiveValue: { _ in\n                continuation.resume()\n            })\n        }\n        _ = cancellable\n    }\n\n    // MARK: - Basics\n\n    @Test func imageIsLoaded() async throws {\n        // GIVEN\n        dataLoader.results[Test.url] = .success((Test.data, Test.urlResponse))\n\n        // WHEN/THEN\n        var cancellable: AnyCancellable?\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            cancellable = pipeline.imagePublisher(with: Test.url).sink(\n                receiveCompletion: { result in\n                    if case .failure = result {\n                        Issue.record(\"Expected success\")\n                    }\n                },\n                receiveValue: { response in\n                    continuation.resume()\n                }\n            )\n        }\n        _ = cancellable\n    }\n\n    @Test func errorIsPropagated() async throws {\n        // GIVEN a network error\n        dataLoader.results[Test.url] = .failure(Foundation.URLError(.notConnectedToInternet) as NSError)\n\n        // WHEN/THEN\n        var cancellable: AnyCancellable?\n        var receivedError: ImagePipeline.Error?\n        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in\n            cancellable = pipeline.imagePublisher(with: Test.url).sink(\n                receiveCompletion: { result in\n                    if case .failure(let error) = result {\n                        receivedError = error\n                    }\n                    continuation.resume()\n                },\n                receiveValue: { _ in }\n            )\n        }\n        _ = cancellable\n        #expect(receivedError != nil)\n    }\n\n    @Test func syncCacheLookup() {\n        // GIVEN\n        let cache = MockImageCache()\n        cache[Test.request] = ImageContainer(image: Test.image)\n        let pipeline = pipeline.reconfigured {\n            $0.imageCache = cache\n        }\n\n        // WHEN\n        var image: PlatformImage?\n        let cancellable = pipeline.imagePublisher(with: Test.url).sink(receiveCompletion: { result in\n            switch result {\n            case .finished:\n                break // Expected result\n            case .failure:\n                Issue.record(\"Expected success\")\n            }\n        }, receiveValue: {\n            image = $0.image\n        })\n        _ = cancellable\n\n        // THEN image returned synchronously\n        #expect(image != nil)\n    }\n\n    @Test func cancellation() async {\n        dataLoader.queue.isSuspended = true\n\n        var cancellable: AnyCancellable?\n\n        // Wait for start notification\n        await notification(MockDataLoader.DidStartTask, object: dataLoader) {\n            cancellable = pipeline.imagePublisher(with: Test.url).sink(receiveCompletion: { _ in }, receiveValue: { _ in })\n        }\n\n        // Wait for cancel notification\n        await notification(MockDataLoader.DidCancelTask, object: dataLoader) {\n            cancellable?.cancel()\n        }\n        _ = cancellable\n    }\n}\n\n/// We have to mock it because there is no way to construct native `URLError`\n/// with a `networkUnavailableReason`.\nprivate struct URLError: Swift.Error {\n    var networkUnavailableReason: NetworkUnavailableReason?\n\n    enum NetworkUnavailableReason {\n        case cellular\n        case expensive\n        case constrained\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageRequestTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageRequestTests {\n    // The compiler picks up the new version\n    @Test func testInit() {\n        _ = ImageRequest(url: Test.url)\n        _ = ImageRequest(url: Test.url, processors: [])\n        _ = ImageRequest(url: Test.url, processors: [])\n        _ = ImageRequest(url: Test.url, priority: .high)\n        _ = ImageRequest(url: Test.url, options: [.reloadIgnoringCachedData])\n    }\n\n    @Test func expressibleByStringLiteral() {\n        let _: ImageRequest = \"https://example.com/image.jpeg\"\n    }\n\n    // MARK: - CoW\n\n    @Test func copyOnWrite() {\n        // GIVEN\n        var request = ImageRequest(url: URL(string: \"http://test.com/1.png\"))\n        request.options.insert(.disableMemoryCacheReads)\n        request.userInfo[\"key\"] = \"3\"\n        request.processors = [MockImageProcessor(id: \"4\")]\n        request.priority = .high\n\n        // WHEN\n        var copy = request\n        // Request makes a copy at this point under the hood.\n        copy.priority = .low\n\n        // THEN\n        #expect(copy.options.contains(.disableMemoryCacheReads) == true)\n        #expect(copy.userInfo[\"key\"] as? String == \"3\")\n        #expect((copy.processors.first as? MockImageProcessor)?.identifier == \"4\")\n        #expect(request.priority == .high) // Original request not updated\n        #expect(copy.priority == .low)\n    }\n\n    // MARK: - Misc\n\n    // Just to make sure that comparison works as expected.\n    @Test func priorityComparison() {\n        typealias Priority = ImageRequest.Priority\n        #expect(Priority.veryLow < Priority.veryHigh)\n        #expect(Priority.low < Priority.normal)\n        #expect(Priority.normal == Priority.normal)\n    }\n\n    @Test func userInfoKey() {\n        // WHEN\n        let request = ImageRequest(url: Test.url, userInfo: [.init(\"a\"): 1])\n\n        // THEN\n        #expect(request.userInfo[\"a\"] != nil)\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageRequestCacheKeyTests {\n    @Test func defaults() {\n        let request = Test.request\n        assertHashableEqual(MemoryCacheKey(request), MemoryCacheKey(request)) // equal to itself\n    }\n\n    @Test func requestsWithTheSameURLsAreEquivalent() {\n        let lhs = ImageRequest(url: Test.url)\n        let rhs = ImageRequest(url: Test.url)\n        assertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))\n    }\n\n    @Test func requestsWithDefaultURLRequestAndURLAreEquivalent() {\n        let lhs = ImageRequest(url: Test.url)\n        let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url))\n        assertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))\n    }\n\n    @Test func requestsWithDifferentURLsAreNotEquivalent() {\n        let lhs = ImageRequest(url: URL(string: \"http://test.com/1.png\"))\n        let rhs = ImageRequest(url: URL(string: \"http://test.com/2.png\"))\n        #expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs))\n    }\n\n    @Test func requestsWithTheSameProcessorsAreEquivalent() {\n        let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"1\")])\n        let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"1\")])\n        assertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))\n    }\n\n    @Test func requestsWithDifferentProcessorsAreNotEquivalent() {\n        let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"1\")])\n        let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"2\")])\n        #expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs))\n    }\n\n    @Test func urlRequestParametersAreIgnored() {\n        let lhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50))\n        let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0))\n        assertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))\n    }\n\n    @Test func settingDefaultProcessorManually() {\n        let lhs = ImageRequest(url: Test.url)\n        let rhs = ImageRequest(url: Test.url, processors: lhs.processors)\n        assertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageRequestLoadKeyTests {\n    @Test func defaults() {\n        let request = ImageRequest(url: Test.url)\n        assertHashableEqual(TaskFetchOriginalDataKey(request), TaskFetchOriginalDataKey(request))\n    }\n\n    @Test func requestsWithTheSameURLsAreEquivalent() {\n        let lhs = ImageRequest(url: Test.url)\n        let rhs = ImageRequest(url: Test.url)\n        assertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))\n    }\n\n    @Test func requestsWithDifferentURLsAreNotEquivalent() {\n        let lhs = ImageRequest(url: URL(string: \"http://test.com/1.png\"))\n        let rhs = ImageRequest(url: URL(string: \"http://test.com/2.png\"))\n        #expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs))\n    }\n\n    @Test func requestsWithTheSameProcessorsAreEquivalent() {\n        let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"1\")])\n        let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"1\")])\n        assertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))\n    }\n\n    @Test func requestsWithDifferentProcessorsAreEquivalent() {\n        let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"1\")])\n        let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: \"2\")])\n        assertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))\n    }\n\n    @Test func requestWithDifferentURLRequestParametersAreNotEquivalent() {\n        let lhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50))\n        let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0))\n        #expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs))\n    }\n\n    @Test func mockImageProcessorCorrectlyImplementsIdentifiers() {\n        #expect(MockImageProcessor(id: \"1\").identifier == MockImageProcessor(id: \"1\").identifier)\n        #expect(MockImageProcessor(id: \"1\").hashableIdentifier == MockImageProcessor(id: \"1\").hashableIdentifier)\n\n        #expect(MockImageProcessor(id: \"1\").identifier != MockImageProcessor(id: \"2\").identifier)\n        #expect(MockImageProcessor(id: \"1\").hashableIdentifier != MockImageProcessor(id: \"2\").hashableIdentifier)\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageRequestImageIdTests {\n    @Test func thatCacheKeyUsesAbsoluteURLByDefault() {\n        let lhs = ImageRequest(url: Test.url)\n        let rhs = ImageRequest(url: Test.url.appendingPathComponent(\"?token=1\"))\n        #expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs))\n    }\n\n    @Test func thatCacheKeyUsesFilteredURLWhenSet() {\n        let lhs = ImageRequest(url: Test.url).with {\n            $0.imageID = Test.url.absoluteString\n        }\n        let rhs = ImageRequest(url: Test.url.appendingPathComponent(\"?token=1\")).with {\n            $0.imageID = Test.url.absoluteString\n        }\n        assertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))\n    }\n\n    @Test func thatCacheKeyForProcessedImageDataUsesAbsoluteURLByDefault() {\n        let lhs = ImageRequest(url: Test.url)\n        let rhs = ImageRequest(url: Test.url.appendingPathComponent(\"?token=1\"))\n        #expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs))\n    }\n\n    @Test func thatCacheKeyForProcessedImageDataUsesFilteredURLWhenSet() {\n        let lhs = ImageRequest(url: Test.url).with {\n            $0.imageID = Test.url.absoluteString\n        }\n        let rhs = ImageRequest(url: Test.url.appendingPathComponent(\"?token=1\")).with {\n            $0.imageID = Test.url.absoluteString\n        }\n        assertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))\n    }\n\n    @Test func thatLoadKeyForProcessedImageDoesntUseFilteredURL() {\n        let lhs = ImageRequest(url: Test.url).with {\n            $0.imageID = Test.url.absoluteString\n        }\n        let rhs = ImageRequest(url: Test.url.appendingPathComponent(\"?token=1\")).with {\n            $0.imageID = Test.url.absoluteString\n        }\n        #expect(TaskLoadImageKey(lhs) != TaskLoadImageKey(rhs))\n    }\n\n    @Test func thatLoadKeyForOriginalImageDoesntUseFilteredURL() {\n        let lhs = ImageRequest(url: Test.url).with {\n            $0.imageID = Test.url.absoluteString\n        }\n        let rhs = ImageRequest(url: Test.url.appendingPathComponent(\"?token=1\")).with {\n            $0.imageID = Test.url.absoluteString\n        }\n        #expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs))\n    }\n\n    @Test(.disabled()) func memoryLayout() {\n        #expect(ImageRequest._containerInstanceSize == 104)\n\n        #expect(MemoryLayout<ImageRequest.ThumbnailOptions>.size == 9)\n        #expect(MemoryLayout<ImageRequest.ThumbnailOptions>.stride == 12)\n\n        #expect(MemoryLayout<ImageRequest.Resource>.size == 17)\n        #expect(MemoryLayout<ImageRequest.Resource>.stride == 24)\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ThumbnailOptionsTests {\n    // MARK: - Default Values\n\n    @Test func defaultBoolPropertiesWithMaxPixelSize() {\n        let options = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        #expect(options.createThumbnailFromImageIfAbsent == true)\n        #expect(options.createThumbnailFromImageAlways == true)\n        #expect(options.createThumbnailWithTransform == true)\n        #expect(options.shouldCacheImmediately == true)\n    }\n\n    @Test func defaultBoolPropertiesWithSize() {\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels)\n        #expect(options.createThumbnailFromImageIfAbsent == true)\n        #expect(options.createThumbnailFromImageAlways == true)\n        #expect(options.createThumbnailWithTransform == true)\n        #expect(options.shouldCacheImmediately == true)\n    }\n\n    // MARK: - contentMode\n\n    @Test func contentModeDefaultsToAspectFill() {\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels)\n        #expect(options.contentMode == .aspectFill)\n    }\n\n    @Test func contentModeAspectFitIsPreserved() {\n        let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFit)\n        #expect(options.contentMode == .aspectFit)\n    }\n\n    // MARK: - Identifier reflects flag changes\n\n    @Test func identifierChangesWhenCreateFromImageIfAbsentIsFalse() {\n        var options = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        options.createThumbnailFromImageIfAbsent = false\n        #expect(options.identifier.hasSuffix(\"options=falsetruetruetrue\"))\n    }\n\n    @Test func identifierChangesWhenCreateFromImageAlwaysIsFalse() {\n        var options = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        options.createThumbnailFromImageAlways = false\n        #expect(options.identifier.hasSuffix(\"options=truefalsetruetrue\"))\n    }\n\n    @Test func identifierChangesWhenCreateWithTransformIsFalse() {\n        var options = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        options.createThumbnailWithTransform = false\n        #expect(options.identifier.hasSuffix(\"options=truetruefalsetrue\"))\n    }\n\n    @Test func identifierChangesWhenShouldCacheImmediatelyIsFalse() {\n        var options = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        options.shouldCacheImmediately = false\n        #expect(options.identifier.hasSuffix(\"options=truetruetruefalse\"))\n    }\n\n    // MARK: - Hashable\n\n    @Test func equalOptionsAreEqual() {\n        #expect(ImageRequest.ThumbnailOptions(maxPixelSize: 400) == ImageRequest.ThumbnailOptions(maxPixelSize: 400))\n    }\n\n    @Test func optionsWithDifferentFlagAreNotEqual() {\n        let lhs = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        var rhs = ImageRequest.ThumbnailOptions(maxPixelSize: 400)\n        rhs.createThumbnailWithTransform = false\n        #expect(lhs != rhs)\n    }\n\n    @Test func optionsWithDifferentContentModeAreNotEqual() {\n        let lhs = ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFill)\n        let rhs = ImageRequest.ThumbnailOptions(size: CGSize(width: 400, height: 400), unit: .pixels, contentMode: .aspectFit)\n        #expect(lhs != rhs)\n    }\n\n    @Test func thumbnailOptionsWithDifferentMaxPixelSizeHaveDifferentIdentifiers() {\n        let small = ImageRequest.ThumbnailOptions(maxPixelSize: 200)\n        let large = ImageRequest.ThumbnailOptions(maxPixelSize: 800)\n        #expect(small.identifier != large.identifier)\n    }\n\n    @Test func thumbnailOptionsWithSameParametersAreEqual() {\n        let a = ImageRequest.ThumbnailOptions(size: CGSize(width: 300, height: 300), unit: .pixels, contentMode: .aspectFit)\n        let b = ImageRequest.ThumbnailOptions(size: CGSize(width: 300, height: 300), unit: .pixels, contentMode: .aspectFit)\n        #expect(a == b)\n        #expect(a.identifier == b.identifier)\n    }\n\n    @Test func modifyingOptionsOnCopyDoesNotAffectOriginal() {\n        // GIVEN\n        var original = Test.request\n        original.options = []\n\n        // WHEN - make a copy and add an option only to the copy\n        var copy = original\n        copy.options.insert(.disableMemoryCacheReads)\n\n        // THEN - original is unchanged\n        #expect(!original.options.contains(.disableMemoryCacheReads))\n        #expect(copy.options.contains(.disableMemoryCacheReads))\n    }\n\n    @Test func loadOptionsDoNotAffectMemoryCacheKey() {\n        // GIVEN - same URL, but different load-time options\n        let base          = ImageRequest(url: Test.url)\n        let disableReads  = ImageRequest(url: Test.url, options: [.disableDiskCacheReads])\n        let disableWrites = ImageRequest(url: Test.url, options: [.disableDiskCacheWrites])\n        let reload        = ImageRequest(url: Test.url, options: [.reloadIgnoringCachedData])\n\n        // THEN - the memory-cache key is determined by URL/processors, not by load options\n        let baseKey = MemoryCacheKey(base)\n        #expect(MemoryCacheKey(disableReads)  == baseKey)\n        #expect(MemoryCacheKey(disableWrites) == baseKey)\n        #expect(MemoryCacheKey(reload)        == baseKey)\n    }\n}\n\nprivate func assertHashableEqual<T: Hashable>(_ lhs: T, _ rhs: T, sourceLocation: SourceLocation = #_sourceLocation) {\n    #expect(lhs.hashValue == rhs.hashValue, sourceLocation: sourceLocation)\n    #expect(lhs == rhs, sourceLocation: sourceLocation)\n}\n"
  },
  {
    "path": "Tests/NukeTests/ImageResponseTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ImageResponseTests {\n\n    @Test func imageForwardsFromContainer() {\n        let container = ImageContainer(image: Test.image)\n        let response = ImageResponse(container: container, request: Test.request)\n        #expect(response.image === container.image)\n    }\n\n    @Test func isPreviewForwardsFromContainer() {\n        let container = ImageContainer(image: Test.image, isPreview: true)\n        let response = ImageResponse(container: container, request: Test.request)\n        #expect(response.isPreview == true)\n    }\n\n    @Test func defaultsForOptionalProperties() {\n        let response = ImageResponse(container: ImageContainer(image: Test.image), request: Test.request)\n        #expect(response.urlResponse == nil)\n        #expect(response.cacheType == nil)\n    }\n\n    @Test func cacheTypeValuesAreDistinct() {\n        #expect(ImageResponse.CacheType.memory != .disk)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/LinkedListTest.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2)))\nstruct LinkedListTests {\n    let list = LinkedList<Int>()\n\n    @Test func emptyWhenCreated() {\n        #expect(list.first == nil)\n        #expect(list.last == nil)\n        #expect(list.isEmpty)\n    }\n\n    // MARK: - Append\n\n    @Test func appendOnce() {\n        // When\n        list.append(1)\n\n        // Then\n        #expect(!list.isEmpty)\n        #expect(list.first?.value == 1)\n        #expect(list.last?.value == 1)\n    }\n\n    @Test func appendTwice() {\n        // When\n        list.append(1)\n        list.append(2)\n\n        // Then\n        #expect(list.first?.value == 1)\n        #expect(list.last?.value == 2)\n    }\n\n    // MARK: - Remove\n\n    @Test func removeSingle() {\n        // Given\n        let node = list.append(1)\n\n        // When\n        list.remove(node)\n\n        // Then\n        #expect(list.first == nil)\n        #expect(list.last == nil)\n    }\n\n    @Test func removeFromBeginning() {\n        // Given\n        let node = list.append(1)\n        list.append(2)\n        list.append(3)\n\n        // When\n        list.remove(node)\n\n        // Then\n        #expect(list.first?.value == 2)\n        #expect(list.last?.value == 3)\n    }\n\n    @Test func removeFromEnd() {\n        // Given\n        list.append(1)\n        list.append(2)\n        let node = list.append(3)\n\n        // When\n        list.remove(node)\n\n        // Then\n        #expect(list.first?.value == 1)\n        #expect(list.last?.value == 2)\n    }\n\n    @Test func removeFromMiddle() {\n        // Given\n        list.append(1)\n        let node = list.append(2)\n        list.append(3)\n\n        // When\n        list.remove(node)\n\n        // Then\n        #expect(list.first?.value == 1)\n        #expect(list.last?.value == 3)\n    }\n\n    @Test func removeAll() {\n        // Given\n        list.append(1)\n        list.append(2)\n        list.append(3)\n\n        // When\n        list.removeAllElements()\n\n        // Then\n        #expect(list.first == nil)\n        #expect(list.last == nil)\n    }\n\n    // MARK: - Prepend\n\n    @Test func prependToEmptyList() {\n        // Given\n        let node = LinkedList<Int>.Node(value: 42)\n\n        // When\n        list.prepend(node)\n\n        // Then\n        #expect(list.first?.value == 42)\n        #expect(list.last?.value == 42)\n        #expect(!list.isEmpty)\n    }\n\n    @Test func prependToNonEmptyList() {\n        // Given\n        list.append(2)\n        list.append(3)\n        let node = LinkedList<Int>.Node(value: 1)\n\n        // When\n        list.prepend(node)\n\n        // Then\n        #expect(list.first?.value == 1)\n        #expect(list.last?.value == 3)\n    }\n\n    // MARK: - Node Links\n\n    @Test func appendPreservesOrder() {\n        // Given\n        list.append(1)\n        list.append(2)\n        list.append(3)\n\n        // Then values are accessible in insertion order via first/last\n        #expect(list.first?.value == 1)\n        #expect(list.last?.value == 3)\n        #expect(!list.isEmpty)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/RateLimiterTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2))) @ImagePipelineActor\nstruct RateLimiterTests {\n    let rateLimiter = RateLimiter(rate: 10, burst: 2)\n\n    @Test func burstIsExecutedImmediately() {\n        var isExecuted = Array(repeating: false, count: 4)\n        for i in isExecuted.indices {\n            rateLimiter.execute {\n                isExecuted[i] = true\n                return true\n            }\n        }\n        #expect(isExecuted == [true, true, false, false], \"Expect first 2 items to be executed immediately\")\n    }\n\n    @Test func posponedItemsDoNotExtractFromBucket() {\n        var isExecuted = Array(repeating: false, count: 4)\n        for i in isExecuted.indices {\n            rateLimiter.execute {\n                isExecuted[i] = true\n                return i != 1 // important!\n            }\n        }\n        #expect(isExecuted == [true, true, true, false], \"Expect first 2 items to be executed immediately\")\n    }\n\n    @Test(.disabled(\"Deadlocks on @ImagePipelineActor with withUnsafeContinuation — iOS 26.2\")) func overflow() async {\n        let count = 3\n        await confirmation(expectedCount: count) { done in\n            for _ in 0..<count {\n                await withUnsafeContinuation { continuation in\n                    rateLimiter.execute {\n                        done()\n                        continuation.resume(returning: ())\n                        return true\n                    }\n                }\n            }\n        }\n    }\n\n    // MARK: - Edge Cases\n\n    @Test func burstOfOneExecutesSingleItemImmediately() {\n        // GIVEN - rate limiter that only allows 1 immediate execution\n        let limiter = RateLimiter(rate: 10, burst: 1)\n        var executed = [false, false]\n\n        // WHEN\n        limiter.execute { executed[0] = true; return true }\n        limiter.execute { executed[1] = true; return true }\n\n        // THEN - only the first item runs immediately; the second is deferred\n        #expect(executed[0] == true)\n        #expect(executed[1] == false)\n    }\n\n    @Test func allPostponedItemsDoNotDrainBucket() {\n        // GIVEN - all items return false (none extract a token)\n        let limiter = RateLimiter(rate: 10, burst: 2)\n        var executed = [false, false, false, false, false]\n\n        for i in executed.indices {\n            limiter.execute {\n                executed[i] = true\n                return false // never consumes a token\n            }\n        }\n\n        // THEN - burst allows the first 2 to run; subsequent items are queued\n        // but since they all return false, earlier items' buckets refill and\n        // the third item also executes (no token consumed)\n        #expect(executed[0] == true)\n        #expect(executed[1] == true)\n        #expect(executed[2] == true)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/ResumableDataTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n// Test ResumableData directly to make sure it makes the right decisions based\n// on HTTP flows.\n@Suite(.timeLimit(.minutes(2)))\nstruct ResumableDataTests {\n    @Test func resumingRequest() {\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"ETag\": \"1234\"\n        ])\n        let data = ResumableData(response: response, data: _data)!\n        var request = URLRequest(url: Test.url)\n        data.resume(request: &request)\n\n        // Check that we've set both required \"range\" filed\n        #expect(request.allHTTPHeaderFields?[\"Range\"] == \"bytes=1000-\")\n        #expect(request.allHTTPHeaderFields?[\"If-Range\"] == \"1234\")\n    }\n\n    @Test func resumingRequestUsesLastModifiedWhenNoETag() {\n        // GIVEN resumable data validated by Last-Modified (no ETag)\n        let lastModified = \"Wed, 21 Oct 2015 07:28:00 GMT\"\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"Last-Modified\": lastModified\n        ])\n        let data = ResumableData(response: response, data: _data)!\n        var request = URLRequest(url: Test.url)\n        data.resume(request: &request)\n\n        // THEN Range header is correct and If-Range contains the Last-Modified value\n        #expect(request.allHTTPHeaderFields?[\"Range\"] == \"bytes=1000-\")\n        #expect(request.allHTTPHeaderFields?[\"If-Range\"] == lastModified)\n    }\n\n    @Test func checkingResumedResponse() {\n        #expect(ResumableData.isResumedResponse(_makeResponse(statusCode: 206)))\n\n        // Need to load new data\n        #expect(!ResumableData.isResumedResponse(_makeResponse(statusCode: 200)))\n\n        #expect(!ResumableData.isResumedResponse(_makeResponse(statusCode: 404)))\n    }\n\n    // MARK: - Creation (Positive)\n\n    @Test func createWithETag() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"ETag\": \"1234\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data != nil)\n        #expect(data?.data.count == 1000)\n        #expect(data?.validator == \"1234\")\n    }\n\n    @Test func createWithETagSpelledIncorrectly() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"Etag\": \"1234\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data != nil)\n        #expect(data?.data.count == 1000)\n        #expect(data?.validator == \"1234\")\n    }\n\n    @Test func createWithLastModified() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"Last-Modified\": \"Wed, 21 Oct 2015 07:28:00 GMT\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data != nil)\n        #expect(data?.data.count == 1000)\n        #expect(data?.validator == \"Wed, 21 Oct 2015 07:28:00 GMT\")\n    }\n\n    @Test func createWithBothValidators() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"ETag\": \"1234\",\n            \"Content-Length\": \"2000\",\n            \"Last-Modified\": \"Wed, 21 Oct 2015 07:28:00 GMT\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data != nil)\n        #expect(data?.data.count == 1000)\n        #expect(data?.validator == \"1234\")\n    }\n\n    // We should store resumable data not just for status code \"200 OK\", but also\n    // for \"206 Partial Content\" in case the resumed download fails.\n    @Test func createWithStatusCodePartialContent() {\n        // Given\n        let response = _makeResponse(statusCode: 206, headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"ETag\": \"1234\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data != nil)\n        #expect(data?.data.count == 1000)\n        #expect(data?.validator == \"1234\")\n    }\n\n    // MARK: - Creation (Negative)\n\n    @Test func createWithEmptyData() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"ETag\": \"1234\"\n        ])\n        let data = ResumableData(response: response, data: Data())\n\n        // Then\n        #expect(data == nil)\n    }\n\n    @Test func createWithNotHTTPResponse() {\n        // Given\n        let response = URLResponse(url: Test.url, mimeType: \"jpeg\", expectedContentLength: 10000, textEncodingName: nil)\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data == nil)\n    }\n\n    @Test func createWithInvalidStatusCode() {\n        // Given\n        let response = _makeResponse(statusCode: 304, headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"ETag\": \"1234\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data == nil)\n    }\n\n    @Test func createWithMissingValidator() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data == nil)\n    }\n\n    @Test func createWithMissingAcceptRanges() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"ETag\": \"1234\",\n            \"Content-Length\": \"2000\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data == nil)\n    }\n\n    @Test func createWithAcceptRangesNone() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"none\",\n            \"Content-Length\": \"2000\",\n            \"ETag\": \"1234\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data == nil)\n    }\n\n    @Test func createWhenFullDataIsLoaded() {\n        // Given\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"none\",\n            \"Content-Length\": \"1000\",\n            \"ETag\": \"1234\"\n        ])\n        let data = ResumableData(response: response, data: _data)\n\n        // Then\n        #expect(data == nil)\n    }\n\n    @Test func createWhenDownloadIsCompleteReturnsNil() {\n        // GIVEN data whose length equals the Content-Length (download is complete)\n        let completeData = Data(count: 2000)\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"ETag\": \"1234\"\n        ])\n\n        // WHEN trying to create resumable data from a fully-downloaded response\n        let data = ResumableData(response: response, data: completeData)\n\n        // THEN no resumable data is created — there is nothing to resume\n        #expect(data == nil)\n    }\n\n    @Test func createWhenDataExceedsContentLengthReturnsNil() {\n        // GIVEN data that exceeds the declared Content-Length (e.g. due to rounding)\n        let oversizedData = Data(count: 2001)\n        let response = _makeResponse(headers: [\n            \"Accept-Ranges\": \"bytes\",\n            \"Content-Length\": \"2000\",\n            \"ETag\": \"xyz\"\n        ])\n\n        let data = ResumableData(response: response, data: oversizedData)\n\n        #expect(data == nil)\n    }\n}\n\n@ImagePipelineActor\n@Suite(.timeLimit(.minutes(2)))\nstruct ResumableDataStorageTests {\n    @Test func registerAndUnregister() {\n        let storage = ResumableDataStorage.shared\n\n        let pipeline = ImagePipeline {\n            $0.dataLoader = MockDataLoader()\n        }\n\n        storage.register(pipeline.id)\n        storage.unregister(pipeline.id)\n    }\n\n    @Test func storeAndRemoveResumableData() throws {\n        let storage = ResumableDataStorage.shared\n        let pipeline = ImagePipeline {\n            $0.dataLoader = MockDataLoader()\n        }\n        storage.register(pipeline.id)\n\n        let response = HTTPURLResponse(\n            url: Test.url,\n            statusCode: 200,\n            httpVersion: \"HTTP/1.1\",\n            headerFields: [\n                \"Accept-Ranges\": \"bytes\",\n                \"Content-Length\": \"2000\",\n                \"ETag\": \"abc123\"\n            ]\n        )!\n        let resumableData = ResumableData(response: response, data: Data(count: 1000))!\n\n        let request = ImageRequest(url: Test.url)\n        storage.storeResumableData(resumableData, for: request, pipeline: pipeline)\n\n        let retrieved = try #require(storage.removeResumableData(for: request, pipeline: pipeline))\n        #expect(retrieved.data.count == 1000)\n        #expect(retrieved.validator == \"abc123\")\n\n        // Should be nil after removal\n        #expect(storage.removeResumableData(for: request, pipeline: pipeline) == nil)\n\n        storage.unregister(pipeline.id)\n    }\n\n    @Test func removeAllResponses() {\n        let storage = ResumableDataStorage.shared\n        let pipeline = ImagePipeline {\n            $0.dataLoader = MockDataLoader()\n        }\n        storage.register(pipeline.id)\n\n        let response = HTTPURLResponse(\n            url: Test.url,\n            statusCode: 200,\n            httpVersion: \"HTTP/1.1\",\n            headerFields: [\n                \"Accept-Ranges\": \"bytes\",\n                \"Content-Length\": \"2000\",\n                \"ETag\": \"xyz\"\n            ]\n        )!\n        let resumableData = ResumableData(response: response, data: Data(count: 1000))!\n\n        storage.storeResumableData(resumableData, for: ImageRequest(url: Test.url), pipeline: pipeline)\n        storage.removeAllResponses()\n\n        let retrieved = storage.removeResumableData(for: ImageRequest(url: Test.url), pipeline: pipeline)\n        #expect(retrieved == nil)\n\n        storage.unregister(pipeline.id)\n    }\n}\n\nprivate let _data = Data(count: 1000)\n\nprivate func _makeResponse(statusCode: Int = 200, headers: [String: String]? = nil) -> HTTPURLResponse {\n    return HTTPURLResponse(url: Test.url, statusCode: statusCode, httpVersion: \"HTTP/1.2\", headerFields: headers)!\n}\n"
  },
  {
    "path": "Tests/NukeTests/TaskQueueTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2))) @ImagePipelineActor\nstruct TaskQueueTests {\n    // MARK: - Basic Execution\n\n    @Test func addedWorkIsExecuted() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let executed = Ref(false)\n\n        // When\n        queue.add { executed.value = true }\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(executed.value)\n    }\n\n    @Test func multipleWorkItemsAllComplete() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let count = Ref(0)\n\n        // When\n        queue.add { count.value += 1 }\n        queue.add { count.value += 1 }\n        queue.add { count.value += 1 }\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(count.value == 3)\n    }\n\n    @Test func addReturnsTaskQueueOperation() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n\n        // When\n        let operation = queue.add { }\n\n        // Then\n        #expect(!operation.isCancelled)\n    }\n\n    // MARK: - Concurrency Limit\n\n    @Test func respectsmaxConcurrentOperationCountOfOne() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let item1Started = TestExpectation()\n        let item2Started = Ref(false)\n        let gate = TestExpectation()\n\n        queue.add {\n            item1Started.fulfill()\n            await gate.wait()\n        }\n        queue.add {\n            item2Started.value = true\n        }\n\n        // When – wait for item 1 to start\n        await item1Started.wait()\n\n        // Then – item 2 hasn't started because maxConcurrentOperationCount is 1\n        #expect(!item2Started.value)\n\n        // Cleanup – release item 1 so the queue drains\n        gate.fulfill()\n        await queue.waitUntilAllOperationsAreFinished()\n        #expect(item2Started.value)\n    }\n\n    @Test func respectsmaxConcurrentOperationCountOfTwo() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 2)\n        let item1Started = TestExpectation()\n        let item2Started = TestExpectation()\n        let item3Started = Ref(false)\n        let gate1 = TestExpectation()\n        let gate2 = TestExpectation()\n\n        queue.add {\n            item1Started.fulfill()\n            await gate1.wait()\n        }\n        queue.add {\n            item2Started.fulfill()\n            await gate2.wait()\n        }\n        queue.add {\n            item3Started.value = true\n        }\n\n        // When – wait for both slots to fill\n        await item1Started.wait()\n        await item2Started.wait()\n\n        // Then – item 3 is blocked because both slots are occupied\n        #expect(!item3Started.value)\n\n        // Cleanup\n        gate1.fulfill()\n        gate2.fulfill()\n        await queue.waitUntilAllOperationsAreFinished()\n        #expect(item3Started.value)\n    }\n\n    @Test func increasingmaxConcurrentOperationCountDrainsPendingWork() async {\n        // Given – capacity 1, two items: one running, one blocked\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let item1Started = TestExpectation()\n        let item2Started = TestExpectation()\n        let gate1 = TestExpectation()\n        let gate2 = TestExpectation()\n\n        queue.add {\n            item1Started.fulfill()\n            await gate1.wait()\n        }\n        queue.add {\n            item2Started.fulfill()\n            await gate2.wait()\n        }\n\n        await item1Started.wait()\n\n        // When – increase capacity to 2\n        queue.maxConcurrentOperationCount = 2\n\n        // Then – item 2 starts immediately\n        await item2Started.wait()\n\n        // Cleanup\n        gate1.fulfill()\n        gate2.fulfill()\n        await queue.waitUntilAllOperationsAreFinished()\n    }\n\n    @Test func decreasingmaxConcurrentOperationCountLetsRunningFinish() async {\n        // Given – capacity 2, two items running\n        let queue = TaskQueue(maxConcurrentOperationCount: 2)\n        let item1Started = TestExpectation()\n        let item2Started = TestExpectation()\n        let item3Started = Ref(false)\n        let gate1 = TestExpectation()\n        let gate2 = TestExpectation()\n\n        queue.add {\n            item1Started.fulfill()\n            await gate1.wait()\n        }\n        queue.add {\n            item2Started.fulfill()\n            await gate2.wait()\n        }\n        queue.add {\n            item3Started.value = true\n        }\n\n        await item1Started.wait()\n        await item2Started.wait()\n\n        // When – reduce capacity to 1 while 2 are running\n        queue.maxConcurrentOperationCount = 1\n\n        // Then – both running items complete\n        gate1.fulfill()\n        gate2.fulfill()\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Item 3 also ran (after one of the first two finished, count dropped below limit)\n        #expect(item3Started.value)\n    }\n\n    @Test func settingSamemaxConcurrentOperationCountDoesNotDrain() async {\n        // Given – capacity 1, one item running, one pending\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let item1Started = TestExpectation()\n        let item2Started = Ref(false)\n        let gate = TestExpectation()\n\n        queue.add {\n            item1Started.fulfill()\n            await gate.wait()\n        }\n        queue.add {\n            item2Started.value = true\n        }\n\n        await item1Started.wait()\n\n        // When – set to the same value\n        queue.maxConcurrentOperationCount = 1\n\n        // Then – no extra drain, item 2 still pending\n        #expect(!item2Started.value)\n\n        // Cleanup\n        gate.fulfill()\n        await queue.waitUntilAllOperationsAreFinished()\n    }\n\n    // MARK: - Priority\n\n    @Test func highPriorityItemExecutesFirst() async {\n        // Given – suspended queue so we can enqueue before any execute\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n\n        let order = Ref<[String]>([])\n\n        let lowOp = queue.add { order.value.append(\"low\") }\n        lowOp.priority = .low\n\n        queue.add { order.value.append(\"normal\") }\n\n        let highOp = queue.add { order.value.append(\"high\") }\n        highOp.priority = .high\n\n        // When\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(order.value == [\"high\", \"normal\", \"low\"])\n    }\n\n    @Test func priorityCanBeUpdatedBeforeExecution() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n\n        let order = Ref<[String]>([])\n\n        let op1 = queue.add { order.value.append(\"first\") }\n        let op2 = queue.add { order.value.append(\"second\") }\n\n        // When – boost the second item's priority\n        op1.priority = .low\n        op2.priority = .high\n\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(order.value == [\"second\", \"first\"])\n    }\n\n    @Test func fifoOrderWithinSamePriority() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let order = Ref<[Int]>([])\n\n        // When – all items have the same priority\n        queue.add { order.value.append(1) }\n        queue.add { order.value.append(2) }\n        queue.add { order.value.append(3) }\n\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then – FIFO within the same priority bucket\n        #expect(order.value == [1, 2, 3])\n    }\n\n    @Test func decreasingPriorityMovesOperationBackward() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let order = Ref<[String]>([])\n\n        let opA = queue.add { order.value.append(\"A\") }\n        opA.priority = .high\n\n        let opB = queue.add { order.value.append(\"B\") }\n        opB.priority = .high\n\n        queue.add { order.value.append(\"C\") }\n\n        // When – drop A from high to low\n        opA.priority = .low\n\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then – B (high), C (normal), A (low)\n        #expect(order.value == [\"B\", \"C\", \"A\"])\n    }\n\n    @Test func decreasedPriorityGoesAheadOfExistingLowerPriorityItems() async {\n        // Given – A(high), B(normal), C(normal)\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let order = Ref<[String]>([])\n\n        let opA = queue.add { order.value.append(\"A\") }\n        opA.priority = .high\n\n        queue.add { order.value.append(\"B\") }\n        queue.add { order.value.append(\"C\") }\n\n        // When – drop A from high to normal; it should go *ahead* of B and C\n        opA.priority = .normal\n\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then – A was once higher priority, so it leads the normal bucket\n        #expect(order.value == [\"A\", \"B\", \"C\"])\n    }\n\n    @Test func decreasedPriorityPrependsAcrossMultipleDrops() async {\n        // Given – A(veryHigh), B(normal), C(normal)\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let order = Ref<[String]>([])\n\n        let opA = queue.add { order.value.append(\"A\") }\n        opA.priority = .veryHigh\n\n        queue.add { order.value.append(\"B\") }\n        queue.add { order.value.append(\"C\") }\n\n        // When – drop A from veryHigh to normal\n        opA.priority = .normal\n\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then – A prepended into normal bucket, ahead of B and C\n        #expect(order.value == [\"A\", \"B\", \"C\"])\n    }\n\n    @Test func increasedPriorityAppendsAfterExistingHigherPriorityItems() async {\n        // Given – A(normal), B(high), C(high)\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let order = Ref<[String]>([])\n\n        let opA = queue.add { order.value.append(\"A\") }\n        let opB = queue.add { order.value.append(\"B\") }\n        opB.priority = .high\n        let opC = queue.add { order.value.append(\"C\") }\n        opC.priority = .high\n\n        // When – boost A from normal to high; it should go *after* B and C (FIFO)\n        opA.priority = .high\n\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then – B and C were already high, A appended behind them\n        #expect(order.value == [\"B\", \"C\", \"A\"])\n    }\n\n    @Test func twoDecreasesPrependInOrder() async {\n        // Given – A(high), B(high), C(normal)\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let order = Ref<[String]>([])\n\n        let opA = queue.add { order.value.append(\"A\") }\n        opA.priority = .high\n        let opB = queue.add { order.value.append(\"B\") }\n        opB.priority = .high\n        queue.add { order.value.append(\"C\") }\n\n        // When – drop both A then B to normal; each prepend goes to front\n        opA.priority = .normal  // normal bucket: [A]\n        opB.priority = .normal  // normal bucket: [B, A, C]\n\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then – B was prepended last so it's first, then A, then C\n        #expect(order.value == [\"B\", \"A\", \"C\"])\n    }\n\n    @Test func multiplePriorityChangesBeforeExecution() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let order = Ref<[String]>([])\n\n        let opA = queue.add { order.value.append(\"A\") }\n        let opB = queue.add { order.value.append(\"B\") }\n\n        // When – move A through several priorities\n        opA.priority = .high\n        opA.priority = .veryHigh\n        opA.priority = .low   // final\n\n        opB.priority = .normal // stays normal\n\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then – B (normal) before A (low)\n        #expect(order.value == [\"B\", \"A\"])\n    }\n\n    @Test func priorityChangeOfRunningOperationIsNoOp() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let started = TestExpectation()\n        let gate = TestExpectation()\n\n        let op = queue.add {\n            started.fulfill()\n            await gate.wait()\n        }\n\n        await started.wait()\n\n        // When – operation is already running (node is nil)\n        op.priority = .veryHigh // should not crash\n\n        // Then\n        #expect(op.priority == .veryHigh)\n\n        // Cleanup\n        gate.fulfill()\n        await queue.waitUntilAllOperationsAreFinished()\n    }\n\n    // MARK: - Cancellation\n\n    @Test func cancelledOperationIsNotExecuted() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let executed = Ref(false)\n\n        let operation = queue.add { executed.value = true }\n\n        // When – cancel, then add a sentinel to prove the queue drained\n        operation.cancel()\n        let sentinel = Ref(false)\n        queue.add { sentinel.value = true }\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(!executed.value)\n        #expect(sentinel.value)\n    }\n\n    @Test func cancellingOperationRemovesItFromPending() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n\n        let done = Ref(false)\n        let op1 = queue.add { done.value = true }\n        let op2 = queue.add { }\n\n        // When\n        op2.cancel()\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(done.value)\n        #expect(!op1.isCancelled)\n        #expect(op2.isCancelled)\n    }\n\n    @Test func cancellingAlreadyCancelledOperationIsNoop() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let operation = queue.add { }\n\n        // When\n        operation.cancel()\n        operation.cancel()\n\n        // Then – no crash, still cancelled\n        #expect(operation.isCancelled)\n    }\n\n    @Test func cancellingRunningOperationCancelsUnderlyingTask() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let started = TestExpectation()\n        let taskWasCancelled = Ref(false)\n        let gate = TestExpectation()\n\n        let operation = queue.add {\n            started.fulfill()\n            await withTaskCancellationHandler {\n                await gate.wait()\n            } onCancel: {\n                taskWasCancelled.value = true\n                gate.fulfill() // unblock the closure so it can return\n            }\n        }\n\n        await started.wait()\n\n        // When\n        operation.cancel()\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(taskWasCancelled.value)\n    }\n\n    @Test func cancelledItemIsSkippedDuringDrain() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n\n        let order = Ref<[Int]>([])\n\n        queue.add { order.value.append(1) }\n        let op2 = queue.add { order.value.append(2) }\n        queue.add { order.value.append(3) }\n\n        // When – cancel the middle item\n        op2.cancel()\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(order.value == [1, 3])\n    }\n\n    // MARK: - Suspension\n\n    @Test func suspendedQueueDoesNotExecuteWork() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let executed = Ref(false)\n\n        // When – drain() exits immediately because isSuspended is true\n        queue.add { executed.value = true }\n\n        // Then\n        #expect(!executed.value)\n    }\n\n    @Test func resumingQueueExecutesPendingWork() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let executed = Ref(false)\n\n        queue.add { executed.value = true }\n\n        // When\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(executed.value)\n    }\n\n    @Test func suspendingAlreadySuspendedQueueIsNoop() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n\n        // When\n        queue.isSuspended = true\n        queue.isSuspended = true\n\n        // Then – no crash\n        #expect(queue.isSuspended)\n    }\n\n    @Test func resumingAlreadyResumedQueueIsNoop() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n\n        // When\n        queue.isSuspended = false\n\n        // Then – no crash\n        #expect(!queue.isSuspended)\n    }\n\n    // MARK: - Throwing Work\n\n    @Test func throwingWorkFreesSlot() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let secondRan = Ref(false)\n\n        struct TestError: Error {}\n\n        // When – first work throws, second should still run\n        queue.add { throw TestError() }\n        queue.add { secondRan.value = true }\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(secondRan.value)\n    }\n\n    // MARK: - TaskQueue.Operation\n\n    @Test func defaultPriorityIsNormal() {\n        let operation = TaskQueue.Operation()\n        #expect(operation.priority == .normal)\n    }\n\n    @Test func priorityChangeFiresEvent() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        var didChange = false\n        queue.onEvent = { event in\n            if case .priorityChanged = event { didChange = true }\n        }\n        let operation = queue.add { }\n\n        // When\n        operation.priority = .high\n\n        // Then\n        #expect(didChange)\n        #expect(operation.priority == .high)\n    }\n\n    @Test func settingSamePriorityDoesNotFireEvent() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        var changeCount = 0\n        queue.onEvent = { event in\n            if case .priorityChanged = event { changeCount += 1 }\n        }\n        let operation = queue.add { }\n\n        // When\n        operation.priority = .normal\n\n        // Then\n        #expect(changeCount == 0)\n    }\n\n    @Test func cancelSetsFlag() {\n        let operation = TaskQueue.Operation()\n        operation.cancel()\n        #expect(operation.isCancelled)\n    }\n\n    @Test func cancelFiresEvent() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        var called = false\n        queue.onEvent = { event in\n            if case .cancelled = event { called = true }\n        }\n        let operation = queue.add { }\n\n        // When\n        operation.cancel()\n\n        // Then\n        #expect(called)\n    }\n\n    // MARK: - onEvent(.enqueued)\n\n    @Test func onEventEnqueuedCalledForEachAdd() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 2)\n        var enqueuedCount = 0\n        queue.onEvent = { event in\n            if case .enqueued = event { enqueuedCount += 1 }\n        }\n\n        // When\n        queue.add { }\n        queue.add { }\n        queue.add { }\n\n        // Then\n        #expect(enqueuedCount == 3)\n    }\n\n    @Test func onEventEnqueuedReceivesCorrectOperation() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n\n        var enqueuedOperations = [TaskQueue.Operation]()\n        queue.onEvent = { event in\n            if case .enqueued(let op) = event { enqueuedOperations.append(op) }\n        }\n\n        // When\n        let op1 = queue.add { }\n        let op2 = queue.add { }\n\n        // Then\n        #expect(enqueuedOperations.count == 2)\n        #expect(enqueuedOperations[0] === op1)\n        #expect(enqueuedOperations[1] === op2)\n    }\n\n    // MARK: - operationCount\n\n    @Test func operationCountReflectsPendingItems() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n\n        // When\n        queue.add { }\n        queue.add { }\n\n        // Then – 2 pending, 0 running\n        #expect(queue.operationCount == 2)\n    }\n\n    @Test func operationCountIncludesRunningItems() {\n        // Given – unsuspended queue, items start immediately\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n\n        // When\n        queue.add { }\n        queue.add { }\n\n        // Then – 1 running (started by drain) + 1 pending\n        #expect(queue.operationCount == 2)\n    }\n\n    @Test func operationCountIsZeroWhenEmpty() {\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        #expect(queue.operationCount == 0)\n    }\n\n    @Test func operationCountDecreasesAfterCancellation() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        let op = queue.add { }\n        #expect(queue.operationCount == 1)\n\n        // When\n        op.cancel()\n\n        // Then\n        #expect(queue.operationCount == 0)\n    }\n\n    // MARK: - waitForOperations helper\n\n    @Test func waitForOperationsHelper() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n\n        // When\n        let operations = await queue.waitForOperations(count: 2) {\n            queue.add { }\n            queue.add { }\n        }\n\n        // Then\n        #expect(operations.count == 2)\n    }\n\n    // MARK: - Suspension (Running + Pending)\n\n    @Test func suspendingQueueDoesNotAffectRunningOperations() async {\n        // Given – one running, one pending\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let item1Started = TestExpectation()\n        let item1Finished = TestExpectation()\n        let gate = TestExpectation()\n        let item2Ran = Ref(false)\n\n        queue.add {\n            item1Started.fulfill()\n            await gate.wait()\n            item1Finished.fulfill()\n        }\n        queue.add { item2Ran.value = true }\n\n        await item1Started.wait()\n\n        // When – suspend while item 1 is running\n        queue.isSuspended = true\n        gate.fulfill()\n\n        // Then – item 1 completes despite suspension\n        await item1Finished.wait()\n\n        // Item 2 did NOT start because the queue was suspended\n        #expect(!item2Ran.value)\n        #expect(queue.pendingCount == 1)\n\n        // Resume and let item 2 run\n        queue.isSuspended = false\n        await queue.waitUntilAllOperationsAreFinished()\n        #expect(item2Ran.value)\n    }\n\n    // MARK: - onEvent(.finished)\n\n    @Test func onEventFinishedCalledForEachCompletion() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        var finishedCount = 0\n        queue.onEvent = { event in\n            if case .finished = event { finishedCount += 1 }\n        }\n\n        // When\n        queue.add { }\n        queue.add { }\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(finishedCount == 2)\n    }\n\n    // MARK: - Edge Cases\n\n    @Test func emptyQueueDoesNotCrash() {\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        #expect(!queue.isSuspended)\n        #expect(queue.operationCount == 0)\n    }\n\n    @Test func waitUntilAllOperationsAreFinishedOnEmptyQueue() async {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n\n        // When/Then – returns immediately, no hang\n        await queue.waitUntilAllOperationsAreFinished()\n    }\n\n    @Test func addingWorkAfterQueueHasDrained() async {\n        // Given – queue has already processed work\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.add { }\n        await queue.waitUntilAllOperationsAreFinished()\n        #expect(queue.operationCount == 0)\n\n        // When – add more work\n        let ran = Ref(false)\n        queue.add { ran.value = true }\n        await queue.waitUntilAllOperationsAreFinished()\n\n        // Then\n        #expect(ran.value)\n    }\n\n    @Test func priorityChangeOnCancelledOperationIsNoOp() {\n        // Given\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        queue.isSuspended = true\n        var priorityEventCount = 0\n        queue.onEvent = { event in\n            if case .priorityChanged = event { priorityEventCount += 1 }\n        }\n        let op = queue.add { }\n        op.cancel()\n\n        // When – change priority after cancellation (node is nil)\n        op.priority = .high\n\n        // Then – priority value updates but no queue event fires\n        #expect(op.priority == .high)\n        #expect(priorityEventCount == 0)\n    }\n\n    @Test func cancellingRunningOperationDoesNotChangePendingCount() async {\n        // Given – one running, one pending\n        let queue = TaskQueue(maxConcurrentOperationCount: 1)\n        let started = TestExpectation()\n        let gate = TestExpectation()\n\n        let runningOp = queue.add {\n            started.fulfill()\n            await gate.wait()\n        }\n        queue.add { }\n        await started.wait()\n\n        // Snapshot: 1 running + 1 pending = 2\n        #expect(queue.operationCount == 2)\n\n        // When – cancel the running operation\n        runningOp.cancel()\n\n        // Then – pendingCount unchanged (still 1 pending), running slot freed on completion\n        #expect(queue.pendingCount == 1)\n\n        gate.fulfill()\n        await queue.waitUntilAllOperationsAreFinished()\n    }\n}\n"
  },
  {
    "path": "Tests/NukeTests/TaskTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\n@testable import Nuke\n\n@Suite(.timeLimit(.minutes(2))) @ImagePipelineActor\nstruct TaskTests {\n    // MARK: - Starter\n\n    @Test func starterCalledOnFirstSubscription() {\n        // Given\n        var startCount = 0\n        _ = SimpleTask<Int, Error>(starter: { _ in\n            startCount += 1\n        })\n\n        // Then\n        #expect(startCount == 0)\n    }\n\n    @Test func starterCalledWhenSubscriptionIsAdded() {\n        // Given\n        var startCount = 0\n        let task = SimpleTask<Int, Error>(starter: { _ in\n            startCount += 1\n        })\n\n        // When first subscription is added\n        _ = task.subscribe { _ in }\n\n        // Then started is called\n        #expect(startCount == 1)\n    }\n\n    @Test func starterOnlyCalledOnce() {\n        // Given\n        var startCount = 0\n        let task = SimpleTask<Int, Error>(starter: { _ in\n            startCount += 1\n        })\n\n        // When two subscriptions are added\n        _ = task.subscribe { _ in }\n        _ = task.subscribe { _ in }\n\n        // Then started is only called once\n        #expect(startCount == 1)\n    }\n\n    @Test func starterIsDeallocated() {\n        // Given\n        class Foo {\n        }\n\n        weak var weakFoo: Foo?\n\n        let task: AsyncTask<Int, Error> = autoreleasepool { // Just in case\n            let foo = Foo()\n            weakFoo = foo\n            return SimpleTask<Int, Error>(starter: { _ in\n                _ = foo // Retain foo\n            })\n        }\n\n        #expect(weakFoo != nil, \"Foo is retained by starter\")\n\n        // When first subscription is added and starter is called\n        _ = task.subscribe { _ in }\n\n        // Then\n        #expect(weakFoo == nil, \"Started wasn't deallocated\")\n    }\n\n    // MARK: - Subscribe\n\n    @Test func whenSubscriptionAddedEventsAreForwarded() {\n        // Given\n        let task = SimpleTask<Int, MyError>(starter: {\n            $0.send(progress: TaskProgress(completed: 1, total: 2))\n            $0.send(value: 1)\n            $0.send(progress: TaskProgress(completed: 2, total: 2))\n            $0.send(value: 2, isCompleted: true)\n        })\n\n        // When\n        var recordedEvents = [AsyncTask<Int, MyError>.Event]()\n        _ = task.subscribe { event in\n            recordedEvents.append(event)\n        }\n\n        // Then\n        #expect(recordedEvents == [\n            .progress(TaskProgress(completed: 1, total: 2)),\n            .value(1, isCompleted: false),\n            .progress(TaskProgress(completed: 2, total: 2)),\n            .value(2, isCompleted: true)\n        ])\n    }\n\n    @Test func bothSubscriptionsReceiveEvents() {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n\n        // When there are two subscriptions\n        var eventCount = 0\n\n        _ = task.subscribe { event in\n            #expect(event == .value(1, isCompleted: false))\n            eventCount += 1 }\n        _ = task.subscribe { event in\n            #expect(event == .value(1, isCompleted: false))\n            eventCount += 1\n        }\n\n        task.send(value: 1)\n\n        // Then\n        #expect(eventCount == 2)\n    }\n\n    @Test func cantSubscribeToAlreadyCancelledTask() async {\n        // Given\n        let task = SimpleTask<Int, MyError>(starter: { _ in })\n        let subscription = task.subscribe { _ in }\n\n        // When\n        subscription?.unsubscribe()\n        await Task.yield()\n\n        // Then\n        #expect(task.subscribe { _ in } == nil)\n    }\n\n    @Test func cantSubscribeToAlreadySucceededTask() {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        _ = task.subscribe { _ in }\n\n        // When\n        task.send(value: 1, isCompleted: true)\n\n        // Then\n        #expect(task.subscribe { _ in } == nil)\n    }\n\n    @Test func cantSubscribeToAlreadyFailedTasks() {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        _ = task.subscribe { _ in }\n\n        // When\n        task.send(error: .init(raw: \"1\"))\n\n        // Then\n        #expect(task.subscribe { _ in } == nil)\n    }\n\n    @Test func subscribeToTaskWithSynchronousCompletionReturnsNil() async {\n        // Given\n        let task = SimpleTask<Int, MyError> { (task) in\n            task.send(value: 0, isCompleted: true)\n        }\n\n        // When\n        await confirmation { confirm in\n            let subscription = task.subscribe { _ in\n                confirm()\n            }\n\n            // Then\n            #expect(subscription == nil)\n        }\n    }\n\n    // MARK: - Unsubscribe\n\n    @Test func whenSubscriptionIsRemovedNoEventsAreSent() async {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        var recordedEvents = [AsyncTask<Int, MyError>.Event]()\n        let subscription = task.subscribe { recordedEvents.append($0) }\n\n        // When\n        subscription?.unsubscribe()\n        await Task.yield()\n        task.send(value: 1)\n\n        // Then\n        #expect(recordedEvents.isEmpty, \"Expect no events to be received by observer after subscription is removed\")\n    }\n\n    @Test func whenSubscriptionIsRemovedTaskBecomesDisposed() async {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        let subscription = task.subscribe { _ in }\n\n        // When\n        subscription?.unsubscribe()\n        await Task.yield()\n\n        // Then\n        #expect(task.isDisposed, \"Expect task to be marked as disposed\")\n    }\n\n    @Test func whenSubscriptionIsRemovedOnCancelIsCalled() async {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        let subscription = task.subscribe { _ in }\n\n        var onCancelledIsCalled = false\n        task.onCancelled = {\n            onCancelledIsCalled = true\n        }\n\n        // When\n        subscription?.unsubscribe()\n        await Task.yield()\n\n        // Then\n        #expect(onCancelledIsCalled)\n    }\n\n    @Test func whenSubscriptionIsRemovedOperationIsCancelled() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let subscription = task.subscribe { _ in }\n        #expect(!operation.isCancelled)\n\n        // When\n        subscription?.unsubscribe()\n        await Task.yield()\n\n        // Then\n        #expect(operation.isCancelled)\n    }\n\n    @Test func whenSubscriptionIsRemovedDependencyIsCancelled() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let dependency = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let task = SimpleTask<Int, MyError>(starter: { $0.dependency = dependency.subscribe { _ in } })\n        let subscription = task.subscribe { _ in }\n        #expect(!operation.isCancelled)\n\n        // When\n        await waitForCancellation(of: operation) {\n            subscription?.unsubscribe()\n        }\n\n        // Then\n        #expect(operation.isCancelled)\n    }\n\n    @Test func whenOneOfTwoSubscriptionsAreRemovedTaskNotCancelled() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let subscription1 = task.subscribe { _ in }\n        _ = task.subscribe { _ in }\n\n        // When\n        subscription1?.unsubscribe()\n        await Task.yield()\n\n        // Then\n        #expect(!operation.isCancelled)\n    }\n\n    @Test func whenTwoOfTwoSubscriptionsAreRemovedTaskIsCancelled() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let subscription1 = task.subscribe { _ in }\n        let subscription2 = task.subscribe { _ in }\n\n        // When\n        subscription1?.unsubscribe()\n        subscription2?.unsubscribe()\n        await Task.yield()\n\n        // Then\n        #expect(operation.isCancelled)\n    }\n\n    // MARK: - Priority\n\n    @Test func whenPriorityIsUpdatedOperationPriorityAlsoUpdated() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let subscription = task.subscribe { _ in }\n\n        // When\n        subscription?.setPriority(.high)\n        await Task.yield()\n\n        // Then\n        #expect(operation.priority == .high)\n    }\n\n    @Test func whenTaskChangesOperationPriorityUpdated() async {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        let subscription = task.subscribe { _ in }\n\n        // When\n        subscription?.setPriority(.high)\n        await Task.yield()\n        let operation = TaskQueue.Operation()\n        task.operation = operation\n\n        // Then\n        #expect(operation.priority == .high)\n    }\n\n    @Test func thatPriorityCanBeLowered() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let subscription = task.subscribe { _ in }\n\n        // When\n        subscription?.setPriority(.low)\n        await Task.yield()\n\n        // Then\n        #expect(operation.priority == .low)\n    }\n\n    @Test func thatPriorityEqualMaximumPriorityOfAllSubscriptions() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let subscription1 = task.subscribe { _ in }\n        let subscription2 = task.subscribe { _ in }\n\n        // When\n        subscription1?.setPriority(.low)\n        subscription2?.setPriority(.high)\n        await Task.yield()\n\n        // Then\n        #expect(operation.priority == .high)\n    }\n\n    @Test func whenSubscriptionIsRemovedPriorityIsUpdated() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let subscription1 = task.subscribe { _ in }\n        let subscription2 = task.subscribe { _ in }\n\n        subscription1?.setPriority(.low)\n        subscription2?.setPriority(.high)\n        await Task.yield()\n\n        // When\n        subscription2?.unsubscribe()\n        await Task.yield()\n\n        // Then\n        #expect(operation.priority == .low)\n    }\n\n    @Test func whenSubscriptionLowersPriorityButExistingSubscriptionHasHigherPriority() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let subscription1 = task.subscribe { _ in }\n        let subscription2 = task.subscribe { _ in }\n\n        // When\n        subscription2?.setPriority(.high)\n        subscription1?.setPriority(.low)\n        await Task.yield()\n\n        // Then order of updating sub\n        #expect(operation.priority == .high)\n    }\n\n    @Test func priorityOfDependencyUpdated() async {\n        // Given\n        let operation = TaskQueue.Operation()\n        let dependency = SimpleTask<Int, MyError>(starter: { $0.operation = operation })\n        let task = SimpleTask<Int, MyError>(starter: { $0.dependency = dependency.subscribe { _ in } })\n        let subscription = task.subscribe { _ in }\n\n        // When\n        await waitForPriorityChange(of: operation, to: .high) {\n            subscription?.setPriority(.high)\n        }\n\n        // Then\n        #expect(operation.priority == .high)\n    }\n\n    // MARK: - Dispose\n\n    @Test func executingTaskIsntDisposed() {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        var isDisposeCalled = false\n        task.onDisposed = { isDisposeCalled = true }\n        _ = task.subscribe { _ in }\n\n        // When\n        task.send(value: 1) // Casually sending value\n\n        // Then\n        #expect(!isDisposeCalled)\n        #expect(!task.isDisposed)\n    }\n\n    @Test func thatTaskIsDisposedWhenCancelled() async {\n        // Given\n        let task = SimpleTask<Int, MyError>(starter: { _ in })\n        var isDisposeCalled = false\n        task.onDisposed = { isDisposeCalled = true }\n        let subscription = task.subscribe { _ in }\n\n        // When\n        subscription?.unsubscribe()\n        await Task.yield()\n\n        // Then\n        #expect(isDisposeCalled)\n        #expect(task.isDisposed)\n    }\n\n    @Test func thatTaskIsDisposedWhenCompletedWithSuccess() {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        var isDisposeCalled = false\n        task.onDisposed = { isDisposeCalled = true }\n        _ = task.subscribe { _ in }\n\n        // When\n        task.send(value: 1, isCompleted: true)\n\n        // Then\n        #expect(isDisposeCalled)\n        #expect(task.isDisposed)\n    }\n\n    @Test func thatTaskIsDisposedWhenCompletedWithFailure() {\n        // Given\n        let task = AsyncTask<Int, MyError>()\n        var isDisposeCalled = false\n        task.onDisposed = { isDisposeCalled = true }\n        _ = task.subscribe { _ in }\n\n        // When\n        task.send(error: .init(raw: \"1\"))\n\n        // Then\n        #expect(isDisposeCalled)\n        #expect(task.isDisposed)\n    }\n}\n\n// MARK: - Helpers\n\nprivate struct MyError: Equatable {\n    let raw: String\n}\n\n@ImagePipelineActor\nprivate final class SimpleTask<T, E>: AsyncTask<T, E>, @unchecked Sendable {\n    private var starter: ((SimpleTask) -> Void)?\n\n    /// Initializes the task with the `starter`.\n    /// - parameter starter: The closure which gets called as soon as the first\n    /// subscription is added to the task. Only gets called once and is immediately\n    /// deallocated after it is called.\n    init(starter: ((SimpleTask) -> Void)? = nil) {\n        self.starter = starter\n    }\n\n    override func start() {\n        starter?(self)\n        starter = nil\n    }\n}\n\n@ImagePipelineActor\nextension AsyncTask {\n    func subscribe(priority: TaskPriority = .normal, _ observer: @escaping (Event) -> Void) -> TaskSubscription? {\n        publisher.subscribe(priority: priority, subscriber: \"\" as AnyObject, observer)\n    }\n}\n"
  },
  {
    "path": "Tests/NukeThreadSafetyTests/ThreadSafetyTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\n@testable import Nuke\nimport Testing\nimport Foundation\n\n#if os(iOS) || os(tvOS) || os(visionOS)\nimport UIKit\n#endif\n\n@Suite(.timeLimit(.minutes(2)))\nstruct ThreadSafetyTests {\n    @Test func imagePipelineThreadSafety() async {\n        let dataLoader = MockDataLoader()\n        let pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n        }\n\n        await performPipelineThreadSafetyTest(pipeline)\n\n        _ = (dataLoader, pipeline)\n    }\n\n    @Test func sharingConfigurationBetweenPipelines() async { // Especially operation queues\n        var configuration = ImagePipeline.Configuration()\n        configuration.dataLoader = MockDataLoader()\n        configuration.imageCache = nil\n\n        let pipelines = [\n            ImagePipeline(configuration: configuration),\n            ImagePipeline(configuration: configuration),\n            ImagePipeline(configuration: configuration)\n        ]\n\n        for pipeline in pipelines {\n            await performPipelineThreadSafetyTest(pipeline)\n        }\n\n        _ = pipelines\n    }\n\n    @Test func prefetcherThreadSafety() {\n        let pipeline = ImagePipeline {\n            $0.dataLoader = MockDataLoader()\n            $0.imageCache = nil\n        }\n\n        let prefetcher = ImagePrefetcher(pipeline: pipeline)\n\n        @Sendable func makeRequests() -> [ImageRequest] {\n            return (0...Int.random(in: 0..<30)).map { _ in\n                return ImageRequest(url: URL(string: \"http://\\(Int.random(in: 0..<15))\")!)\n            }\n        }\n        let queue = OperationQueue()\n        queue.maxConcurrentOperationCount = 4\n        for _ in 0...300 {\n            queue.addOperation {\n                prefetcher.stopPrefetching(with: makeRequests())\n                prefetcher.startPrefetching(with: makeRequests())\n            }\n        }\n        queue.waitUntilAllOperationsAreFinished()\n    }\n\n    @Test func imageCacheThreadSafety() {\n        let cache = ImageCache()\n\n        @Sendable func rnd_cost() -> Int {\n            return (2 + Int.random(in: 0..<20)) * 1024 * 1024\n        }\n\n        var ops = [@Sendable () -> Void]()\n\n        for _ in 0..<10 { // those ops happen more frequently\n            ops += [\n                { cache[_request(index: Int.random(in: 0..<10))] = ImageContainer(image: Test.image) },\n                { cache[_request(index: Int.random(in: 0..<10))] = nil },\n                { let _ = cache[_request(index: Int.random(in: 0..<10))] }\n            ]\n        }\n\n        ops += [\n            { cache.trim(toCost: rnd_cost()) },\n            { cache.removeAll() }\n        ]\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n        ops.append {\n            NotificationCenter.default.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil)\n        }\n        ops.append {\n            NotificationCenter.default.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil)\n        }\n#endif\n\n        let finalOps = ops\n        let queue = OperationQueue()\n        queue.maxConcurrentOperationCount = 5\n\n        for _ in 0..<10000 {\n            queue.addOperation {\n                finalOps.randomElement()?()\n            }\n        }\n\n        queue.waitUntilAllOperationsAreFinished()\n    }\n\n    // MARK: - DataCache\n\n    @Test func dataCacheThreadSafety() throws {\n        let cache = try DataCache(name: UUID().uuidString, filenameGenerator: { $0 })\n\n        let data = Data(repeating: 1, count: 256 * 1024)\n\n        for idx in 0..<500 {\n            cache[\"\\(idx)\"] = data\n        }\n        cache.flush()\n\n        let queue = OperationQueue()\n        queue.maxConcurrentOperationCount = 5\n\n        for _ in 0..<5 {\n            for idx in 0..<500 {\n                queue.addOperation {\n                    _ = cache[\"\\(idx)\"]\n                }\n                queue.addOperation {\n                    cache[\"\\(idx)\"] = data\n                    cache.flush()\n                }\n            }\n        }\n        queue.waitUntilAllOperationsAreFinished()\n    }\n\n    @Test func dataCacheMultipleThreadAccess() async throws {\n        let cache = try DataCache(name: UUID().uuidString)\n\n        let aURL = URL(string: \"https://example.com/image-01-small.jpeg\")!\n        let imageData = Test.data(name: \"fixture\", extension: \"jpeg\")\n\n        let expectation = TestExpectation()\n\n        let pipeline = ImagePipeline {\n            $0.dataCache = cache\n            $0.dataLoader = MockDataLoader()\n        }\n        pipeline.cache.storeCachedData(imageData, for: ImageRequest(url: aURL))\n        pipeline.loadImage(with: aURL) { result in\n            switch result {\n            case .success(let response):\n                if response.cacheType == .memory || response.cacheType == .disk {\n                    expectation.fulfill()\n                } else {\n                    Issue.record(\"didn't load that just cached image data: \\(response)\")\n                }\n            case .failure:\n                Issue.record(\"didn't load that just cached image data\")\n            }\n        }\n\n        await expectation.wait()\n\n        try? FileManager.default.removeItem(at: cache.path)\n    }\n}\n\n@Suite(.timeLimit(.minutes(2)))\nstruct RandomizedTests {\n    @Test func imagePipeline() async {\n        let dataLoader = MockDataLoader()\n        let pipeline = ImagePipeline {\n            $0.dataLoader = dataLoader\n            $0.imageCache = nil\n            $0.isRateLimiterEnabled = false\n        }\n\n        let queue = OperationQueue()\n        queue.maxConcurrentOperationCount = 8\n\n        @Sendable func every(_ count: Int) -> Bool {\n            return Int.random(in: 0..<Int.max) % count == 0\n        }\n\n        @Sendable func randomRequest() -> ImageRequest {\n            let url = URL(string: \"\\(Test.url)/\\(Int.random(in: 0..<50))\")!\n            var request = ImageRequest(url: url)\n            request.priority = every(2) ? .high : .normal\n            if every(3) {\n                let size = every(2) ? CGSize(width: 40, height: 40) : CGSize(width: 60, height: 60)\n                request.processors = [ImageProcessors.Resize(size: size, contentMode: .aspectFit)]\n            }\n            return request\n        }\n\n        @Sendable func randomSleep() {\n            let ms = TimeInterval.random(in: 0 ..< 100) / 1000.0\n            Thread.sleep(forTimeInterval: ms)\n        }\n\n        let group = DispatchGroup()\n\n        for _ in 0..<1000 {\n            group.enter()\n            queue.addOperation {\n                randomSleep()\n\n                let request = randomRequest()\n                let shouldCancel = every(3)\n\n                let task = pipeline.loadImage(with: request) { _ in\n                    if !shouldCancel {\n                        group.leave()\n                    }\n                }\n\n                if shouldCancel {\n                    queue.addOperation {\n                        randomSleep()\n                        task.cancel()\n                        group.leave()\n                    }\n                }\n\n                if every(10) {\n                    queue.addOperation {\n                        randomSleep()\n                        let priority: ImageRequest.Priority = every(2) ? .veryHigh : .veryLow\n                        task.priority = priority\n                    }\n                }\n            }\n        }\n\n        await withCheckedContinuation { continuation in\n            group.notify(queue: .global()) {\n                continuation.resume()\n            }\n        }\n\n        _ = pipeline\n    }\n}\n\nprivate func performPipelineThreadSafetyTest(_ pipeline: ImagePipeline) async {\n    let group = DispatchGroup()\n    let queue = OperationQueue()\n    queue.maxConcurrentOperationCount = 16\n\n    for _ in 0..<1000 {\n        group.enter()\n        queue.addOperation {\n            let url = URL(fileURLWithPath: \"\\(Int.random(in: 0..<30))\")\n            let request = ImageRequest(url: url)\n            let shouldCancel = Int.random(in: 0..<3) == 0\n\n            let task = pipeline.loadImage(with: request) { _ in\n                if shouldCancel {\n                    // do nothing, we don't expect completion on cancel\n                } else {\n                    group.leave()\n                }\n            }\n\n            if shouldCancel {\n                task.cancel()\n                group.leave()\n            }\n        }\n    }\n\n    await withCheckedContinuation { continuation in\n        group.notify(queue: .global()) {\n            continuation.resume()\n        }\n    }\n}\n\nprivate func _request(index: Int) -> ImageRequest {\n    return ImageRequest(url: URL(string: \"http://example.com/img\\(index)\")!)\n}\n"
  },
  {
    "path": "Tests/NukeUITests/FetchImageTests.swift",
    "content": "// The MIT License (MIT)\n//\n// Copyright (c) 2015-2026 Alexander Grebenyuk (github.com/kean).\n\nimport Testing\nimport Foundation\nimport Combine\n@testable import Nuke\n@testable import NukeUI\n\n@Suite(.timeLimit(.minutes(2))) @MainActor\nstruct FetchImageTests {\n    let dataLoader: MockDataLoader\n    let observer: ImagePipelineObserver\n    let pipeline: ImagePipeline\n    var image: FetchImage\n\n    init() {\n        let dataLoader = MockDataLoader()\n        let observer = ImagePipelineObserver()\n        self.dataLoader = dataLoader\n        self.observer = observer\n        self.pipeline = ImagePipeline(delegate: observer) {\n            $0.dataLoader = dataLoader\n            $0.imageCache = MockImageCache()\n            $0.dataCache = MockDataCache()\n        }\n        self.image = FetchImage()\n        self.image.pipeline = pipeline\n    }\n\n    @Test func imageLoaded() async throws {\n        let expectation = TestExpectation()\n        image.onCompletion = { _ in expectation.fulfill() }\n        image.load(Test.request)\n        await expectation.wait()\n\n        let result = try #require(image.result)\n        #expect(result.isSuccess)\n        #expect(image.image != nil)\n    }\n\n    @Test func isLoadingUpdated() async {\n        #expect(!image.isLoading)\n\n        let expectation = TestExpectation()\n        image.onCompletion = { _ in expectation.fulfill() }\n        image.load(Test.request)\n        #expect(image.isLoading)\n\n        await expectation.wait()\n        #expect(!image.isLoading)\n    }\n\n    @Test func memoryCacheLookup() throws {\n        pipeline.cache[Test.request] = Test.container\n\n        image.load(Test.request)\n\n        let result = try #require(image.result)\n        #expect(result.isSuccess)\n        let response = try #require(result.value)\n        #expect(response.cacheType == .memory)\n        #expect(image.image != nil)\n    }\n\n    @Test func priorityUpdated() async throws {\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n\n        let expectation = await TestExpectation(queue: queue, count: 1)\n\n        image.priority = .high\n        image.load(Test.request)\n\n        await expectation.wait()\n\n        let operation = try #require(expectation.operations.first)\n        let priority = await operation.priority\n        #expect(priority == .high)\n    }\n\n    @Test func priorityUpdatedDynamically() async throws {\n        let queue = pipeline.configuration.dataLoadingQueue\n        queue.isSuspended = true\n\n        let expectation = await TestExpectation(queue: queue, count: 1)\n        image.load(Test.request)\n        await expectation.wait()\n\n        let operation = try #require(expectation.operations.first)\n\n        await queue.waitForPriorityChange(of: operation, to: .high) { @Sendable in\n            Task { @MainActor in\n                image.priority = .high\n            }\n        }\n    }\n\n    // MARK: - Publisher\n\n    @Test func publisherImageLoaded() async throws {\n        let expectation = TestExpectation()\n        let cancellable = image.$result.dropFirst().sink { _ in\n            expectation.fulfill()\n        }\n\n        image.load(pipeline.imagePublisher(with: Test.request))\n        await expectation.wait()\n\n        let result = try #require(image.result)\n        #expect(result.isSuccess)\n        #expect(image.image != nil)\n        withExtendedLifetime(cancellable) {}\n    }\n\n    @Test func publisherIsLoadingUpdated() async {\n        #expect(!image.isLoading)\n\n        let expectation = TestExpectation()\n        let cancellable = image.$result.dropFirst().sink { _ in\n            expectation.fulfill()\n        }\n\n        image.load(pipeline.imagePublisher(with: Test.request))\n        #expect(image.isLoading)\n\n        await expectation.wait()\n        #expect(!image.isLoading)\n        withExtendedLifetime(cancellable) {}\n    }\n\n    @Test func publisherMemoryCacheLookup() throws {\n        pipeline.cache[Test.request] = Test.container\n\n        image.load(pipeline.imagePublisher(with: Test.request))\n\n        let result = try #require(image.result)\n        #expect(result.isSuccess)\n        let response = try #require(result.value)\n        #expect(response.cacheType == .memory)\n        #expect(image.image != nil)\n    }\n\n    // MARK: - Cancellation\n\n    @Test func requestCancelledWhenTargetGetsDeallocated() async {\n        dataLoader.isSuspended = true\n\n        var localImage: FetchImage? = FetchImage()\n        localImage!.pipeline = pipeline\n\n        let startExpectation = TestExpectation(notification: ImagePipelineObserver.didStartTask, object: observer)\n        localImage!.load(pipeline.imagePublisher(with: Test.request))\n        await startExpectation.wait()\n\n        await notification(ImagePipelineObserver.didCancelTask, object: observer) {\n            localImage = nil\n        }\n    }\n}\n"
  }
]