[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: onevcat\nopen_collective: kingfisher\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\nIssue is for reporting a problem of Kingfisher. To ask a question or discuss about usage, please go to the Discussion (https://github.com/onevcat/Kingfisher/discussions).\n-->\n\n### Check List\n\nThanks for considering to open an issue. Before you submit your issue, please confirm these boxes are checked.\n\n- [ ] I have read the [wiki page](https://github.com/onevcat/Kingfisher/wiki) and [cheat sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet), but there is no information I need.\n- [ ] I have searched in [existing issues](https://github.com/onevcat/Kingfisher/issues?utf8=✓&q=is%3Aissue), but did not find a same one.\n- [ ] I want to report a problem instead of asking a question. It'd better to use [kingfisher tag in Stack Overflow](http://stackoverflow.com/questions/tagged/kingfisher) to ask a question.\n\n### Issue Description\n\n#### What\n\n[Tell us about the issue]\n\n#### Reproduce\n\n[The steps to reproduce this issue. What is the url you were trying to load, where did you put your code, etc.]\n\n#### Other Comment\n\n[Add anything else here]\n\n\n<!-- Love Kingfisher? Please consider supporting our collective:\n👉  https://opencollective.com/Kingfisher/donate -->\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: build\n\ndefaults:\n  run:\n    shell: bash -leo pipefail {0}\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    types: [opened, synchronize, reopened]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-framework:\n    name: build (Xcode ${{ matrix.xcode }}, ${{ matrix.label }})\n    runs-on: self-hosted\n    strategy:\n      matrix:\n        include:\n          - xcode: '16.2'\n            destination: 'macOS'\n            label: 'macOS'\n          - xcode: '16.2'\n            destination: 'iOS Simulator,name=iPhone 16,OS=18.2'\n            label: 'iOS 18.2'\n          - xcode: '16.2'\n            destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=18.2'\n            label: 'tvOS 18.2'\n          - xcode: '16.2'\n            destination: 'watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.2'\n            label: 'watchOS 11.2'\n\n          - xcode: '16.3'\n            destination: 'macOS'\n            label: 'macOS'\n          - xcode: '16.3'\n            destination: 'iOS Simulator,name=iPhone 16,OS=18.4'\n            label: 'iOS 18.4'\n          - xcode: '16.3'\n            destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=18.4'\n            label: 'tvOS 18.4'\n          - xcode: '16.3'\n            destination: 'watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.4'\n            label: 'watchOS 11.4'\n\n          - xcode: '26.0.1'\n            destination: 'macOS'\n            label: 'macOS'\n          - xcode: '26.0.1'\n            destination: 'iOS Simulator,name=iPhone 17,OS=26.0.1'\n            label: 'iOS 26.0.1'\n          - xcode: '26.0.1'\n            destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.0'\n            label: 'tvOS 26.0'\n          - xcode: '26.0.1'\n            destination: 'watchOS Simulator,name=Apple Watch Series 11 (42mm),OS=26.0'\n            label: 'watchOS 26.0'\n\n          - xcode: '26.1.1'\n            destination: 'macOS'\n            label: 'macOS'\n          - xcode: '26.1.1'\n            destination: 'iOS Simulator,name=iPhone 17,OS=26.1'\n            label: 'iOS 26.1'\n          - xcode: '26.1.1'\n            destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.1'\n            label: 'tvOS 26.1'\n          - xcode: '26.1.1'\n            destination: 'watchOS Simulator,name=Apple Watch Series 11 (42mm),OS=26.1'\n            label: 'watchOS 26.1'\n\n          - xcode: '26.2'\n            destination: 'macOS'\n            label: 'macOS'\n          - xcode: '26.2'\n            destination: 'iOS Simulator,name=iPhone 17,OS=26.2'\n            label: 'iOS 26.2'\n          - xcode: '26.2'\n            destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.2'\n            label: 'tvOS 26.2'\n          - xcode: '26.2'\n            destination: 'watchOS Simulator,name=Apple Watch Series 11 (42mm),OS=26.2'\n            label: 'watchOS 26.2'\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install Gems\n        run: bundle install\n      - name: Build framework\n        env:\n          DESTINATION: platform=${{ matrix.destination }}\n          XCODE_VERSION: ${{ matrix.xcode }}\n          FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: '60'\n          FASTLANE_XCODEBUILD_SETTINGS_RETRIES: '4'\n        run: bundle exec fastlane build_ci\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: test\n\ndefaults:\n  run:\n    shell: bash -leo pipefail {0}\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    types: [opened, synchronize, reopened]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  run-tests:\n    name: test (Xcode ${{ matrix.xcode }}, ${{ matrix.label }})\n    runs-on: self-hosted\n    strategy:\n      matrix:\n        include:\n          - xcode: '16.4'\n            destination: 'macOS'\n            label: 'macOS'\n          - xcode: '16.4'\n            destination: 'iOS Simulator,name=iPhone 16,OS=18.5'\n            label: 'iOS 18.5'\n          - xcode: '16.4'\n            destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=18.5'\n            label: 'tvOS 18.5'\n          - xcode: '16.4'\n            destination: 'watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.5'\n            label: 'watchOS 11.5'\n\n          - xcode: '26.3'\n            destination: 'macOS'\n            label: 'macOS'\n          - xcode: '26.3'\n            destination: 'iOS Simulator,name=iPhone 17,OS=26.2'\n            label: 'iOS 26.2'\n          - xcode: '26.3'\n            destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.2'\n            label: 'tvOS 26.2'\n          - xcode: '26.3'\n            destination: 'watchOS Simulator,name=Apple Watch Series 11 (42mm),OS=26.2'\n            label: 'watchOS 26.2'\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install Gems\n        run: bundle install\n      - name: Run tests\n        env:\n          DESTINATION: platform=${{ matrix.destination }}\n          XCODE_VERSION: ${{ matrix.xcode }}\n          FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: '60'\n          FASTLANE_XCODEBUILD_SETTINGS_RETRIES: '4'\n        run: bundle exec fastlane test_ci\n"
  },
  {
    "path": ".gitignore",
    "content": "# Created by https://www.gitignore.io\n\n# Xcode\nbuild/\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\nxcuserdata\n*.xccheckout\n*.moved-aside\nDerivedData\n*.xcuserstate\n*.hmap\n*.ipa\n.swiftpm\n\n# AppCode\n.idea\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# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control\n#\n# Pods/\n\n# Carthage\n#\n# Add this line if you want to avoid checking in source code from Carthage dependencies.\n# Carthage/Checkouts\n\nCarthage/Build\nKingfisher.framework.zip\n\n# macOS\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\nimages/logo.sketch\n\n# fastlane specific\nfastlane/report.xml\n\n# deliver temporary files\nfastlane/Preview.html\n\n# snapshot generated screenshots\nfastlane/screenshots/**/*.png\nfastlane/screenshots/screenshots.html\n\n# scan temporary files\nfastlane/test_output\ntest_output\nfastlane/.env\npre-change.yml\n.build\nfastlane/README.md\n/Kingfisher-TestImages\n\n.bundle/\nvendor/\n.vscode/settings.json\n\n# Claude local settings\n.claude/\n.claude/settings.local.json\n.claude/CLAUDE.local.md\n.claude/*.local.*\n.codex/\n\n# xcode-build-server files\nbuildServer.json\n.compile\n"
  },
  {
    "path": ".ruby-version",
    "content": "3.3.6\n"
  },
  {
    "path": ".spi.yml",
    "content": "version: 1\nbuilder:\n  configs:\n    - documentation_targets: [Kingfisher]"
  },
  {
    "path": "AGENTS.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Documentation\n\nYou can find the complete documentation for this project in the `docs/` directory. It describes the architecture, build system, development practices and more in detail the the project.\n\nThere is also a minimal, LLM-friendly version of the documentation in `README-LLM.md`. You can refer to the file if you need a quick overview of the project without going through the entire documentation.\n\n## Build and Development Commands\n\n### Primary Build System\nThis project uses **Fastlane** for primary build automation. Key commands:\n\n```bash\n# Install dependencies\nbundle install\n\n# Run all tests across platforms (iOS, macOS, tvOS, watchOS)\nbundle exec fastlane tests\n\n# Run specific platform tests\nbundle exec fastlane test destination:\"platform=iOS Simulator,name=iPhone 16\"\n\n# Build for specific platform\nbundle exec fastlane build destination:\"platform=iOS Simulator,name=iPhone 16\"\n\n# Lint CocoaPods spec and Swift Package Manager\nbundle exec fastlane lint\n```\n\n### Release Process\n```bash\n# Full release workflow (tests, linting, versioning, GitHub release, CocoaPods push)\nbundle exec fastlane release version:X.X.X\n```\n\n## Architecture Overview\n\n@docs/architecture.md\n\nKingfisher is a modular image loading and caching library with clear separation of concerns:\n\n### Core Components Flow\n1. **KingfisherManager** (`Sources/General/KingfisherManager.swift`) - Central coordinator\n2. **ImageDownloader** (`Sources/Networking/ImageDownloader.swift`) - Network layer\n3. **ImageCache** (`Sources/Cache/ImageCache.swift`) - Dual-layer caching (memory + disk)\n4. **ImageProcessor** (`Sources/Image/ImageProcessor.swift`) - Image transformation pipeline\n\n### Key Architectural Patterns\n- **Protocol-oriented design** with `KingfisherCompatible` protocol\n- **Namespace wrapper pattern** - All functionality accessed via `.kf` property\n- **Builder pattern** - `KF.url()...` method chaining\n- **Options pattern** - `KingfisherOptionsInfo` for configuration\n\n### Module Structure\n```\nSources/\n├── General/           # Core managers, options, data providers\n├── Networking/        # Download, prefetch, session management  \n├── Cache/            # Multi-layer caching system\n├── Image/            # Processing, filters, formats, transitions\n├── Extensions/       # UIKit/AppKit/SwiftUI integration\n├── SwiftUI/         # SwiftUI-specific components\n├── Utility/         # Helper utilities and extensions\n└── Views/           # Custom UI components\n```\n\n### Integration Points\n- **UIKit**: Extensions for `UIImageView`, `UIButton` via `.kf` namespace\n- **SwiftUI**: `KFImage` and `KFAnimatedImage` components\n- **Cross-platform**: Extensive conditional compilation for iOS/macOS/tvOS/watchOS/visionOS\n\n## Platform Support\n- **UIKit/AppKit**: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+\n- **SwiftUI**: iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+\n- **Swift**: 5.9+ (with Swift 6 strict concurrency support)\n\n## Testing\n\n### Test Structure\n- **Location**: `Tests/KingfisherTests/`\n- **Framework**: XCTest with custom `KingfisherTestHelper`\n- **Network mocking**: Uses Nocilla dependency for HTTP stubbing\n- **Test assets**: `dancing-banana.gif`, `single-frame.gif`\n\n### Running Tests\n```bash\n# All platforms (preferred)\nbundle exec fastlane tests\n\n# Single platform via destination\nbundle exec fastlane test destination:\"platform=iOS Simulator,name=iPhone 15\"\n```\n\n## Documentation System\n\nProvides a DocC-based documentation for framework users:\n\n- **DocC integration** with comprehensive tutorials and API docs\n- **Location**: `Sources/Documentation.docc/`\n- **Online**: Swift Package Index hosted documentation\n- **Tutorials**: Both UIKit and SwiftUI getting started guides available"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\n-----\n\n## [8.8.0 - Background Relief](https://github.com/onevcat/Kingfisher/releases/tag/8.8.0) (2026-03-04)\n\n#### Add\n* Add `AnimatedImageView.purgeFrames(keepCurrentFrame:)` and opt-in `purgeFramesOnBackground` to reduce animated frame memory while app is backgrounded. [#2482](https://github.com/onevcat/Kingfisher/pull/2482) [#2445](https://github.com/onevcat/Kingfisher/issues/2445) @onevcat @Ceylo\n* Add `KFAnimatedImage.purgeFramesOnBackground(_:)` to expose background frame purging in SwiftUI. [#2484](https://github.com/onevcat/Kingfisher/pull/2484) @WZBbiao\n\n#### Fix\n* Fix missing completion callback when original cache reports cached but returns no image. [#2481](https://github.com/onevcat/Kingfisher/pull/2481) [#2472](https://github.com/onevcat/Kingfisher/issues/2472) @onevcat @hotngui\n* Fix `AnimatedImageView` deinit compatibility for older Swift 6 toolchains without isolated deinit support. [#2485](https://github.com/onevcat/Kingfisher/pull/2485) @onevcat\n* Apply `retryStrategy` in `ImagePrefetcher` load path so retry options also work during prefetching. [#2487](https://github.com/onevcat/Kingfisher/pull/2487) @TastyHeadphones\n* Fix non-Sendable `RetryDecision` capture warning in ImagePrefetcher retry flow under Swift 6 concurrency checks. [#2488](https://github.com/onevcat/Kingfisher/pull/2488) @onevcat\n\n---\n\n## [8.7.0 - Async Expedition](https://github.com/onevcat/Kingfisher/releases/tag/8.7.0) (2026-02-18)\n\n#### Add\n* Add opt-in async cache type check API `imageCachedTypeAsync` to avoid synchronous disk access on the calling thread. [#2480](https://github.com/onevcat/Kingfisher/pull/2480) [#2323](https://github.com/onevcat/Kingfisher/issues/2323) @onevcat @jotai-coder\n* Add optional `cacheKey` parameter for `PhotosPickerItemImageDataProvider` and `PHPickerResultImageDataProvider` for better cache control. [#2479](https://github.com/onevcat/Kingfisher/pull/2479) @onevcat\n* Support using an `OperationQueue` or equivalent interface in `CallbackQueue` for custom processing queue control. [#2474](https://github.com/onevcat/Kingfisher/pull/2474) @onevcat\n\n#### Fix\n* {\"Fix\"=>\"stabilize cacheKey for PhotosPicker/PHPicker data providers. Now uses stored property with picker-provided identifier or falls back to a per-instance UUID. [#2478](https://github.com/onevcat/Kingfisher/pull/2478) @onevcat\"}\n* Fix a race condition crash in `ImagePrefetcher.handleComplete` when iterating sources during concurrent mutation. [#2465](https://github.com/onevcat/Kingfisher/pull/2465) @erichoracek\n* Fix GIF disk cache losing animation when original data is missing. Now `DefaultCacheSerializer` prefers embedded GIF bytes over re-encoding to PNG. [#2454](https://github.com/onevcat/Kingfisher/pull/2454) [#2453](https://github.com/onevcat/Kingfisher/issues/2453) @onevcat @rztime\n* Fix a crash when accessing `KingfisherWrapper<UIApplication>.shared` in unit tests. [#2450](https://github.com/onevcat/Kingfisher/pull/2450) @maxchuquimia\n* Call async modifier start callback before resume to ensure proper callback timing. [#2462](https://github.com/onevcat/Kingfisher/pull/2462) @onevcat\n* Remove ActorBox and harden background task cleanup to fix Sendable/main actor issues. [#2459](https://github.com/onevcat/Kingfisher/pull/2459) @onevcat\n* Mark `ImagePrefetcher` callback types as `@Sendable` to fix Swift 6 concurrency warnings.\n* Deprecate SwiftUI `.onFailureImage` modifier in favor of `.onFailureView`. [#2451](https://github.com/onevcat/Kingfisher/pull/2451) [#2449](https://github.com/onevcat/Kingfisher/issues/2449) @onevcat @sagarrai21802\n\n---\n\n## [8.6.2 - High Fidelity](https://github.com/onevcat/Kingfisher/releases/tag/8.6.2) (2025-11-17)\n\n#### Fix\n* Improve macOS graphics context for high bit depth to support rendering 10-bit images. [#2448](https://github.com/onevcat/Kingfisher/pull/2448) [#2447](https://github.com/onevcat/Kingfisher/issues/2447) @onevcat @BobbyRohweder\n\n---\n\n## [8.6.1 - Atomic](https://github.com/onevcat/Kingfisher/releases/tag/8.6.1) (2025-10-27)\n\n#### Fix\n* Fix non-atomic task creation for concurrent same-URL requests to prevent callback loss. [#2444](https://github.com/onevcat/Kingfisher/pull/2444) @darkbrewx\n\n---\n\n## [8.6.0 - Retryfisher](https://github.com/onevcat/Kingfisher/releases/tag/8.6.0) (2025-10-05)\n\n#### Add\n* Add network retry strategy with configurable retry logic for failed downloads. [#2439](https://github.com/onevcat/Kingfisher/pull/2439) @komkovla\n* Add network metrics collection for download tasks with download speed measurement and timestamp handling. [#2416](https://github.com/onevcat/Kingfisher/pull/2416) @darkbrewx\n\n#### Fix\n* Fix crash on setting indicator on macOS 26 by changing default indicator style. [#2442](https://github.com/onevcat/Kingfisher/pull/2442) @onevcat\n* Fix retain cycle in ImageDownloader when transferring network metrics. [#2419](https://github.com/onevcat/Kingfisher/pull/2419) @darkbrewx\n* Upgrade to the latest Xcode recommended settings for improved build configuration. [#2417](https://github.com/onevcat/Kingfisher/pull/2417) @onevcat\n* Update CI script to make it work with Xcode 26. [#2442](https://github.com/onevcat/Kingfisher/pull/2442) @onevcat\n\n---\n\n## [8.5.0 - Transition Dancer](https://github.com/onevcat/Kingfisher/releases/tag/8.5.0) (2025-07-15)\n\n#### Add\n* Add SwiftUI native transition support for KFImage with `loadTransition(_:animation:)` method. [#2410](https://github.com/onevcat/Kingfisher/pull/2410) @darkbrewx @onevcat\n\n#### Fix\n* Fix documentation for `loadDiskFileSynchronously` in SwiftUI components to clarify default synchronous behavior. [#2411](https://github.com/onevcat/Kingfisher/pull/2411) @pinkjuice66 @onevcat\n* Fix BorderImageProcessor.identifier implementation. [#2409](https://github.com/onevcat/Kingfisher/pull/2409) @teameh\n\n---\n\n## [8.4.0 - Failure Fisher](https://github.com/onevcat/Kingfisher/releases/tag/8.4.0) (2025-07-03)\n\n#### Add\n* Add `onFailureView` modifier for custom failure views in SwiftUI. [#2406](https://github.com/onevcat/Kingfisher/pull/2406) @onevcat [#2404](https://github.com/onevcat/Kingfisher/pull/2404) @alobaili [#2082](https://github.com/onevcat/Kingfisher/issues/2082) @brzzdev\n\n#### Fix\n* Fix Sendable warnings in Xcode 26 with stricter concurrency checking. [#2400](https://github.com/onevcat/Kingfisher/pull/2400) @onevcat\n* Fix test timing issue in ImageCacheTests for CI stability. [#2401](https://github.com/onevcat/Kingfisher/pull/2401) @onevcat\n* Optimize CI workflow to avoid duplicate runs on pull requests. [#2402](https://github.com/onevcat/Kingfisher/pull/2402) @onevcat\n\n---\n\n## [8.3.3 - Swift Harmony](https://github.com/onevcat/Kingfisher/releases/tag/8.3.3) (2025-06-22)\n\n#### Add\n* Add Carthage support for both watchOS and iOS platforms [#2399](https://github.com/onevcat/Kingfisher/pull/2399) @wolfcon\n\n#### Fix\n* Fix Swift Task Continuation Misuse issue with Swift 6 compatible solution [#2398](https://github.com/onevcat/Kingfisher/pull/2398) @VladimirHorky @onevcat\n* Fix ThumbnailImageDataProvider image orientation issue [#2396](https://github.com/onevcat/Kingfisher/pull/2396) @gongzhang @onevcat\n* Remove incorrect watchOS available declaration [#2382](https://github.com/onevcat/Kingfisher/pull/2382) @wolfcon @onevcat\n\n---\n\n## [8.3.2 - Tariffisher](https://github.com/onevcat/Kingfisher/releases/tag/8.3.2) (2025-04-10)\n\n#### Fix\n* Memory cache cleanning timer will now be correctly set when the cache configuration is set. [#2376](https://github.com/onevcat/Kingfisher/issues/2376) @erincolkan\n* Add `BUILD_LIBRARY_FOR_DISTRIBUTION` flag to podspec file. Now CocoaPods build can produce stabible module. [#2372](https://github.com/onevcat/Kingfisher/issues/2372) @gquattromani\n* Refactoring on cache file name method in `DiskStorage`. [#2374](https://github.com/onevcat/Kingfisher/issues/2374) @NeoSelf1\n\n---\n\n## [8.3.1 - Potential Cache Deadlock](https://github.com/onevcat/Kingfisher/releases/tag/8.3.1) (2025-03-15)\n\n#### Fix\n* Fix a potential deadlock in disk cache. It might happen on older devices & systems when preparing the cache file list. @onevcat @xbk713 [#2371](https://github.com/onevcat/Kingfisher/pull/2371)\n\n---\n\n## [8.3.0 - Progressive Loading Improvement](https://github.com/onevcat/Kingfisher/releases/tag/8.3.0) (2025-03-04)\n\n#### Add\n* The progressive JPEG loading option is now available for SwiftUI too. You can load a progressive JPEG image with the `progressiveJPEG` modifier in `KFImage`. @onevcat @nikolaydubina @mantoljak [#2366](https://github.com/onevcat/Kingfisher/pull/2366)\n\n#### Fix\n* Solves a memory leak when using progressive JPEG loading. @onevcat @james-app @Adobels [#2368](https://github.com/onevcat/Kingfisher/pull/2368)\n* The filename and the content structure of the prebuilt xcframework zip in the Assets section of the release page have been updated. If your script depends on this file, you may need to adjust it accordingly. See more in [#2361](https://github.com/onevcat/Kingfisher/pull/2361) @olejnjak\n* A wrong `imageNotExisting` was used in KingfisherManager. Now the correct low level error is propagated to caller side. @onevcat @iAllenC @kuzomenskyi [#2336](https://github.com/onevcat/Kingfisher/pull/2336)]\n\n---\n\n## [8.2.0 - Snake Year](https://github.com/onevcat/Kingfisher/releases/tag/8.2.0) (2025-02-05)\n\n#### Add\n* Add a `ThumbnailImageDataProvider` to get a thumbnail image from a URL directly with `CGImageSourceCreateThumbnailAtIndex`. @onevcat [#2349](https://github.com/onevcat/Kingfisher/pull/2349)\n* Add iOS-only XCFramework distribution for smaller package size when only iOS platform is needed. You can download it from the Release page. @onevcat [#2350](https://github.com/onevcat/Kingfisher/pull/2350)\n\n#### Fix\n* Fix a performance issue when referring the same animated image source, which was introduced in 8.1.4. Special thanks to @pNre for the report and @yeatse for the quick fix. [#2357](https://github.com/onevcat/Kingfisher/pull/2357)\n* Fix a compiling issue when building under certain CI environments that triggers a Swift compiler error. @onevcat [#2353](https://github.com/onevcat/Kingfisher/pull/2353)\n\n---\n\n## [8.1.4 - Avoid Recreation](https://github.com/onevcat/Kingfisher/releases/tag/8.1.4) (2025-01-29)\n\n#### Fix\n* Avoid recreating the animated image if the options are the same. This improves the reloading performance. @yeatse [#2347](https://github.com/onevcat/Kingfisher/pull/2347)\n\n---\n\n## [8.1.3 - Failing Size](https://github.com/onevcat/Kingfisher/releases/tag/8.1.3) (2024-12-17)\n\n#### Fix\n* An issue where redrawing a vector image on macOS without specifying the image size could cause an assertion failure. @onevcat @maoxiaoke [#2334](https://github.com/onevcat/Kingfisher/issues/2334)\n\n---\n\n## [8.1.2 - Data Racing](https://github.com/onevcat/Kingfisher/releases/tag/8.1.2) (2024-12-07)\n\n#### Fix\n* Fix a race condition when downloading and reading the image data in session. It should improve the stability. @meisbedi @onevcat [#2327](https://github.com/onevcat/Kingfisher/pull/2327)\n\n---\n\n## [8.1.1 - Clean Completion](https://github.com/onevcat/Kingfisher/releases/tag/8.1.1) (2024-11-20)\n\n#### Fix\n* Resolved an issue where the completion handler could be called multiple times under certain circumstances, potentially leading to crashes if the download task is cancelled. [#2319](https://github.com/onevcat/Kingfisher/pull/2319) @onevcat\n\n---\n\n## [8.1.0 - Live Photo](https://github.com/onevcat/Kingfisher/releases/tag/8.1.0) (2024-10-13)\n\n#### Add\n* Live Photo support. Now you can use the `kf` extension on `PHLivePhotoView` to load a live photo from network. Check [its documentation](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/kingfisherwrapper/setimage(with:options:completionhandler:)-1to8a) for more information. [#2302](https://github.com/onevcat/Kingfisher/pull/2302) @onevcat\n* A set of new APIs (new resource types, optional parameters for existing methods and error types, etc) for Live Photo support. [#2302](https://github.com/onevcat/Kingfisher/pull/2302) @onevcat\n\n#### Fix\n* Necessary `@MainActor` annotations for `ImageTransition.custom` member. [#2300](https://github.com/onevcat/Kingfisher/pull/2300) @mlight3\n\n---\n\n## [8.0.3 - Animated Image Hitting](https://github.com/onevcat/Kingfisher/releases/tag/8.0.3) (2024-09-21)\n\n#### Fix\n* A regression of iOS 18 that the `KFAnimatedImage` does not receive user interaction. [#2295](https://github.com/onevcat/Kingfisher/issues/2295) @onevcat @danieldaquino\n\n---\n\n## [8.0.2 - Blur Scale](https://github.com/onevcat/Kingfisher/releases/tag/8.0.2) (2024-09-21)\n\n#### Fix\n* An issue the the blurred image has a wrong size if the image contains a scale value other than one. [#2293](https://github.com/onevcat/Kingfisher/pull/2293) @Semty\n\n---\n\n## [8.0.1 - Old Friends Matter](https://github.com/onevcat/Kingfisher/releases/tag/8.0.1) (2024-09-18)\n\n#### Fix\n* A build issue in Xcode 15.2. Now the project builds and runs again in that old Xcode version. [#2289](https://github.com/onevcat/Kingfisher/pull/2289)\n\n---\n\n## [8.0.0 - 8.0.0 - Version 8](https://github.com/onevcat/Kingfisher/releases/tag/8.0.0) (2024-09-17)\n\n#### Add\n* Full Swift 6 support. Now Kingfisher compiles with both Swift 5 and Swift 6 language mode. [#2259](https://github.com/onevcat/Kingfisher/pull/2259) @onevcat\n* Swift Concurrency prepared. All necessary public APIs in Kingfisher are now `async` compatible. Kingfisher is also now built under strict concurrency mode. [#2239](https://github.com/onevcat/Kingfisher/pull/2239) @onevcat\n* Xcode 16 support. Explicitly built modules option is enabled and now Kingfisher can get better build performance under Xcode 16. [#2260](https://github.com/onevcat/Kingfisher/pull/2260) @onevcat\n* Refined documentation and beautified tutorials with DocC. [#2160](https://github.com/onevcat/Kingfisher/pull/2160) @onevcat\n\n#### Fix\n* MD5 is deprecated by the system. Now the hash method for file URL is replaced with SHA256. [#2117](https://github.com/onevcat/Kingfisher/pull/2117) @kmaschke85\n* Now the view extension methods are created in a more generic way, which provides better compatibility and extensibility. [#2244](https://github.com/onevcat/Kingfisher/pull/2244) @Mx-Iris @onevcat\n* Rewrite the blur rendering method without deprecated `UIGraphicsBeginImageContextWithOptions`. [#2274](https://github.com/onevcat/Kingfisher/pull/2274) @onevcat\n* Apply existential any to protocol for Swift 6. [#2283](https://github.com/onevcat/Kingfisher/pull/2283) @qwerty3345\n\n---\n\n## [7.12.0 - Lucky Seven](https://github.com/onevcat/Kingfisher/releases/tag/7.12.0) (2024-06-10)\n\n#### Add\n* Mark the `removeSizeExceededValues` method in `DiskStorage` as `public`. Now it is possible to call this method to trigger a cleanup of the disk cache manually. [#2214](https://github.com/onevcat/Kingfisher/pull/2214) @nickruddeni\n* A new `PHPickerResultImageDataProvider` for loading and caching images from `PHPickerResult`. [#2233](https://github.com/onevcat/Kingfisher/pull/2233) @nuomi1\n* An option of `reducePriorityOnDisappear` for SwiftUI. It sets a lower priority for the image download task when the view disappears, and restore it when re-appears. [#2211](https://github.com/onevcat/Kingfisher/pull/2211) @Aelx-Vaiman\n\n#### Fix\n* Some improvements for documentation grammar and typos. [#2236](https://github.com/onevcat/Kingfisher/pull/2236) @FlyingCaiChong\n* Use `.process` for the `PrivacyInfo.xcprivacy` in SPM to follow the practice suggested by Apple. [#2243](https://github.com/onevcat/Kingfisher/pull/2243) @BorysKhl @onevcat\n* An issue that the file extension was not correctly retrieved for calculating hash file name when `autoExtAfterHashedFileName` is set to `true`. [#2250](https://github.com/onevcat/Kingfisher/pull/2250) @freezy7\n\n---\n\n## [7.11.0 - visionOS for CocoaPods](https://github.com/onevcat/Kingfisher/releases/tag/7.11.0) (2024-02-12)\n\n#### Add\n* Add visionOS as a supported platform when being used in CocoaPods. For other dependency managers, it was already supported from previous versions. [#2205](https://github.com/onevcat/Kingfisher/pull/2205) @onevcat @grachyov\n* A name for background task started for image cache cleanup. [#2201](https://github.com/onevcat/Kingfisher/pull/2201) @antohisorin\n\n---\n\n## [7.10.2 - GIF crash fix](https://github.com/onevcat/Kingfisher/releases/tag/7.10.2) (2024-01-11)\n\n#### Fix\n* An issue that loading the same GIF image in differnet image views may crash the app. [#2194](https://github.com/onevcat/Kingfisher/pull/2194)\n* A build script issue that exported the xcframeworks does not have the correct cert signing. [#2179](https://github.com/onevcat/Kingfisher/pull/2179)\n* In iOS 13 and earlier, the new Swift runtime fails to convert `Any?` to a protocol value. [#2182](https://github.com/onevcat/Kingfisher/pull/2182)\n\n---\n\n## [7.10.1 - Compilation & Infinity](https://github.com/onevcat/Kingfisher/releases/tag/7.10.1) (2023-12-09)\n\n#### Fix\n* Now the CarPlay support (`CPListItem`) compiles again for iOS SDK 14.0 to 14.4. It was because an undocumented API change in the `CPListItem` property. [#2172](https://github.com/onevcat/Kingfisher/pull/2172) @brendonjkding\n* Fix an infinite `View` refreshing loop when `KFImage` is set with `startLoadingBeforeViewAppear` to `true` and the loading keeping fails. [#2169](https://github.com/onevcat/Kingfisher/pull/2169) @onevcat @sisoje @mirkokg\n\n---\n\n## [7.10.0 - Privacy Manifest](https://github.com/onevcat/Kingfisher/releases/tag/7.10.0) (2023-10-29)\n\n#### Add\n* Actually add the privacy manifest files to the xcframework, Swift Package Manager and CocoaPods. [#2122](https://github.com/onevcat/Kingfisher/issues/2122)[#2156](https://github.com/onevcat/Kingfisher/pull/2156) @CloudosaurusRex @NikcN22\n* Enable the modulemap generation and `-Swift.h` header again for ObjC compatibility. [#2138](https://github.com/onevcat/Kingfisher/pull/2138) @yev-kanivets\n\n#### Fix\n* Use the trait collection to determine animated image scale, instead of the deprecated `UIScreen` API. [#2157](https://github.com/onevcat/Kingfisher/pull/2157) @hyun99999\n* An issue that a local AV asset creates multiple disk caches when connected to Xcode during Debug phase. [#2158](https://github.com/onevcat/Kingfisher/pull/2157) @onevcat @elijahdou\n* The disk cache now is still availiable when the whole cache folder is removed by external operations instead of the methods in Kingfisher. [#2162](https://github.com/onevcat/Kingfisher/pull/2162) @onevcat @uclort\n* Some documentation and CI impro/vements.\n\n---\n\n## [7.9.1 - Lastest Xcode 15 beta](https://github.com/onevcat/Kingfisher/releases/tag/7.9.1) (2023-08-26)\n\n#### Fix\n* Update to the terminology for the latest Xcode 15 beta. It prevents building failing and warnings from previous beta versions. [#2123](https://github.com/onevcat/Kingfisher/pull/2123) @simonbs\n* A misused reason in the privacy manifest file. Now Kingfisher should declare the reason of using file creation and access time correctly. (However, the manifest file mechanism of SDK seems not working yet in Xcode 15 beta 7) [#2135](https://github.com/onevcat/Kingfisher/pull/2135) @CloudosaurusRex @onevcat\n* Some warnings which happens when building xcframework. This prevents them from becoming errors in the coming Swift 6. [#2136](https://github.com/onevcat/Kingfisher/pull/2136)\n\n---\n\n## [7.9.0 - visionOS & Xcode 15](https://github.com/onevcat/Kingfisher/releases/tag/7.9.0) (2023-07-29)\n\n#### Add\n* Add visionOS as support target. Now Kingfisher can run natively on visionOS, in both UIKit or SwiftUI mode. [#2103](https://github.com/onevcat/Kingfisher/pull/2103)\n* Add private manifest file (`PrivacyInfo.xcprivacy`) to the project to meet Apple's requirement of describing data collected and use of required reason API. [#2104](https://github.com/onevcat/Kingfisher/pull/2104)\n* Support digital signature in xcframework. Now the xcframework of Kingfisher is signed with the Apple Developer ID of the maintainer team. [#2106](https://github.com/onevcat/Kingfisher/pull/2106)\n* A public initializer of `ImageDownloadResult`. This allows overriding side to construct and return a valid download result. [#2107](https://github.com/onevcat/Kingfisher/pull/2107) @kmaschke85\n\n#### Fix\n* Some documentation fixes.\n\n---\n\n## [7.8.1 - Animated <3 Processor](https://github.com/onevcat/Kingfisher/releases/tag/7.8.1) (2023-06-19)\n\n#### Fix\n* Now the animated image creation from disk cache will use the input processor correctly. [#2099](https://github.com/onevcat/Kingfisher/pull/2099) @yeatse\n\n---\n\n## [7.8.0 - ImageSource Protocol](https://github.com/onevcat/Kingfisher/releases/tag/7.8.0) (2023-06-13)\n\n#### Add\n* Introduce a custom image source provider to enable third-party image processors to utilize `AnimatedImageView`. [#2094](https://github.com/onevcat/Kingfisher/pull/2094) @yeatse\n\n#### Fix\n* Deprecate the `ImageResource` and rename it to `KF.ImageResource`. This triggers a warning when explicitly refering to `ImageResource`, which conflicts to the identical names from Apple's `GeneratedAssetSymbols` or `DeveloperToolsSupport` in Xcode 15. It does not fix the issue automatically, but can help to achieve a smoother transition. [#2092](https://github.com/onevcat/Kingfisher/pull/2092) @JohnnyTseng @rtharston\n\n---\n\n## [7.7.0 - The Last Chance](https://github.com/onevcat/Kingfisher/releases/tag/7.7.0) (2023-05-20)\n\n#### Add\n* Expose a new `imageDownloader(_:didReceive:completionHandler:)` delegate method in `ImageDownloaderDelegate` to allow making `ResponseDisposition` decision to the download task. [#2048](https://github.com/onevcat/Kingfisher/pull/2048) @onevcat\n\n#### Fix\n* Some type conversion warnings which might annoy under Swift 6 compiler. [#2060](https://github.com/onevcat/Kingfisher/pull/2060) [#2063](https://github.com/onevcat/Kingfisher/pull/2063) @zunda-pixel\n* Apply access limitation to the internal `Source.Identifier`. [#2074](https://github.com/onevcat/Kingfisher/pull/2074) @iwill-hwang\n\n---\n\n## [7.6.2 - Fix Dead Loop](https://github.com/onevcat/Kingfisher/releases/tag/7.6.2) (2023-02-23)\n\n#### Fix\n* An issue causes high CPU usage and infinite loop when setting `nil` URL to a `KFImage` when `startLoadingBeforeViewAppear` is also `true`. [#2035](https://github.com/onevcat/Kingfisher/issues/2035) Big thanks to @BobbyRohweder\n* The extension support for `CPListItem` won't set the image back to blank when the loading failing. Now it keeps showing the placeholder, if set. [#2031](https://github.com/onevcat/Kingfisher/pull/2031) @DevVenusK\n\n---\n\n## [7.6.1 - Strict for Compiling](https://github.com/onevcat/Kingfisher/releases/tag/7.6.1) (2023-02-13)\n\n#### Fix\n* A compiling issue that new version of Swift (Swift 5.8) refuses to accept the false-positive optional binding. [#2029](https://github.com/onevcat/Kingfisher/pull/2029) @JetForMe\n\n---\n\n## [7.6.0 - Content Configuration](https://github.com/onevcat/Kingfisher/releases/tag/7.6.0) (2023-02-05)\n\n#### Add\n* Add a `contentConfigure` modifier to `KFImage` and related view types under SwiftUI. This allows you returning a non-image view to finish the configuation and display it as the loading result of `KFImage`. [#2027](https://github.com/onevcat/Kingfisher/pull/2027)\n* Make the `cachePathBlock` public so you can also configure it when creating a custom `DiskStorage.Config`. [#2025](https://github.com/onevcat/Kingfisher/pull/2025) by @zarechnyy\n\n---\n\n## [7.5.0 - Aggressive New Year](https://github.com/onevcat/Kingfisher/releases/tag/7.5.0) (2023-01-08)\n\n#### Add\n* Add a `KFImage` modifier `startLoadingBeforeViewAppear` to allow image loading before SwiftUI view's `onAppear`. This is a workaround for [#1988](https://github.com/onevcat/Kingfisher/issues/1988).\n\n#### Fix\n* Now loading images from local disk also respects the `backgroundDecode` option. [#2009](https://github.com/onevcat/Kingfisher/pull/2009)\n\n---\n\n## [7.4.1 - Maple Days](https://github.com/onevcat/Kingfisher/releases/tag/7.4.1) (2022-10-26)\n\n#### Fix\n* A rare crash from `_UIImageCGImageContent` when loading GIF files on iOS 15 or later. [#2004](https://github.com/onevcat/Kingfisher/pull/2004)\n* Now the dSYM symbols are contained inside the xcframework bundle instead of as standalone files. [#1998](https://github.com/onevcat/Kingfisher/pull/1998)\n* An issue that the processor is not applied to original image data when `DefaultCacheSerializer.preferCacheOriginalData` is set to `true`. [#1999](https://github.com/onevcat/Kingfisher/pull/1999)\n\n---\n\n## [7.4.0 - Summer Ends](https://github.com/onevcat/Kingfisher/releases/tag/7.4.0) (2022-10-05)\n\n#### Add\n* A `data` property in `RetrieveImageResult` for reading the original data when an image loading is done. [#1986](https://github.com/onevcat/Kingfisher/pull/1986)\n* An async `data` getter in `ImageDataProvider`. More async methods are on the way. [#1989](https://github.com/onevcat/Kingfisher/pull/1989)\n\n#### Fix\n* A workaround for some cases the `KFImage` does not load images when embedded in the SwiftUI List on iOS 16. This only alleviates the problem when shallow embedded. For deeper nested, waiting for Apple's fix. [#1988](https://github.com/onevcat/Kingfisher/issues/1988) FB11564208\n\n---\n\n## [7.3.2 - Align Layout](https://github.com/onevcat/Kingfisher/releases/tag/7.3.2) (2022-08-10)\n\n#### Fix\n* A regression introduced by the previous version, which changed the default layout behavior when setting a placeholder. Now the `KFImage` should have the same layout behavior as SwiftUI's `AsyncImage` while loading. if no placeholder is set, it takes all the proposed size while loading. If a placeholder is set, it propose size to the placeholder and follow placeholder's layout. [#1975](https://github.com/onevcat/Kingfisher/pull/1975)\n\n---\n\n## [7.3.1 - Empty Not Void](https://github.com/onevcat/Kingfisher/releases/tag/7.3.1) (2022-07-31)\n\n#### Fix\n* An issue that `EmptyView` as `KFImage` placeholder fails loading of the image. [#1973](https://github.com/onevcat/Kingfisher/pull/1973) [@damian-rzeszot]\n\n---\n\n## [7.3.0 - Progressive Progress](https://github.com/onevcat/Kingfisher/releases/tag/7.3.0) (2022-07-06)\n\n#### Add\n* Added `ImageProgressive` now contains a delegate `onImageUpdated` which will notify you everytime the progressive scanner can decode an intermediate image. You also have a chance to choose an image update strategy to respond the delegate. [#1957](https://github.com/onevcat/Kingfisher/issues/1957) @jyounus\n* Now the `progressive` option can work with `KingfisherManager`. Previously it only works when set in the view extension methods under `kf`. [#1961](https://github.com/onevcat/Kingfisher/pull/1961) @onevcat\n\n#### Fix\n* A potential crash in `AnimatedImageView` that releasing on another thread. [#1956](https://github.com/onevcat/Kingfisher/pull/1956) @ufosky\n* A few internal clean up and removal of unused code. [#1958](https://github.com/onevcat/Kingfisher/pull/1958) @idrougge\n\n#### Remove\n* With the support of `ImageProgressive.onImageUpdated`, the semantic of `ImageProgressive.default` is conflicting with the behavior. `ImageProgressive.default` is now marked as deprecated. To initilize a default `ImageProgressive`, use `ImageProgressive.init()` instead.\n\n---\n\n## [7.2.4 - Removing DocC plugin](https://github.com/onevcat/Kingfisher/releases/tag/7.2.4) (2022-06-15)\n\n#### Fix\n* Dependency of DocC plugin is now removed and Swift Package Index can still generate and host the documentation. [#1952](https://github.com/onevcat/Kingfisher/discussions/1952) @marcusziade\n\n---\n\n## [7.2.3 - Track Transform](https://github.com/onevcat/Kingfisher/releases/tag/7.2.3) (2022-06-09)\n\n#### Fix\n* Now the URL based `AVAssetImageDataProvider` support tracking transform by default. This could solve some cases that the video thumbnail were not at correct orientation. [#1951](https://github.com/onevcat/Kingfisher/pull/1951) @sgarg4008\n* Use DocC as documentation generator and switch to [Swift Package Index as the host](https://swiftpackageindex.com/onevcat/Kingfisher/master/documentation/kingfisher). Big thanks to @daveverwer and all other fellows for the fantastic work!\n\n---\n\n## [7.2.2 - Rainy Season](https://github.com/onevcat/Kingfisher/releases/tag/7.2.2) (2022-05-08)\n\n#### Fix\n* Loading an animated images from cache now respects the received options. [#1935](https://github.com/onevcat/Kingfisher/pull/1935) @uclort\n\n---\n\n## [7.2.1 - Spring Earth](https://github.com/onevcat/Kingfisher/releases/tag/7.2.1) (2022-04-11)\n\n#### Fix\n* Align `requestModifier` parameter with `AsyncImageDownloadRequestModifier` to allow async request changing. [#1918](https://github.com/onevcat/Kingfisher/pull/1918) @KKirsten\n* Fix an issue that data downloading task callbacks are held even when the task is removed. [#1913](https://github.com/onevcat/Kingfisher/pull/1913) @onevcat\n* Give correct cache key for local urls in its conformance of `Resource`. [#1914](https://github.com/onevcat/Kingfisher/pull/1914) @onevcat\n* Reset placeholder image when loading fails. [#1925](https://github.com/onevcat/Kingfisher/pull/1925) @PJ-LT\n* Fix several typos and grammar. [#1926](https://github.com/onevcat/Kingfisher/pull/1926) @johnmckerrell [#1927](https://github.com/onevcat/Kingfisher/pull/1927) @SunsetWan\n\n---\n\n## [7.2.0 - End of the tunnel](https://github.com/onevcat/Kingfisher/releases/tag/7.2.0) (2022-02-27)\n\n#### Add\n* An option in memory cache that allows the cached images not be purged while the app is switchted to background. [#1890](https://github.com/onevcat/Kingfisher/pull/1890)\n\n#### Fix\n* Now the animated images are reset when deinit. This might fix some ocasional crash when destroying the `AnimatedImageView`. [#1886](https://github.com/onevcat/Kingfisher/pull/1886)\n* Fix wrong key override when a local resource created by `ImageResource`'s initializer. [#1903](https://github.com/onevcat/Kingfisher/pull/1903)\n\n---\n\n## [7.1.2 - Cold Days](https://github.com/onevcat/Kingfisher/releases/tag/7.1.2) (2021-12-07)\n\n#### Fix\n* Lacking of `diskStoreWriteOptions` from `KFOptionSetter`. Now it supports to be set in a chainable way. [#1862](https://github.com/onevcat/Kingfisher/issues/1862) @ignotusverum\n* A duplicated nested `Radius` type which prevents the framework being used in Playground. [#1872](https://github.com/onevcat/Kingfisher/pull/1872)\n* An issue that sometimes `KFImage` does not load images correctly when a huge amount of images are being loaded due to animation setting. [#1873](https://github.com/onevcat/Kingfisher/pull/1873) @tatsuz0u\n* Remove explicit usage of `@Published` to allow refering `KFImage` even under a deploy target below iOS 13. [#1875](https://github.com/onevcat/Kingfisher/pull/1875)\n* Now the image cache calculats the cost animated images correctly with all frames. [#1881](https:://github.com/onevcat/Kingfisher/pull/1881) @pal-aspringfield\n* Remove CarPlay support when building against macCatalyst, which is not properly conditionally supported. [#1876](https://github.com/onevcat/Kingfisher/pull/1876)\n\n---\n\n## [7.1.1 - Double Ninth](https://github.com/onevcat/Kingfisher/releases/tag/7.1.1) (2021-10-16)\n\n#### Fix\n* In some cases the `KFImage` loading causes a freeze on certain iOS 14 systems. [#1849](https://github.com/onevcat/Kingfisher/issues/1849) Thanks reporting from @JetForMe @benjamincombes @aralatpulat\n* Setting image to an `AnimatedImageView` now correctly replaces its layer contents. [#1836](https://github.com/onevcat/Kingfisher/issues/1836) @phantomato\n\n---\n\n## [7.1.0 - Autumn Patch](https://github.com/onevcat/Kingfisher/releases/tag/7.1.0) (2021-10-12)\n\n#### Add\n* Extension for CarPlay support. Now you can use Kingfisher's extension image setting methods on `CPListItem`. [#1802](https://github.com/onevcat/Kingfisher/pull/1820) from @waynehartman\n\n#### Fix\n* An Xcode issue that not recognizes iOS 15 availability checking for Apple Silicon. [#1822](https://github.com/onevcat/Kingfisher/pull/1822) from @enoktate\n* Add `onFailureImage` modifier back to `KFImage`, which was unexpected removed while upgrading. [#1829](https://github.com/onevcat/Kingfisher/pull/1829) from @skellock\n* Start binder loading when `body` is evaluated. This fixes an unwanted flickering. This also adds a protection for internal loading state.  [#1828](https://github.com/onevcat/Kingfisher/pull/1828) from @JetForMe and @IvanShah\n* Use color description based on `CGFloat` style of a color instead of a hex value to allow extended color space when setting it to a processor. [#1826](https://github.com/onevcat/Kingfisher/pull/1826) from @vonox7\n* An issue that the local file provided images are cached for multiple times when an app is updated. This is due to a changing main bundle location on the disk. Now Kingfisher uses a stable version of disk URL as the default cache key. [#1831](https://github.com/onevcat/Kingfisher/pull/1831) from @iaomw\n* Now `KFImage`'s internal rendered view is wrapped by a `ZStack`. This prevents a lazy container from recognizing different `KFImage`s with a same URL as the same view. [#1840](https://github.com/onevcat/Kingfisher/pull/1840) from @iOSappssolutions\n\n---\n\n## [7.0.0 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0) (2021-09-21)\n\n#### Add\n* Rewrite SwiftUI support based on `@StateObject` instead of the old `@ObservedObject`. It provides a stable and better data model backs the image rendering in SwiftUI. For this, Kingfisher SwiftUI supports from iOS 14 now. [#1707](https://github.com/onevcat/Kingfisher/pull/1707)\n* Mark `ImageCache.retrieveImageInMemoryCache(forKey:options:)` as `open` to expose a correct override entry point to outside. [#1703](https://github.com/onevcat/Kingfisher/pull/1703)\n* The `NSTextAttachment` extension method now accepts closure instead of a evaluated view. This allows delaying the passing in view to the timing which actually it is needed. [#1746](https://github.com/onevcat/Kingfisher/pull/1746)\n* A `KFAnimatedImage` type to display a GIF image in SwiftUI. [#1705](https://github.com/onevcat/Kingfisher/pull/1705)\n* Add a `progress` parameter to the `KFImage`'s `placeholder` closure. This allows you create a view based on the loading progress. [#1707](https://github.com/onevcat/Kingfisher/pull/1707)\n* Now `KFAnimatedImage` also supports `configure` modifier so you can set options to the underhood `AnimatedImageView`. [#1768](https://github.com/onevcat/Kingfisher/pull/1768)\n* Expose `AnimatedImageView` fields to allow consumers to observe GIF progress. [#1789](https://github.com/onevcat/Kingfisher/pull/1789) @AttilaTheFun\n* An option to pass in an [write option](https://developer.apple.com/documentation/foundation/nsdata/writingoptions) for writing data to the disk cache. This allows writing cache in a fine-tuned way, such as `.atomic` or `.completeFileProtection`. [#1793](https://github.com/onevcat/Kingfisher/pull/1793) @ignotusverum\n\n#### Fix\n* Uses `UIGraphicsImageRenderer` on iOS and tvOS for better image drawing. [#1706](https://github.com/onevcat/Kingfisher/pull/1706)\n* An issue that prevents Kingfisher compiling on mac Catalyst target in some certain of Xcode versions. [#1692](https://github.com/onevcat/Kingfisher/pull/1692) @kvyatkovskys\n* The `KF.retry(:_)` method now accepts an optional value. It allows to reset the retry strategy by passing in a `nil` value. [#1729](https://github.com/onevcat/Kingfisher/pull/1729)\n* The `placeholder` view builder of `KFImage` now works when it gets changed instead of using its initial value forever. [#1707](https://github.com/onevcat/Kingfisher/pull/1707)\n* Some minor performance improvement. [#1739](https://github.com/onevcat/Kingfisher/pull/1739) @fuyoufang\n* The `LocalFileImageDataProvider` now loads data in a background queue by default. This prevents loading performance issue when the loading is created on main thread. [#1764](https://github.com/onevcat/Kingfisher/pull/1764) @ConfusedVorlon\n* Respect transition for SwiftUI view when using `KFImage`. [#1767](https://github.com/onevcat/Kingfisher/pull/1767)\n* A type of `AuthenticationChallengeResponsable`. Now use `AuthenticationChallengeResponsible` instead. [#1780](https://github.com/onevcat/Kingfisher/pull/1780) @fakerlogic\n* An issue that `AnimatedImageView` dose not change the `tintColor` for templated images. [#1786](https://github.com/onevcat/Kingfisher/pull/1786) @leonpesdk\n* A crash when loading a GIF image in iOS 13 and below. [#1805](https://github.com/onevcat/Kingfisher/pull/1805/) @leonpesdk\n\n#### Remove\n* Drop support for iOS 10/11, macOS 10.13/10.14, tvOS 10/11 and watch OS 3/4. [#1802](https://github.com/onevcat/Kingfisher/issues/1802)\n* The workaround of `KFImage.loadImmediately` is not necessary anymore due to the model switching to `@StateObject`. The interface is kept for backward compatibility, but it does nothing in the new version. [#1707](https://github.com/onevcat/Kingfisher/pull/1707)\n\n---\n\n## [7.0.0-beta.4 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0-beta.4) (2021-09-16)\n\n#### Add\n* An option to pass in an [write option](https://developer.apple.com/documentation/foundation/nsdata/writingoptions) for writing data to the disk cache. This allows writing cache in a fine-tuned way, such as `.atomic` or `.completeFileProtection`. [#1793](https://github.com/onevcat/Kingfisher/pull/1793)\n\n#### Fix\n* A crash when loading a GIF image in iOS 13 and below. [#1805](https://github.com/onevcat/Kingfisher/pull/1805/files)\n\n---\n\n## [7.0.0-beta.3 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0-beta.3) (2021-08-29)\n\n#### Add\n* Now `KFAnimatedImage` also supports `configure` modifier so you can set options to the underhood `AnimatedImageView`. [#1768](https://github.com/onevcat/Kingfisher/pull/1768)\n* Expose `AnimatedImageView` fields to allow consumers to observe GIF progress. [#1789](https://github.com/onevcat/Kingfisher/pull/1789)\n\n#### Fix\n* Respect transition for SwiftUI view when using `KFImage`. [#1767](https://github.com/onevcat/Kingfisher/pull/1767)\n* A type of `AuthenticationChallengeResponsable`. Now use `AuthenticationChallengeResponsible` instead. [#1780](https://github.com/onevcat/Kingfisher/pull/1780/files)\n* An issue that `AnimatedImageView` dose not change the `tintColor` for templated images. [#1786](https://github.com/onevcat/Kingfisher/pull/1786)\n\n---\n\n## [7.0.0-beta.2 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0-beta.2) (2021-08-02)\n\n#### Fix\n\n- `LocalFileImageDataProvider` now loads data in a background queue by default. This prevents loading performance issue when the loading is created on main thread. [#1764]\n\n---\n\n## [7.0.0-beta.1 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0-beta.1) (2021-07-27)\n\n#### Add\n* Rewrite SwiftUI support based on `@StateObject` instead of the old `@ObservedObject`. It provides a stable and better data model backs the image rendering in SwiftUI. For this, Kingfisher SwiftUI supports from iOS 14 now. [#1707](https://github.com/onevcat/Kingfisher/pull/1707)\n* Mark `ImageCache.retrieveImageInMemoryCache(forKey:options:)` as `open` to expose a correct override entry point to outside. [#1703](https://github.com/onevcat/Kingfisher/pull/1703)\n* The `NSTextAttachment` extension method now accepts closure instead of a evaluated view. This allows delaying the passing in view to the timing which actually it is needed. [#1746](https://github.com/onevcat/Kingfisher/pull/1746)\n* A `KFAnimatedImage` type to display a GIF image in SwiftUI. [#1705](https://github.com/onevcat/Kingfisher/pull/1705)\n* Add a `progress` parameter to the `KFImage`'s `placeholder` closure. This allows you create a view based on the loading progress. [#1707](https://github.com/onevcat/Kingfisher/pull/1707)\n\n#### Fix\n* Uses `UIGraphicsImageRenderer` on iOS and tvOS for better image drawing. [#1706](https://github.com/onevcat/Kingfisher/pull/1706)\n* An issue that prevents Kingfisher compiling on mac Catalyst target in some certain of Xcode versions. [#1692](https://github.com/onevcat/Kingfisher/pull/1692)\n* The `KF.retry(:_)` method now accepts an optional value. It allows to reset the retry strategy by passing in a `nil` value. [#1729](https://github.com/onevcat/Kingfisher/pull/1729)\n* The `placeholder` view builder of `KFImage` now works when it gets changed instead of using its initial value forever. [#1707](https://github.com/onevcat/Kingfisher/pull/1707)\n* Some minor performance improvement. [#1739](https://github.com/onevcat/Kingfisher/pull/1739)\n\n#### Remove\n* Drop support for iOS 10, macOS 10.13, tvOS 10 and watch OS 3.\n* The workaround of `KFImage.loadImmediately` is not necessary anymore due to the model switching to `@StateObject`. The interface is kept for backward compatibility, but it does nothing in the new version. [#1707](https://github.com/onevcat/Kingfisher/pull/1707)\n\n---\n\n## [6.3.0 - Open To Better](https://github.com/onevcat/Kingfisher/releases/tag/6.3.0) (2021-04-21)\n\n#### Add\n* Mark `SessionDelegate` as public to allow a subclass to take over the delegate methods from session tasks. [#1658](https://github.com/onevcat/Kingfisher/pull/1658)\n* A new `imageDownloader(_:didDownload:with:)` in `ImageDownloaderDelegate` to pass not only `Data` but also the whole `URLResponse` to delegate method. Now you can determine how to handle these data based on the received response. [#1676](https://github.com/onevcat/Kingfisher/pull/1676)\n* An option `autoExtAfterHashedFileName` in `DiskStorage.Config` to allow appending the file extension extracted from the cache key. [#1671](https://github.com/onevcat/Kingfisher/pull/1671)\n\n#### Fix\n* Now the GIF continues to play in a collection view cell with highlight support. [#1685](https://github.com/onevcat/Kingfisher/pull/1685)\n* Fix a crash when loading GIF files with lots of frames in `AnimatedImageView`. Thanks for contribution from @wow-such-amazing [#1686](https://github.com/onevcat/Kingfisher/pull/1686)\n\n---\n\n## [6.2.1 - Spring Release Fix](https://github.com/onevcat/Kingfisher/releases/tag/6.2.1) (2021-03-09)\n\n#### Fix\n* Revert changes for the external delegate in [#1620](https://github.com/onevcat/Kingfisher/pull/1620), which caused some image resource loading failing due to a CFNetwork internal error.\n\n---\n\n## [6.2.0 - Spring Release](https://github.com/onevcat/Kingfisher/releases/tag/6.2.0) (2021-03-08)\n\n#### Add\n* The backend of Kingfisher's cache solution, `DiskStorage` and `MemoryStorage`, are now marked as `public`. So you can use them standalone in your project. [#1649](https://github.com/onevcat/Kingfisher/pull/1649)\n* An `imageFrameCount` property in image view extensions. It holds the frame count of an animated image if available. [#1647](https://github.com/onevcat/Kingfisher/pull/1647)\n* A new `extraSessionDelegateHandler` in `ImageDownloader`. Now you can receive the related session task delegate method by registering an external delegate object. [#1620](https://github.com/onevcat/Kingfisher/pull/1620)\n* A new method `loadImmediately` for `KFImage` to start the load manually. It is useful if you want to load the image before `onAppear` is called.\n\n#### Fix\n* Drop the use of `@State` for keeping image across `View` update for `KFImage`. This should fix some SwiftUI internal crash when setting the image in `KFImage`. [#1642](https://github.com/onevcat/Kingfisher/pull/1642)\n* The image reference in `ImageBinder` now is marked with `weak`. This helps release memory quicker in some cases. [#1640](https://github.com/onevcat/Kingfisher/pull/1640)\n\n---\n\n## [6.1.1 - SwiftUI Issues](https://github.com/onevcat/Kingfisher/releases/tag/6.1.1) (2021-02-17)\n\n#### Fix\n* Remove unnecessary queue dispatch when setting image result. This prevents image flickering when some situation. [#1615](https://github.com/onevcat/Kingfisher/pull/1615)\n* Now the `KF` builder methods also accept optional `URL` or `Source`. It aligns the syntax with the normal view extension methods. [#1617](https://github.com/onevcat/Kingfisher/pull/1617)\n* Fix an issue that wrong hash is calculated for `ImageBinder`. It might cause view state lost for a `KFImage`. [#1624](https://github.com/onevcat/Kingfisher/pull/1624)\n* Now the `ImageCache` will disable the disk storage when there is no free space on disk when creating the cache folder, instead of just crashing it. [#1628](https://github.com/onevcat/Kingfisher/pull/1628)\n* A workaround for `@State` lost when using a view inside another container in a `Lazy` stack or grid. [#1631](https://github.com/onevcat/Kingfisher/pull/1631)\n* Performance improvement for images with an non-up orientation in Exif when displaying in `KFImage`. [#1629](https://github.com/onevcat/Kingfisher/pull/1629)\n\n---\n\n## [6.1.0 - SwiftUI Rework](https://github.com/onevcat/Kingfisher/releases/tag/6.1.0) (2021-02-01)\n\n#### Add\n* Rewrite state management for `KFImage`. Now the image reloading works in a more stable way without task dispatching. [#1604](https://github.com/onevcat/Kingfisher/pull/1604)\n* Add `fade` and `forceTransition` modifier to `KFImage` to support built-in fade in effect when loading image in SwiftUI. [#1604](https://github.com/onevcat/Kingfisher/pull/1604)\n\n#### Fix\n* When an `ImageModifier` is applied, the modified image is not cached to memory cache anymore. The `ImageModifier` is intended to be used just before setting the image to a view and now it works as expected. [#1612](https://github.com/onevcat/Kingfisher/pull/1612)\n* Now `SwiftUI` and `Combine` are declared as weak link in podspec. This is a workaround for [some rare case build issue](https://stackoverflow.com/a/60198305). It does not affect supported deploy version of Kingfisher. [#1607](https://github.com/onevcat/Kingfisher/pull/1607)\n* Remove header file from podspec to allow Kingfisher built as a static framework in a Swift-ObjC mixed project. [#1608](https://github.com/onevcat/Kingfisher/pull/1608)\n\n---\n\n## [6.0.1 - Bind & Hug](https://github.com/onevcat/Kingfisher/releases/tag/6.0.1) (2021-01-05)\n\n#### Fix\n* Start the binder again when `KFImage` initialized, to keep the same behavior as previous versions. [#1594](https://github.com/onevcat/Kingfisher/issues/1594)\n\n---\n\n## [6.0.0 - New Year 2021](https://github.com/onevcat/Kingfisher/releases/tag/6.0.0) (2021-01-03)\n\n#### Add\n* A `KF` shorthand to create image setting tasks and config them. It provides a cleaner and modern way to use Kingfisher. Now, instead of using `imageView.kf.setImage(with:options:)`, you can perform chain-able invocation with `KF` helpers. For example, the code below is identical. [#1546](https://github.com/onevcat/Kingfisher/pull/1546)\n\n    ```swift\n    // Old way\n    imageView.kf.setImage(\n      with: url,\n      placeholder: localImage,\n      options: [.transition(.fade(1)), .loadDiskFileSynchronously],\n      progressBlock: { receivedSize, totalSize in\n          print(\"progressBlock\")\n      },\n      completionHandler: { result in\n          print(result)\n      }\n    )\n\n    // New way\n    KF.url(url)\n      .placeholder(localImage)\n      .fade(duration: 1)\n      .loadDiskFileSynchronously()\n      .onProgress { _ in print(\"progressBlock\") }\n      .onSuccess { result in print(result) }\n      .onFailure { err in print(\"Error: \\(err)\") }\n      .set(to: imageView)\n    ```\n    \n* Similar to `KF`, The `KFImage` for SwiftUI is now having the similar chain-able syntax to setup an image task and options. This makes the `KFImage` APIs closer to the way how SwiftUI code is written. [#1586](https://github.com/onevcat/Kingfisher/pull/1586)\n* Add support for `TVMonogramView` on tvOS. [#1571](https://github.com/onevcat/Kingfisher/pull/1571)\n* Some important properties and method in `AnimatedImageView.Animator` are marked as `public` now. It provides some useful information of the decoded GIF files. [#1575](https://github.com/onevcat/Kingfisher/pull/1575)\n* An `AsyncImageDownloadRequestModifier` to support modifying the request in an asynchronous way. [#1589](https://github.com/onevcat/Kingfisher/pull/1589/files)\n* Add a `.lowDataMode` option to support for Low Data Mode. When the `.lowDataMode` option is provided with an alternative source (usually a low-resolution version of the original image), Kingfisher will respect user's Low Data Mode setting and download the alternative image instead. [#1590](https://github.com/onevcat/Kingfisher/pull/1590)\n\n#### Fix\n* An issue that importing AppKit wrongly in a macCatalyst build. [#1547](https://github.com/onevcat/Kingfisher/pull/1547/commits/096498f7798a6fd34c70efc6f80014dfc6d8a9b7)\n\n#### Remove\n* Deprecated types, methods and properties are removed. If you are still using `Kingfisher.Image`, `Kingfisher.ImageView` or `Kingfisher.Button`, use the equivalent `KFCrossPlatform` types (such as `KFCrossPlatformImage`, etc) instead. Please make sure you do not have any warnings before migrate to Kingfisher v6. For more about the removed deprecated things, check [#1525](https://github.com/onevcat/Kingfisher/pull/1525/files).\n* The standalone framework target of SwiftUI support is removed. Now the SwiftUI support is a part in the main Kingfisher library. To upgrade to v6, first remove `Kingfisher/SwiftUI` subpod (if you are using CocoaPods) or remove the `KingfisherSwiftUI` target (if you are using Carthage or Swift Package Manager), then reinstall Kingfisher. [#1574](https://github.com/onevcat/Kingfisher/pull/1574)\n\n---\n\n## [5.15.8 - KFImage handler](https://github.com/onevcat/Kingfisher/releases/tag/5.15.8) (2020-11-27)\n\n#### Fix\n* An issue caused the `onSuccess` handler not be called when the image is already cached. [#1570](https://github.com/onevcat/Kingfisher/pull/1570)\n\n---\n\n## [5.15.7 - Cancel Lock](https://github.com/onevcat/Kingfisher/releases/tag/5.15.7) (2020-10-29)\n\n#### Fix\n* A potential crash when cancelling image downloading task while accessing its original request on iOS 13 or earlier. [#1558](https://github.com/onevcat/Kingfisher/pull/1558)\n\n---\n\n## [5.15.6 - ImageBinder Callback](https://github.com/onevcat/Kingfisher/releases/tag/5.15.6) (2020-10-11)\n\n#### Fix\n* Prevent main queue dispatching in `ImageBinder` if it is already on main thread. This prevents unintended flickering when reloading. [#1551](https://github.com/onevcat/Kingfisher/pull/1551)\n\n---\n\n## [5.15.5 - Cancelling Fix](https://github.com/onevcat/Kingfisher/releases/tag/5.15.5) (2020-09-29)\n\n#### Fix\n* A possible fix for the crashes when cancelling a huge amount of image tasks too fast. [#1537]\n\n---\n\n## [5.15.4 - Farewell Objective-C (CocoaPods)](https://github.com/onevcat/Kingfisher/releases/tag/5.15.4) (2020-09-24)\n\n#### Fix\n* Give `SessionDelegate` an Objective-C name so it can work with other libraries even added by a dependency which generates Objective-C header. [#1532](https://github.com/onevcat/Kingfisher/pull/1532)\n\n---\n\n## [5.15.3 - Farewell Objective-C](https://github.com/onevcat/Kingfisher/releases/tag/5.15.3) (2020-09-21)\n\n#### Fix\n* Removed the unnecessary ObjC header generating and module defining due to Xcode 12 is now generating conflicted types even for different libraries. [#1517](https://github.com/onevcat/Kingfisher/issues/1517)\n* Set deploy target for SwiftUI target and its pod spec to iOS 10 and macOS 10.12, which aligns to the settings of core framework. That resolves some dependency issues when using CocoaPods for both app target and extension targets. But it does not mean you can use the SwiftUI support on those minimal target. All related APIs are still unavailable on old system versions. [#1524](https://github.com/onevcat/Kingfisher/pull/1524)\n\n---\n\n## [5.15.2 - Xcode 11 Revived](https://github.com/onevcat/Kingfisher/releases/tag/5.15.2) (2020-09-19)\n\n#### Fix\n* Fix a build error introduced by the previous SwiftUI fix for Xcode 12. Now Xcode 11 can also build the KingfisherSwiftUI target. [#1515](https://github.com/onevcat/Kingfisher/pull/1515)\n\n---\n\n## [5.15.1 - SwiftUI Layout](https://github.com/onevcat/Kingfisher/releases/tag/5.15.1) (2020-09-16)\n\n#### Fix\n* A workaround for a SwiftUI issue that embedding an image view inside the `List` > `NavigationLink` > `HStack` hierarchy could crash the app on iOS 14. [#1508](https://github.com/onevcat/Kingfisher/issues/1508)\n\n---\n\n## [5.15.0 - Video and Text Attachment](https://github.com/onevcat/Kingfisher/releases/tag/5.15.0) (2020-08-17)\n\n#### Add\n* An `AVAssetImageDataProvider` to generate an image from a remote video asset at a specified time. All the processing gets benefits from current existing Kingfisher technologies, such as cache and image processors. [#1500](https://github.com/onevcat/Kingfisher/pull/1500)\n* New extension methods on `NSTextAttachment` to load an image from network for an attachment. [#1495](https://github.com/onevcat/Kingfisher/pull/1495)\n* A general clear cache method which combines clearing for memory cache and disk cache. [#1494](https://github.com/onevcat/Kingfisher/pull/1494)\n\n#### Fix\n* Now the sample app has a new look and supports dark mode, finally. [#1496](https://github.com/onevcat/Kingfisher/pull/1496)\n\n---\n\n## [5.14.1 - Summer Fix](https://github.com/onevcat/Kingfisher/releases/tag/5.14.1) (2020-07-06)\n\n#### Fix\n* Early return if no valid animator in an `AnimatedImageView`. This prevents a CGImage rendering issue displaying a static image. [#1428](https://github.com/onevcat/Kingfisher/issues/1428)\n* Enable Define Module setting to generate module map. So Kingfisher could be used in libraries imported to Objective-C projects. [#1451](https://github.com/onevcat/Kingfisher/pull/1451)\n* A fix to workaround on implicitly initializer of queue that might cause a crash. [#1449](https://github.com/onevcat/Kingfisher/issues/1449)\n* Improve the disk cache performance by avoiding unnecessary disk operations. [#1480](https://github.com/onevcat/Kingfisher/pull/1480)\n\n---\n\n## [5.14.0 - Retry Strategy](https://github.com/onevcat/Kingfisher/releases/tag/5.14.0) (2020-05-13)\n\n#### Add\n* A `.retryStrategy` option and associated `RetryStrategy` to define a highly customizable retry mechanism in Kingfisher. [#1424]\n* Built-in `DelayRetryStrategy` to provide a most common used retry strategy implementation. It simplifies the normal retry requirement when downloading an image from network. [#1447](https://github.com/onevcat/Kingfisher/pull/1447)\n* Now you can set the round corner radius for a `RoundCornerImageProcessor` in a fraction way. This is useful when you do not know the desire image view size, but still want to clip any received image to a certain round corner ratio (such as a circle for any image). [#1443](https://github.com/onevcat/Kingfisher/pull/1443)\n* Add an `isLoaded` binding to `KFImage` to follow SwiftUI pattern better. [#1429](https://github.com/onevcat/Kingfisher/pull/1429)\n\n#### Fix\n* An issue that `.imageModifier` option not working on an `ImageProvider` provided image. [#1435](https://github.com/onevcat/Kingfisher/pull/1435)\n* A workaround for making xcframework continue to work when exported with Swift 5.2 compiler and Xcode 11.4. [#1444](https://github.com/onevcat/Kingfisher/pull/1444)\n\n---\n\n## [5.13.4 - Build Configurations](https://github.com/onevcat/Kingfisher/releases/tag/5.13.4) (2020-04-11)\n\n#### Fix\n* Expose all build configurations in Package.swift file for Swift Package Manager. Now you can choose the linking style by yourself. [#1426](https://github.com/onevcat/Kingfisher/pull/1426)\n\n---\n\n## [5.13.3 - Dynamic SPM](https://github.com/onevcat/Kingfisher/releases/tag/5.13.3) (2020-04-01)\n\n#### Fix\n* Allows Carthage to build this library for macOS. [#1413](https://github.com/onevcat/Kingfisher/pull/1413)\n* Explicitly specify to build as a dynamic framework for Swift Package Manager. [#1420](https://github.com/onevcat/Kingfisher/pull/1420)\n\n---\n\n## [5.13.2 - KFImage Orientation](https://github.com/onevcat/Kingfisher/releases/tag/5.13.2) (2020-02-28)\n\n#### Fix\n* An issue for `KFImage` when resizing images with different EXIF orientation other than top. [#1396](https://github.com/onevcat/Kingfisher/pull/1396)\n* A race condition when setting `CacheCallbackCoordinator` state. [#1394](https://github.com/onevcat/Kingfisher/pull/1394)\n* Move an `@objc` attribute to prevent warnings in Xcode 11.4.\n\n---\n\n## [5.13.1 - Internal Warning](https://github.com/onevcat/Kingfisher/releases/tag/5.13.1) (2020-02-17)\n\n#### Fix\n* Fix an unused variable warning which is on by default in Xcode 11.4 and Swift 5.2, which makes CocoaPods angry when compiling. [#1393](https://github.com/onevcat/Kingfisher/pull/1393)\n\n---\n\n## [5.13.0 - New Year 2020](https://github.com/onevcat/Kingfisher/releases/tag/5.13.0) (2020-01-17)\n\n#### Add\n* Mark `DefaultCacheSerializer` as `public` and enables the ability of original data caching. [#1373](https://github.com/onevcat/Kingfisher/pull/1373/)\n* Add image compression quality parameter to `DefaultCacheSerializer`. [#1372](https://github.com/onevcat/Kingfisher/pull/1372/)\n* A new `contentURL` property in `ImageDataProvider` to provide a URL when it makes sense. [#1386](https://github.com/onevcat/Kingfisher/pull/1386/)\n\n#### Fix\n* Now, local file URLs can be loaded as `Resource`s without converted to `LocalFileImageDataProvider` explicitly. [#1386](https://github.com/onevcat/Kingfisher/pull/1386/)\n\n---\n\n## [5.12.0 - White Overflow](https://github.com/onevcat/Kingfisher/releases/tag/5.12.0) (2019-12-13)\n\n#### Add\n* Two error cases under `KingfisherError.CacheErrorReason` to give out the detail error information and reason when a failure happens when caching the file on disk. Check `.cannotCreateCacheFile` and `.cannotSetCacheFileAttribute` if you need to handle these errors. [#1365](https://github.com/onevcat/Kingfisher/pull/1365)\n\n#### Fix\n* A 32-bit `Int` overflow when calculating expiration duration when a large `days` value is set for `StorageExpiration`. [#1371](https://github.com/onevcat/Kingfisher/pull/1371)\n* The build config for SwiftUI sub-pod now only applies to the KingfisherSwiftUI scheme. [#1368](https://github.com/onevcat/Kingfisher/pull/1368)\n\n---\n\n## [5.11.0 - macCatalyst](https://github.com/onevcat/Kingfisher/releases/tag/5.11.0) (2019-11-30)\n\n#### Add\n* Support macCatalyst platform when building with Carthage. [#1356](https://github.com/onevcat/Kingfisher/pull/1356)\n\n#### Fix\n* Fix an issue that image orientation not correctly applied when an image processor used. [#1358](https://github.com/onevcat/Kingfisher/pull/1358)\n\n---\n\n## [5.10.1 - Repeat Count](https://github.com/onevcat/Kingfisher/releases/tag/5.10.1) (2019-11-20)\n\n#### Fix\n* Fix a wrong calculation of `repeatCount` of `AnimatedImageView`. Now it can play correct count for an animated image. [#1350](https://github.com/onevcat/Kingfisher/pull/1350)\n* Make sure to skip disk cache when `fromMemoryCacheOrRefresh` set. [#1351](https://github.com/onevcat/Kingfisher/pull/1351)\n* Fix a issue which prevents building with Xcode 10. [#1353](https://github.com/onevcat/Kingfisher/pull/1353)\n\n---\n\n## [5.10.0 - Rex Rabbit](https://github.com/onevcat/Kingfisher/releases/tag/5.10.0) (2019-11-17)\n\n#### Add\n* An `.alternativeSources` option to provide a list of alternative image loading `Source`s. These `Source`s act as a fallback when the original `Source` downloading fails where Kingfisher will try to load images from. [#1343](https://github.com/onevcat/Kingfisher/pull/1343)\n\n#### Fix\n* The `.waitForCache` option now also waits for caching for original image if the `.cacheOriginalImage` is also set. [#1344](https://github.com/onevcat/Kingfisher/pull/1344)\n* Now the `retrieveImage` methods in `ImageCache` calls its `callbackQueue` is `.mainCurrentOrAsync` by default instead of `.untouch`. It aligns the behavior of other parts in the framework. [#1338](https://github.com/onevcat/Kingfisher/pull/1338)\n* An issue that causes customize indicator not being placed with correct size. [#1345](https://github.com/onevcat/Kingfisher/pull/1345)\n* Performance improvement for loading progressive images. [#1332](https://github.com/onevcat/Kingfisher/pull/1332)\n\n---\n\n## [5.9.0 - Combination](https://github.com/onevcat/Kingfisher/releases/tag/5.9.0) (2019-10-24)\n\n#### Add\n* Introduce a `|>` operator for combining image processors. [#1320](https://github.com/onevcat/Kingfisher/pull/1320)\n\n#### Fix\n* Improve performance of reading task identifier when handling downloading side effect. [#1310](https://github.com/onevcat/Kingfisher/pull/1310)\n* Improve some type conversion to boost building. [#1321](https://github.com/onevcat/Kingfisher/pull/1321)\n\n---\n\n## [5.8.3 - Carthage Cache](https://github.com/onevcat/Kingfisher/releases/tag/5.8.3) (2019-10-09)\n\n#### Fix\n* Generate Objective-C header to make carthage cache work again. [#1308](https://github.com/onevcat/Kingfisher/pull/1308)\n\n---\n\n## [5.8.2 - Game of Thrones](https://github.com/onevcat/Kingfisher/releases/tag/5.8.2) (2019-10-04)\n\n#### Fix\n* Fix broken semantic versioning introduced by 5.8.0. [#1304](https://github.com/onevcat/Kingfisher/pull/1304)\n* Remove implicit animations in SwiftUI when a `.fade` animation applied in the option. Now Kingfisher respect all animations set by users instead of overwriting it internally. [#1301](https://github.com/onevcat/Kingfisher/pull/1301)\n* Now project uses KingfisherSwiftUI with Swift Package Manager can be archived correctly. [#1300](https://github.com/onevcat/Kingfisher/pull/1300)\n\n---\n\n## [5.8.1 - Borderless](https://github.com/onevcat/Kingfisher/releases/tag/5.8.1) (2019-09-27)\n\n#### Fix\n* Remove the unexpected border in `KFImage` while loading the image. [#1293](https://github.com/onevcat/Kingfisher/pull/1293)\n\n---\n\n## [5.8.0 - Xcode 11 & SwiftUI](https://github.com/onevcat/Kingfisher/releases/tag/5.8.0) (2019-09-25)\n\n#### Add\n* Add support for Swift Package Manager. Now you can build and use Kingfisher with SPM under Xcode 11 and use it in all targets.\n* Add support for iPad Apps for Mac. You can use Kingfisher's UIKit extensions (like `UIImage` and `UIImageView`) on a catalyst project.\n* Add support for SwiftUI. Build and import KingfisherSwiftUI.framework or contain the \"Kingfisher/SwiftUI\" subpod, then you can use `KFImage` to load image asynchronously. `KFImage` provides a similar interface as `View.Image`.\n* Add support for building as a binary framework. A zipped file containing `xcframework` and related dSYMs is provided in the release page.\n* A `diskCacheAccessExtendingExpiration` option to give more control of disk cache extending behavior. [#1287](https://github.com/onevcat/Kingfisher/pull/1287)\n* Combine all targets into one. Now Kingfisher is a cross-platform target and you need to specify an SDK to build it.\n\n#### Fix\n* Rename too generic typealias names in Kingfisher, to avoid conflicting with SwiftUI types. Original `Kingfisher.Image` is now `Kingfisher.KFCrossPlatformImage`. The similar rules are applied to other cross-platform typealias too, such as `Kingfisher.View`, `Kingfisher.Color` and more.\n* A potential thread issue in `taskIdentifier` which might cause a crash when using data provider. [#1276](https://github.com/onevcat/Kingfisher/pull/1276)\n* An issue that causes memory shortage when a large number of network images are loaded in a short time. [#1270](https://github.com/onevcat/Kingfisher/pull/1270)\n\n---\n\n## [5.7.1 - Thread Things](https://github.com/onevcat/Kingfisher/releases/tag/5.7.1) (2019-08-11)\n\n#### Fix\n* Setting `runLoopMode` for `AnimatedImageView` will trigger animation restart normally. [#1253](https://github.com/onevcat/Kingfisher/pull/1253)\n* A possible thread issue when removing storage object from memory cache by the cache policy. [#1255](https://github.com/onevcat/Kingfisher/pull/1255)\n* Manipulating on `AnimateImageView`'s frame array is now thread safe. [#1257](https://github.com/onevcat/Kingfisher/pull/1257)\n\n---\n\n## [5.7.0 - Summer Bird](https://github.com/onevcat/Kingfisher/releases/tag/5.7.0) (2019-07-03)\n\n#### Add\n* Mark `cacheFileURL(forKey:)` of `DiskStorage` to public. [#1214](https://github.com/onevcat/Kingfisher/issues/1214)\n* Mark `KingfisherManager` initializer to public so other dependencies can customize the manager behavior. [#1216](https://github.com/onevcat/Kingfisher/issues/1216)\n\n#### Fix\n* Performance improvement on progressive JPEG scanning. [#1218](https://github.com/onevcat/Kingfisher/pull/1218)\n* Fix a potential thread issue when checking progressive JPEG. [#1220](https://github.com/onevcat/Kingfisher/pull/1220)\n\n#### Remove\n* The deprecated `Result` extensions for Swift 4 back compatibility are removed. [#1224](https://github.com/onevcat/Kingfisher/pull/1224)\n\n---\n\n## [5.6.0 - The Sands of Time](https://github.com/onevcat/Kingfisher/releases/tag/5.6.0) (2019-06-11)\n\n#### Add\n* Support extending memory cache TTL to a specified time instead of the fixed original expire setting. Use the `.memoryCacheAccessExtendingExpiration` to set a customize expiration extending duration when accessing the image. [#1196](https://github.com/onevcat/Kingfisher/pull/1196)\n* Add prebuilt binary framework when releasing to GitHub. Further supporting of fully compatible binary framework would come after Swift module stability. [#1194](https://github.com/onevcat/Kingfisher/pull/1194)\n\n#### Fix\n* Resizing performance for animated images should be improved dramatically. [#1189](https://github.com/onevcat/Kingfisher/pull/1189)\n* A small optimization on MD5 calculation for image file cache key. [#1183](https://github.com/onevcat/Kingfisher/pull/1183)\n\n---\n\n## [5.5.0 - Progressive JPEG](https://github.com/onevcat/Kingfisher/releases/tag/5.5.0) (2019-05-17)\n\n#### Add\n* Add support for loading progressive JPEG images. This feature is still in beta and will be improved in the next few releases. To try it out, make sure you are loading a progressive JPEG image with a `.progressiveJPEG` options passed in. Thanks @lixiang1994 [#1181](https://github.com/onevcat/Kingfisher/pull/1181)\n* Choose to use `Swift.Result` as the default result type when Swift 5.0 or above is applied. [#1146](https://github.com/onevcat/Kingfisher/pull/1146)\n\n#### Fix\n* Apply to some modern Swift syntax, which may also improve internal performance a bit. [#1181](https://github.com/onevcat/Kingfisher/pull/1181)\n\n---\n\n## [5.4.0 - Accio Support](https://github.com/onevcat/Kingfisher/releases/tag/5.4.0) (2019-04-24)\n\n#### Add\n* Add support for building project with [Accio](https://github.com/JamitLabs/Accio) (and Swift Package Manager). [#1153](https://github.com/onevcat/Kingfisher/pull/1153)\n\n#### Fix\n* Now `maxCachePeriodInSecond` of cache would treat 0 as expiring correctly. [#1160](https://github.com/onevcat/Kingfisher/pull/1160)\n* Normalization of image now returns an image with `.up` as orientation. [#1163](https://github.com/onevcat/Kingfisher/pull/1163)\n\n---\n\n## [5.3.1 - Prefetching Thread](https://github.com/onevcat/Kingfisher/releases/tag/5.3.1) (2019-03-28)\n\n#### Fix\n* Some thread issues which may cause crash when loading images by `ImagePrefetcher`. [#1150](https://github.com/onevcat/Kingfisher/pull/1150)\n* Setting a negative value by the deprecated `maxCachePeriodInSecond` API now expires the cache correctly. [#1145](https://github.com/onevcat/Kingfisher/pull/1145)\n\n---\n\n## [5.3.0 - Prefetching Sources](https://github.com/onevcat/Kingfisher/releases/tag/5.3.0) (2019-03-24)\n\n#### Add\n* Now `ImagePretcher` also supports using `Source` as fetching target. [#1142](https://github.com/onevcat/Kingfisher/pull/1142)\n* An option to skip file name hashing when storing image to disk cashe. [#1140](https://github.com/onevcat/Kingfisher/pull/1140)\n* Supports multiple Swift versions for CocoaPods 1.7.0.\n\n#### Fix\n* An issue that loading a downsampled image from original version might lead to different scale and potential memory performance problem. [#1126](https://github.com/onevcat/Kingfisher/pull/1126)\n* Marking setter of `kf` wrapper as `nonmutating` and seperate value/reference version of `KingfisherCompatible`. This allows mutating properties on `kf` even with a `let` declaration. [#1134](https://github.com/onevcat/Kingfisher/pull/1134)\n* A regression which causes stack overflow when using `ImagePretcher` to load huge ammount of images. [#1143](https://github.com/onevcat/Kingfisher/pull/1143)\n\n---\n\n## [5.2.0 - Swift 5.0](https://github.com/onevcat/Kingfisher/releases/tag/5.2.0) (2019-02-27)\n\n#### Add\n* Compatible with Swift 5.0 and Xcode 10.2. Now Kingfisher builds against Swift 4.0, 4.2 and 5.0. [#1098](https://github.com/onevcat/Kingfisher/pull/1098)\n\n#### Fix\n* A possible dead lock when using `ImagePretcher` heavily in another thread. [#1122](https://github.com/onevcat/Kingfisher/pull/1122)\n* Redesign `Result` type based on Swift `Result` in standard library. Deprecate `value` and `error` getter for `Kingfisher.Result`.\n\n---\n\n## [5.1.1 - Racing](https://github.com/onevcat/Kingfisher/releases/tag/5.1.1) (2019-02-11)\n\n#### Fix\n* Deprecate incorrect `ImageCache` initializer with `path` parameter. Now use the `cacheDirectoryURL` version for clearer implemetation. [#1114](https://github.com/onevcat/Kingfisher/pull/1114/)\n* Fix a race condition when setting download delegate from multiple `ImagePrefetcher`s. [#1109](https://github.com/onevcat/Kingfisher/issues/1109)\n* Now `directoryURL` of disk storage backend is marked as public correctly. [#1108](https://github.com/onevcat/Kingfisher/issues/1108)\n\n---\n\n## [5.1.0 - Redirecting & Racing](https://github.com/onevcat/Kingfisher/releases/tag/5.1.0) (2019-01-12)\n\n#### Add\n* Add a `ImageDownloadRedirectHandler` for intercepting HTTP request which redirects. [#1072](https://github.com/onevcat/Kingfisher/pull/1072)\n\n#### Fix\n* Some thread racing when downloading and resetting images in the same image view. [#1089](https://github.com/onevcat/Kingfisher/pull/1089)\n\n---\n\n## [5.0.1 - Interweave](https://github.com/onevcat/Kingfisher/releases/tag/5.0.1) (2018-12-17)\n\n#### Fix\n* Retrieving images from cache now respect options `callbackQueue` setting. [#1066](https://github.com/onevcat/Kingfisher/issues/1066)\n* A crash when passing zero or negative size to `DownsamplingImageProcessor`. [#1073](https://github.com/onevcat/Kingfisher/issues/1073)\n\n---\n\n## [5.0.0 - Reborn](https://github.com/onevcat/Kingfisher/releases/tag/5.0.0) (2018-12-08)\n\n#### Add\n* Add `Result` type to Kingfisher. Now all callbacks in Kingfisher are using `Result` instead of tuples. This is more future-friendly and provides a modern way to make error handling better.\n* Make `KingfisherError` much more elaborate and accurate. Instead of simply provides the error code, now `KingfisherError` contains error reason and necessary associated values. It will help to understand and track the errors easier.\n* Better cache management by separating memory cache and disk cache to their own storages. Now you can use `MemoryStorage` and `DiskStorage` as the `ImageCache` backend.\n* Image cache of memory storage would be purged automatically in a certain time interval. This reduce memory pressure for other parts of your app.\n* The `ImageCache` is rewritten from scratch, to get benefit from new created `MemoryStorage` and `DiskStorage`. At the same time, this hybrid cache abstract keeps most API compatibility from the earlier versions.\n* Now the `ImageCache` can receive only raw `Data` object and cache it as needed.\n* A `KingfisherParsedOptionsInfo` type to parse `KingfisherOptionsInfoItem`s in related API. This improves reusability and performance when handling options in Kingfisher.\n* An option to specify whether an image should also prefetched to memory cache or not.\n* An option to make the disk file loading synchronously instead of in its own I/O queue.\n* Options to specify cache expiration for either memory cache or disk cache. This gives you a chance to control cache lifetime in a per-task grain size.\n* Add a default maximum size for memory cache. Now only at most 25% of your device memory will be used to kept images in memory. You can also change this value if needed.\n* An option to specify a processing queue for image processors. This gives your flexibility if you want to use main queue or if you want to dispatch the processing to a different queue.\n* A `DownsamplingImageProcessor` for downsampling an image to a given size before loading it to memory.\n* Add `ImageDataProvider` protocol to make it possible to provide image data locally instead of downloading from network. Several concrete types are provided to load images from data format. Use `LocalFileImageDataProvider` to load an image from a local disk path, `Base64ImageDataProvider` to load image from a Base64 string representation and `RawImageDataProvider` to provide a raw `Data object`.\n* A general `Source` protocol to define from where the image should be loaded. Currently, we support to load an image from `ImageDataProvider` or from network now.\n\n#### Fix\n* Now CommonCrypto from system is used to calculate file name from cache key, instead of using a customized hash algorithm.\n* An issue which causes `ImageDownloader` crashing when a lot of downloading tasks running at the same time.\n* All operations like image pretching and data receiving should now be performed in non-UI threads correctly.\n* Now `KingfisherCompatible` uses struct for `kf` namespacing for better performance.\n\n---\n\n## [4.10.1 - Time Machine](https://github.com/onevcat/Kingfisher/releases/tag/4.10.1) (2018-11-03)\n\n#### Fix\n* Add Swift 4 compatibility back.\n* Increase watchOS target to 3.0 in podspec.\n\n---\n\n## [4.10.0 - Swift 4.2](https://github.com/onevcat/Kingfisher/releases/tag/4.10.0) (2018-09-20)\n\n#### Add\n* Support for building with Xcode 10 and Swift 4.2. This version requires Xcode 10 or later with Swift 4.2 compiler.\n\n#### Fix\n* Improve performance when an invalide HTTP status code received. [#985](https://github.com/onevcat/Kingfisher/pull/985)\n\n---\n\n## [4.9.0 - Patience is a Virtue](https://github.com/onevcat/Kingfisher/releases/tag/4.9.0) (2018-09-04)\n\n#### Add\n* Add a `waitForCache` option to allowing cache callback called after cache operation finishes. [#963](https://github.com/onevcat/Kingfisher/pull/963)\n\n#### Fix\n* Animated image now will recognize `.once` and `.finite(1)` the same thing. [#982](https://github.com/onevcat/Kingfisher/pull/982)\n* Replace class-only protocol keyword with AnyObject as Swift convention. [#983](https://github.com/onevcat/Kingfisher/pull/983)\n* A wrong cache callback parameter when storing cache with background decoding. [#986](https://github.com/onevcat/Kingfisher/pull/986)\n* Access `downloadHolder` in a serial queue to avoid racing. [#984](https://github.com/onevcat/Kingfisher/pull/984)\n\n---\n\n## [4.8.1 - Prefetch Improvement](https://github.com/onevcat/Kingfisher/releases/tag/4.8.1) (2018-07-26)\n\n#### Fix\n* Fix a performance issue when prefetching images by moving related operation away from main queue. [#957](https://github.com/onevcat/Kingfisher/pull/957)\n* Improvement on stability of some test cases.\n\n---\n\n## [4.8.0 - Watch & Watching](https://github.com/onevcat/Kingfisher/releases/tag/4.8.0) (2018-05-15)\n\n#### Add\n* WKInterfaceImage setting image interface for watchOS. [#913](https://github.com/onevcat/Kingfisher/pull/913)\n* A new delegate method for watching `ImageDownloader` object completes a downloading request with success or failure. [#901](https://github.com/onevcat/Kingfisher/pull/901)\n\n#### Fix\n\n* Use newly created operation queue for downloader. \n* Filter.init(tranform:) is renamed to Filter.init(transform:) \n* Some internal minor fix on constant and typo, etc.\n\n---\n\n## [4.7.0 - Cancel All](https://github.com/onevcat/Kingfisher/releases/tag/4.7.0) (2018-04-06)\n\n#### Add\n* ImageDownloader now contains a method `cancelAll` to cancel all downloading tasks. [#894](https://github.com/onevcat/Kingfisher/pull/894)\n* Supports Swift 4.1 and Xcode 9.3. [#889](https://github.com/onevcat/Kingfisher/pull/889)\n\n---\n\n## [4.6.4 - Customize Activity Indicator](https://github.com/onevcat/Kingfisher/releases/tag/4.6.4) (2018-03-20)\n\n#### Fix\n* An issue caused customize activity indicator not working for Swift 4. [#872](https://github.com/onevcat/Kingfisher/issues/872)\n* Specify Swift compiler version explicity in pod spec file for CocoaPods 1.4. [#875](https://github.com/onevcat/Kingfisher/pull/875)\n\n---\n\n## [4.6.3 - Clean Demo](https://github.com/onevcat/Kingfisher/releases/tag/4.6.3) (2018-03-01)\n\n#### Fix\n* Move demo project out from Kingfisher framework project. [#867](https://github.com/onevcat/Kingfisher/pull/867)\n* An issue that caused stack overflow when prefetching too many images, while they are already cached. [#866](https://github.com/onevcat/Kingfisher/pull/866)\n\n---\n\n## [4.6.2 - GIF frames](https://github.com/onevcat/Kingfisher/releases/tag/4.6.2) (2018-02-14)\n\n#### Fix\n* Animated image view now will call finished delegate method in correct timing. [#860](https://github.com/onevcat/Kingfisher/issues/860)\n\n---\n\n## [4.6.1 - MD5](https://github.com/onevcat/Kingfisher/releases/tag/4.6.1) (2017-12-28)\n\n#### Fix\n* Revert to use non-dependency way to handle MD5, to solve issues which redefination of dependency library. [#834](https://github.com/onevcat/Kingfisher/pull/834)\n\n---\n\n## [4.6.0 - AniBird](https://github.com/onevcat/Kingfisher/releases/tag/4.6.0) (2017-12-27)\n\n#### Add\n* Delegate methods for `AnimatedImageView` to inspect finishing event and/or end of an animation loop. [#829](https://github.com/onevcat/Kingfisher/pull/829)\n\n#### Fix\n* Minor performance improvement by `final` some classes.\n* Remove unnecessary `Box` type since Objective-C world takes `Any`. [#832](https://github.com/onevcat/Kingfisher/pull/832).\n* Some internal failing tests on earlier macOS, in which color space giving different result.\n\n---\n\n## [4.5.0 - Blending](https://github.com/onevcat/Kingfisher/releases/tag/4.5.0) (2017-12-05)\n\n#### Add\n* New image processors to blend an image. See `BlendImageProcessor` on iOS/tvOS and `CompositingImageProcessor` on macOS. [#818](https://github.com/onevcat/Kingfisher/pull/818)\n\n#### Fix\n* A crash when prefetching too many images in a single batch. [#692](https://github.com/onevcat/Kingfisher/issues/692)\n* A possible invalid redeclaration on `Array` from `AnimatedImageView`. [#819](https://github.com/onevcat/Kingfisher/pull/819)\n\n---\n\n## [4.4.0 - Image Modifier](https://github.com/onevcat/Kingfisher/releases/tag/4.4.0) (2017-12-01)\n\n#### Add\n* Add `ImageModifier` to give a final chance for setting image object related properties just before getting back the image from either network or cache. [#810](https://github.com/onevcat/Kingfisher/issues/810)\n\n#### Fix\n* Apply scale on all image based processor methods, including the existing ones from memory cache. [#813](https://github.com/onevcat/Kingfisher/issues/813)\n\n---\n\n## [4.3.1 - Cache Regression](https://github.com/onevcat/Kingfisher/releases/tag/4.3.1) (2017-11-21)\n\n#### Fix\n* A regression introduced in 4.3.0 which cause the cache not working well for processed images.\n\n---\n\n## [4.3.0 - Memory Or Refresh](https://github.com/onevcat/Kingfisher/releases/tag/4.3.0) (2017-11-17)\n\n#### Add\n* An option for only getting cached images from memory or refresh it by downloading. It could be useful for fetching images behind the same URL while keeping to use the latest memory cached ones. [#806](https://github.com/onevcat/Kingfisher/pull/806)\n\n#### Fix\n* A problem when setting customized indicator with non-zero frame. Now the indicator will be no longer resized to image view size incorrectly. [#798](https://github.com/onevcat/Kingfisher/pull/798)\n* Improve store performance by avoiding re-encode images as long as the original data could be provided. [#805](https://github.com/onevcat/Kingfisher/pull/805)\n\n---\n\n## [4.2.0 - A Tale of Two Caches](https://github.com/onevcat/Kingfisher/releases/tag/4.2.0) (2017-10-22)\n\n#### Add\n* An option to provice a specific cache for original image. This gives us a change to caching original iamges on a different cache. [#794](https://github.com/onevcat/Kingfisher/pull/794)\n\n---\n\n## [4.1.1 - Love Barrier Again](https://github.com/onevcat/Kingfisher/releases/tag/4.1.1) (2017-10-17)\n\n#### Fix\n* A potential race condition in `ImageDownloader`. [#763](https://github.com/onevcat/Kingfisher/issues/763)\n\n---\n\n## [4.1.0 - Data in Hand](https://github.com/onevcat/Kingfisher/releases/tag/4.1.0) (2017-09-28)\n\n#### Add\n* An `ImageDownloader` delegate method to provide a chance for you to check and modify the data. [#773](https://github.com/onevcat/Kingfisher/pull/773)\n\n#### Fix\n* Now Kingfisher also supports Swift 3.2, as a workaround for CocoaPods not respecting pod spec build setting. [CocoaPods_#6791](https://github.com/CocoaPods/CocoaPods/issues/6791)\n\n---\n\n## [4.0.1 - Swift 4](https://github.com/onevcat/Kingfisher/releases/tag/4.0.1) (2017-09-15)\n\n#### Add\n* Supports for Swift 4. The new major version of Kingfisher should be source compatible with Kingfisher 3. Please make sure you have no warning left with Kingfisher related APIs before migrating to version 4, since all deprecated methods are removed from our code base. [#704](https://github.com/onevcat/Kingfisher/pull/704)\n* A cleaner API to track whether an image is cached and its cache type. Use `imageChachedType` and `CacheType.cached` instead of `isImageCached` and `CacheCheckResult`. [#704](https://github.com/onevcat/Kingfisher/pull/704/commits/38860911310931842f2d44e020204e894b7b2ae8)\n\n#### Fix\n* Update pod spec to use Swift 4.0 as Swift Version configuration.\n\n---\n\n## [4.0.0 - Swift 4](https://github.com/onevcat/Kingfisher/releases/tag/4.0.0) (2017-09-14)\n\n#### Add\n* Supports for Swift 4. The new major version of Kingfisher should be source compatible with Kingfisher 3. Please make sure you have no warning left with Kingfisher related APIs before migrating to version 4, since all deprecated methods are removed from our code base. [#704](https://github.com/onevcat/Kingfisher/pull/704)\n* A cleaner API to track whether an image is cached and its cache type. Use `imageChachedType` and `CacheType.cached` instead of `isImageCached` and `CacheCheckResult`. [#704](https://github.com/onevcat/Kingfisher/pull/704/commits/38860911310931842f2d44e020204e894b7b2ae8)\n\n---\n\n## [3.13.1 - Evil Setting](https://github.com/onevcat/Kingfisher/releases/tag/3.13.1) (2017-09-14)\n\n#### Fix\n* Disable code coverage for all targets in build setting to avoid rejecting from iTunes when building with Xcode 9. [#753](https://github.com/onevcat/Kingfisher/pull/753)\n\n---\n\n## [3.13.0 - Rum Bird](https://github.com/onevcat/Kingfisher/releases/tag/3.13.0) (2017-09-12)\n\n#### Add\n* Introduces a `backgroundColor` property to `RoundCornerImageProcessor` allowing to specify a desired backgroud color. It could be useful for a JPEG based image to prevent alpha blending. [#766](https://github.com/onevcat/Kingfisher/pull/766)\n\n---\n\n## [3.12.2 - Scaling Background Decoding](https://github.com/onevcat/Kingfisher/releases/tag/3.12.2) (2017-09-02)\n\n#### Fix\n* Fix an issue which causes image scale not correct when background decoding option is used. [#761](https://github.com/onevcat/Kingfisher/issues/761)\n\n---\n\n## [3.12.1 - Placeholder](https://github.com/onevcat/Kingfisher/releases/tag/3.12.1) (2017-08-30)\n\n#### Add\n* Now you could use a customized view (subclass of `UIView` or `NSView`) as placeholder in image view setting extension method. [#746](https://github.com/onevcat/Kingfisher/issues/746)\n\n---\n\n## [3.12.0 - Placeholder](https://github.com/onevcat/Kingfisher/releases/tag/3.12.0) (2017-08-30)\n\n#### Add\n* Now you could use a customized view (subclass of `UIView` or `NSView`) as placeholder in image view setting extension method. [#746](https://github.com/onevcat/Kingfisher/issues/746)\n\n---\n\n## [3.11.0 - Task Auth](https://github.com/onevcat/Kingfisher/releases/tag/3.11.0) (2017-08-16)\n\n#### Add\n* A task based authentication challenge handler for some auth methods like HTTP Digest. [#742](https://github.com/onevcat/Kingfisher/issues/742)\n\n#### Fix\n* The option of `keepCurrentImageWhileLoading` now will respect your placeholder if the original image is `nil` in the image view. [#747](https://github.com/onevcat/Kingfisher/pull/747)\n\n---\n\n## [3.10.4 - Indicator Size](https://github.com/onevcat/Kingfisher/releases/tag/3.10.4) (2017-07-26)\n\n#### Fix\n* Respect image and custom indicator size. Now Kingfisher will not resize the indicators to the image size for you automatically.\n\n---\n\n## [3.10.3 - ProMotion](https://github.com/onevcat/Kingfisher/releases/tag/3.10.3) (2017-07-06)\n\n#### Fix\n* Fix a problem which causes the GIF playing in a slow rate on ProMotion enabled devices (iPad Pro 10.5) [#718](https://github.com/onevcat/Kingfisher/issues/718)\n\n---\n\n## [3.10.2 - Missing Boys](https://github.com/onevcat/Kingfisher/releases/tag/3.10.2) (2017-06-16)\n\n#### Fix\n* Now the processed images result from a cache original image could be cached correctly. [#711](https://github.com/onevcat/Kingfisher/issues/711)\n* Some internal minor clean up.\n\n---\n\n## [3.10.1 - Order, order!](https://github.com/onevcat/Kingfisher/releases/tag/3.10.1) (2017-06-04)\n\n#### Fix\n* Change an inline function order to make Swift 3.0 compiler happy. [#700](https://github.com/onevcat/Kingfisher/issues/700)\n\n---\n\n## [3.10.0 - Hot Bird](https://github.com/onevcat/Kingfisher/releases/tag/3.10.0) (2017-06-03)\n\n#### Add\n* New cache retriving strategy for a request with certain `ImageProcessor` applied. Now Kingfisher will first try to get the processed images from cache. If not existing, it will be smart enough to check whether the original image exists in cache to avoid downloading it.\n* A `cacheOriginalImage` option to also cache original images while an `ImageProcessor` is applied. It is required if you want the new cache strategy. [#650](https://github.com/onevcat/Kingfisher/issues/650)\n* A `FormatIndicatedCacheSerializer` to serialize the image into a certain format (`png`, `jpg` or `gif`). [#693](https://github.com/onevcat/Kingfisher/issues/693)\n\n#### Fix\n* A timing issue when you try to cancel an on-going download task, and start the same one again immediately. Now the previous one will received an error and the later one could be completed normally. [#532](https://github.com/onevcat/Kingfisher/issues/532)\n* Fix the showing/hiding logic for activity indicator in image view to make them independent from race condition.\n* A possible race condition that accessing downloading fetch load conccurently.\n* Invalidate the download session when the downloader gets released. It might cause problem if you were using your own downloader instance.\n* Some internal stability improvement.\n\n---\n\n## [3.9.1 - Compatibility](https://github.com/onevcat/Kingfisher/releases/tag/3.9.1) (2017-05-13)\n\n#### Fix\n* Fix a problem which prevents building under Xcode 8.2 / Swift 3.0. [#677](https://github.com/onevcat/Kingfisher/issues/677)\n\n---\n\n## [3.9.0 - Follow the Rules](https://github.com/onevcat/Kingfisher/releases/tag/3.9.0) (2017-05-11)\n\n#### Add\n* A default option in `KingfisherManager` to let users set a global default option to all `KingfisherManager` related methods, as well as all UI extension methods. [#674](https://github.com/onevcat/Kingfisher/pull/674)\n\n#### Fix\n* Now the options appended will overwrite the previous one. This makes users be able to set proper options in a per-image-way, even when there is already a default option set in `KingfisherManager`.\n* Deprecate `requestsUsePipeling` in `ImageDownloader` since there was a typo. Now use `requestsUsePipelining` instead. [#673](https://github.com/onevcat/Kingfisher/pull/673)\n* Some internal improvement for private APIs.\n\n---\n\n## [3.8.0 - Prowess](https://github.com/onevcat/Kingfisher/releases/tag/3.8.0) (2017-05-10)\n\n#### Add\n* An API to apply rect round for specified corner in `RoundCornerImageProcessor`. Instead of making all four corners rounded, you can now set only some corners rounding. [#668](https://github.com/onevcat/Kingfisher/issues/668)\n\n---\n\n## [3.7.2 - Never Do Things by Halves](https://github.com/onevcat/Kingfisher/releases/tag/3.7.2) (2017-05-09)\n\n#### Fix\n* A wrong design which causes completion handler for previous downloading not called when setting to another url. [#665](https://github.com/onevcat/Kingfisher/issues/665)\n\n---\n\n## [3.7.1 - GIF is Animated](https://github.com/onevcat/Kingfisher/releases/tag/3.7.1) (2017-05-08)\n\n#### Fix\n* Deprecated `preloadAllGIFData`. Change to a more generic name `preloadAllAnimationData` since it could be used for other format with `ImageProcessor`. [#664](https://github.com/onevcat/Kingfisher/pull/664)\n\n---\n\n## [3.7.0 - Summer Bird](https://github.com/onevcat/Kingfisher/releases/tag/3.7.0) (2017-05-04)\n\n#### Add\n* A delegate method in `ImageDownloaderDelegate` to notify starting of a downloading progress.\n\n#### Fix\n* Better documentation for `Resource` parameter in image setting extension.\n\n---\n\n## [3.6.2 - Naughty CGImage](https://github.com/onevcat/Kingfisher/releases/tag/3.6.2) (2017-04-11)\n\n#### Fix\n* A problem in `CroppingImageProcessor` and `crop` method of images which crops wrong area for images with a non-`1` scale. [#649](https://github.com/onevcat/Kingfisher/pull/649)\n* Refactor for `ResizingImageProcessor`. `targetSize` of `ResizingImageProcessor` is now deprecated. Use `referenceSize` instead. It's just a name changing for clearer API. [#646](https://github.com/onevcat/Kingfisher/pull/646)\n\n---\n\n## [3.6.1 - Some Optimization](https://github.com/onevcat/Kingfisher/releases/tag/3.6.1) (2017-04-01)\n\n#### Fix\n* Fix warnings when build Kingfisher in Swift 3.1 compiler. [#632](https://github.com/onevcat/Kingfisher/pull/632)\n* Wrong size when decoding images with a passed-in scale option. [#633](https://github.com/onevcat/Kingfisher/pull/633)\n* Speed up MD5 calculation by turing to a pure Swift implementation. [#636](https://github.com/onevcat/Kingfisher/pull/636)\n* Host docs directly in GitHub. [#641](https://github.com/onevcat/Kingfisher/pull/641)\n\n---\n\n## [3.6.0 - Cropping](https://github.com/onevcat/Kingfisher/releases/tag/3.6.0) (2017-03-26)\n\n#### Add\n* A built-in image processor to crop images with a targeted size and anchor. [#465](https://github.com/onevcat/Kingfisher/issues/465)\n\n---\n\n## [3.5.2 - Bad Apple](https://github.com/onevcat/Kingfisher/releases/tag/3.5.2) (2017-03-09)\n\n#### Fix\n* An issue which causes app crashing while folder enumerating encountered an error in `ImageCache`. [#620](https://github.com/onevcat/Kingfisher/pull/620)\n\n---\n\n## [3.5.1 - Fast is better than slow](https://github.com/onevcat/Kingfisher/releases/tag/3.5.1) (2017-03-01)\n\n#### Fix\n* A minor improvement on slow compiling time due to a method in `Image`. [#611](https://github.com/onevcat/Kingfisher/issues/611)\n\n---\n\n## [3.5.0 - New age, new content](https://github.com/onevcat/Kingfisher/releases/tag/3.5.0) (2017-02-21)\n\n#### Add\n* Resizing processor now support to resize images with content mode. You could choose from `aspectFill`, `aspectFit` or just respect the target size. [#597](https://github.com/onevcat/Kingfisher/issues/597)\n\n#### Fix\n* A problem which might cause the downloaded image set unexpected for a cell which already not in use. [#598](https://github.com/onevcat/Kingfisher/pull/598)\n\n---\n\n## [3.4.0 - Spring is here](https://github.com/onevcat/Kingfisher/releases/tag/3.4.0) (2017-02-11)\n\n#### Add\n* Use the `onlyLoadFirstFrame` option to load only the first frame from a GIF file. It will be useful when you want to display a static preview of the first frame from a GIF image. By doing so, you could save huge ammount of memory. [#591](https://github.com/onevcat/Kingfisher/pull/591)\n\n#### Fix\n* Now `cancel` on a `RetrieveImageTask` will work properly even when the downloading not started for `UIButton` and `NSButton` too. [#580](https://github.com/onevcat/Kingfisher/pull/580)\n* Progress block of extensions setting methods will not be called multiple times if you set another task while the previous one still in downloading. [#583](https://github.com/onevcat/Kingfisher/pull/583)\n* Image cache will work properly when `ImagePrefetcher` trying to prefetch images with an `ImageProcessor`. Now the fetched and processed images could be retrieved correctly. [#590](https://github.com/onevcat/Kingfisher/pull/590)\n\n---\n\n## [3.3.4 - Cancellation means a new start!](https://github.com/onevcat/Kingfisher/releases/tag/3.3.4) (2017-02-04)\n\n#### Fix\n* Now `cancel` on a `RetrieveImageTask` will work properly even when the downloading not started. [#578](https://github.com/onevcat/Kingfisher/pull/578)\n* Use modern float constant of pi. [#576](https://github.com/onevcat/Kingfisher/pull/576)\n\n---\n\n## [3.3.3 - Xcode 8.0 is not dead yet](https://github.com/onevcat/Kingfisher/releases/tag/3.3.3) (2017-01-30)\n\n#### Fix\n* A type inference to make Kingfisher compiles on Xcode 8.0 again. [#572](https://github.com/onevcat/Kingfisher/issues/572)\n\n---\n\n## [3.3.2 - Upside Down](https://github.com/onevcat/Kingfisher/releases/tag/3.3.2) (2017-01-23)\n\n#### Fix\n* An issue which causes the background decoded images drawn upside down.\n\n---\n\n## [3.3.1 - Lunar Eve](https://github.com/onevcat/Kingfisher/releases/tag/3.3.1) (2017-01-21)\n\n#### Add\n* Expose default `pngRepresentation`, `jpegRepresentation` and `gifRepresentation` as public. [#560](https://github.com/onevcat/Kingfisher/pull/560)\n* Support unlimited disk cache duration. [#566](https://github.com/onevcat/Kingfisher/pull/566)\n\n#### Fix\n* A mismatch of CG image component when creating `CGContext` for blur filter. [#567](https://github.com/onevcat/Kingfisher/pull/567)\n* Remove test images from repo to keep slim. [#568](https://github.com/onevcat/Kingfisher/pull/568)\n\n---\n\n## [3.3.0 - Lunar Eve](https://github.com/onevcat/Kingfisher/releases/tag/3.3.0) (2017-01-21)\n\n#### Add\n* Expose default `pngRepresentation`, `jpegRepresentation` and `gifRepresentation` as public. [#560](https://github.com/onevcat/Kingfisher/pull/560)\n* Support unlimited disk cache duration. [#566](https://github.com/onevcat/Kingfisher/pull/566)\n\n#### Fix\n* A mismatch of CG image component when creating `CGContext` for blur filter. [#567](https://github.com/onevcat/Kingfisher/pull/567)\n* Remove test images from repo to keep slim. [#568](https://github.com/onevcat/Kingfisher/pull/568)\n\n---\n\n## [3.2.4 - Love SPM again](https://github.com/onevcat/Kingfisher/releases/tag/3.2.4) (2016-12-22)\n\n#### Fix\n* A problem that causes framework cannot be compiled by Swift Package Manager. [#547](https://github.com/onevcat/Kingfisher/issues/547)\n* Removed an unused parameter from round corner image API. [#548](https://github.com/onevcat/Kingfisher/issues/548)\n\n---\n\n## [3.2.3 - LI ZHENG](https://github.com/onevcat/Kingfisher/releases/tag/3.2.3) (2016-12-20)\n\n#### Fix\n* An issue which caused processed images igoring exif orientation information. [#535](https://github.com/onevcat/Kingfisher/issues/535)\n\n---\n\n## [3.2.2 - Faster GIF](https://github.com/onevcat/Kingfisher/releases/tag/3.2.2) (2016-12-02)\n\n#### Fix\n* Improve preload animated image loading strategy by using background queue. This should improve framerate when loading a lot of GIF files in the same time. [#529](https://github.com/onevcat/Kingfisher/pull/529)\n* Make `ImageDownloader` a pure Swift class to avoid the SDK bug which might leak memory in iOS 10. [#520](https://github.com/onevcat/Kingfisher/issues/520)\n* Fix some typos. [#523](https://github.com/onevcat/Kingfisher/issues/523)\n\n---\n\n## [3.2.1 - Helper Helps](https://github.com/onevcat/Kingfisher/releases/tag/3.2.1) (2016-11-14)\n\n#### Add\n* A new set of `KingfisherOptionsInfo` extension helpers to extract options easiser. It will be useful when you are trying to implement your own processors or serializers. [#505](https://github.com/onevcat/Kingfisher/issues/505)\n* Mark the empty task for downloader as `public`. [#508](https://github.com/onevcat/Kingfisher/issues/508)\n\n#### Fix\n* Set placeholder image even when the input resource is `nil`. This is a regression from version 3.2.0. [#510](https://github.com/onevcat/Kingfisher/issues/510)\n\n---\n\n## [3.2.0 - Quiet](https://github.com/onevcat/Kingfisher/releases/tag/3.2.0) (2016-11-07)\n\n#### Add\n* A new option to ignore placeholder and keep current image while loading/downloading a new one. This would be useful when you want to display the earlier image while loading a new one. [494](https://github.com/onevcat/Kingfisher/issues/494)\n* A disk cache path closure to let you fully customize the disk cache path. [#499](https://github.com/onevcat/Kingfisher/pull/499)\n\n#### Fix\n* Move methods which were marked as `open` to their class defination scope, to avoid the compiler restriction when overridden. [#500](https://github.com/onevcat/Kingfisher/pull/500)\n\n---\n\n## [3.1.4 - CIImageProcessor with Data](https://github.com/onevcat/Kingfisher/releases/tag/3.1.4) (2016-10-19)\n\n#### Fix\n* Fix a problem that `CIImageProcessor` not get called when feeding data to the processor. [#485](https://github.com/onevcat/Kingfisher/issues/485)\n\n---\n\n## [3.1.3 - Collocalia](https://github.com/onevcat/Kingfisher/releases/tag/3.1.3) (2016-10-06)\n\n#### Fix\n* A compiling time issue. Now the compile time of Kingfisher should drop dramatically. [#467](https://github.com/onevcat/Kingfisher/pull/467)\n* kf wrapper of all Kingfisher compatible types now a class instead of struct, to make mutating opearation on it possible. [#469](https://github.com/onevcat/Kingfisher/issues/469)\n\n#### Remove\n* requestModifier of `ImageDownloader` is removed to prevent leading to misunderstanding.\n\n---\n\n## [3.1.1 - Kingfisher likes more](https://github.com/onevcat/Kingfisher/releases/tag/3.1.1) (2016-09-28)\n\n#### Fix\n* An issue which prevents using multiple image processors at the same time. Now you can use different `ImageProcessor` at the same time for an image, while keeping high performance since only one downloading process would be fired. [#460](https://github.com/onevcat/Kingfisher/pull/460)\n* A crash when processing some images with built-in `ResizingImageProcessor` and `OverlayImageProcessor` while the input images not having a standard format. [#440](https://github.com/onevcat/Kingfisher/issues/440), [#461](https://github.com/onevcat/Kingfisher/pull/461)\n* ImageCache could accept a path extension as key now. [#456](https://github.com/onevcat/Kingfisher/pull/456)\n\n---\n\n## [3.1.0 - Namespace](https://github.com/onevcat/Kingfisher/releases/tag/3.1.0) (2016-09-21)\n\n#### Add\n* Add `kf` namespace for all extension APIs in Kingfisher. Now no need to worry about name conflicting any more. [#435](https://github.com/onevcat/Kingfisher/pull/435)\n\n#### Fix\n* Mark `AnimateImageView` to open so you can extend this class again. [#442](https://github.com/onevcat/Kingfisher/pull/442)\n* Update demo code to adopt iOS 10 prefetching cell feature and new cell life cycle. [#447](https://github.com/onevcat/Kingfisher/issues/447)\n\n#### Remove\n* Since `kf` namespace is added, all original `kf_` prefix methods are marked as deprecated.\n\n---\n\n## [3.0.1 - New Age - Swift 3](https://github.com/onevcat/Kingfisher/releases/tag/3.0.1) (2016-09-14)\n\n#### Add\n* Swift 3 compatibility. This version follows Swift 3 API design guideline as well as contains a lot of breaking changes from version 2.x. See [Kingfisher 3.0 Migration Guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-3.0-Migration-Guide) for more about how to migrate your project to 3.0. Kingfisher 2.6.x is still supporting both Swift 2.2 and 2.3.\n* Image Processor. Now you can specify an image processor and it will be used to process images after downloaded. It is useful when you need to apply some transforming or filter to the image. You can also use the processor to support any other image format, like WebP. See [Processor](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#processor) section in the wiki for more. The default processor should behave the same as before. [#420](https://github.com/onevcat/Kingfisher/pull/420)\n* Built-in processors from simple round corner and resizing to filters like tint and blur. Check [Built-in processors of Kingfisher](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#built-in-processors-of-kingfisher) for more.\n* Cache Serializer. [CacheSerializer](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#serializer) will be used to convert some data to an image object for retrieving from disk cache and vice versa for storing to disk cache.\n* New indicator type. Now you should be able to use your own indicators. [#430](https://github.com/onevcat/Kingfisher/pull/430)\n* ImageDownloadRequestModifier. Use this protocol to modify requests being sent to your server.\n\n#### Fix\n* Resource is now a protocol instead of a struct. Use `ImageResource` for your original `Resource` type. And now `URL` conforms `Resource` so the APIs could be clearer.\n* Now Kingfisher cache will store re-encoded image data instead of the original data by default. This is needed due to we want to store the processed data from `ImageProcessor`. If this is not what you want, you should supply your customized instanse of `CacheSerializer`.\n\n#### Remove\n* KingfisherManager.init is removed since you should never create your own manager.\n* cachedImageExistsforURL in `ImageCache` is removed since it introduced unnecessary coupling. Use `isImageCached` instead.\n* requestModifier` is removed. Use `.requestModifier` and pass a `ImageDownloadRequestModifier`.\n* kf_showIndicatorWhenLoading is removed since we have a better and flexible way to use indicator by `kf_indicatorType`.\n\n---\n\n## [3.0.0 - New Age - Swift 3](https://github.com/onevcat/Kingfisher/releases/tag/3.0.0) (2016-09-14)\n\n#### Add\n* Swift 3 compatibility. This version follows Swift 3 API design guideline as well as contains a lot of breaking changes from version 2.x. See [Kingfisher 3.0 Migration Guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-3.0-Migration-Guide) for more about how to migrate your project to 3.0. Kingfisher 2.6.x is still supporting both Swift 2.2 and 2.3.\n* Image Processor. Now you can specify an image processor and it will be used to process images after downloaded. It is useful when you need to apply some transforming or filter to the image. You can also use the processor to support any other image format, like WebP. See [Processor](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#processor) section in the wiki for more. The default processor should behave the same as before. [#420](https://github.com/onevcat/Kingfisher/pull/420)\n* Built-in processors from simple round corner and resizing to filters like tint and blur. Check [Built-in processors of Kingfisher](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#built-in-processors-of-kingfisher) for more.\n* Cache Serializer. [CacheSerializer](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#serializer) will be used to convert some data to an image object for retrieving from disk cache and vice versa for storing to disk cache.\n* New indicator type. Now you should be able to use your own indicators. [#430](https://github.com/onevcat/Kingfisher/pull/430)\n* ImageDownloadRequestModifier. Use this protocol to modify requests being sent to your server.\n\n#### Fix\n* Resource is now a protocol instead of a struct. Use `ImageResource` for your original `Resource` type. And now `URL` conforms `Resource` so the APIs could be clearer.\n* Now Kingfisher cache will store re-encoded image data instead of the original data by default. This is needed due to we want to store the processed data from `ImageProcessor`. If this is not what you want, you should supply your customized instanse of `CacheSerializer`.\n\n---\n\n## [2.6.0 - Indicator Customization](https://github.com/onevcat/Kingfisher/releases/tag/2.6.0) (2016-09-12)\n\n#### Add\n* Support for different types of indicators, including gif images. [#425](https://github.com/onevcat/Kingfisher/pull/425)\n\n---\n\n## [2.5.1 - Prefetcher Trap](https://github.com/onevcat/Kingfisher/releases/tag/2.5.1) (2016-09-06)\n\n#### Fix\n* Fix a possible trap of range making in prefetcher. [#422](https://github.com/onevcat/Kingfisher/pull/422)\n\n---\n\n## [2.5.0 - Swift 2.3](https://github.com/onevcat/Kingfisher/releases/tag/2.5.0) (2016-08-29)\n\n#### Add\n* Support for Swift 2.3\n\n---\n\n## [2.4.3 - Longer Cache](https://github.com/onevcat/Kingfisher/releases/tag/2.4.3) (2016-08-17)\n\n#### Fix\n* The disk cache now will use access date for expiring checking, which should work better than modification date. [#381](https://github.com/onevcat/Kingfisher/issues/381) [#405](https://github.com/onevcat/Kingfisher/issues/405)\n\n---\n\n## [2.4.2 - Optional Welcome](https://github.com/onevcat/Kingfisher/releases/tag/2.4.2) (2016-07-10)\n\n#### Add\n* Accept `nil` as valid URL parameter for image view's extension methods.\n\n#### Fix\n* The completion handler of image view setting method will not be called any more if `self` is released.\n* Improve empty task so some performance improvment could be achieved.\n* Remove SwiftLint since it keeps adding new rules but without a back compatible support. It makes the users confusing when using a different version of SwiftLint.\n* Removed Implicit Unwrapping of CacheType that caused crashes if the image is not cached.\n\n---\n\n## [2.4.1 - Force Transition](https://github.com/onevcat/Kingfisher/releases/tag/2.4.1) (2016-05-10)\n\n#### Add\n* An option (`ForceTransition`) to force image setting for an image view with transition. By default the transition will only happen when downloaded. [#317](https://github.com/onevcat/Kingfisher/pull/317)\n\n---\n\n## [2.4.0 - Animate Me](https://github.com/onevcat/Kingfisher/releases/tag/2.4.0) (2016-05-04)\n\n#### Add\n* A standalone `AnimatedImageView` to reduce memory usage when parsing and displaying GIF images. See README for more about using Kingfisher for GIF images. [#300](https://github.com/onevcat/Kingfisher/pull/300)\n\n#### Fix\n* An issue which may cause iOS app crasing when switching background/foreground multiple times. [#309](https://github.com/onevcat/Kingfisher/pull/309)\n* Change license of String+MD5.swift to a more precise one. [#302](https://github.com/onevcat/Kingfisher/issues/302)\n\n---\n\n## [2.3.1 - Pod Me up](https://github.com/onevcat/Kingfisher/releases/tag/2.3.1) (2016-04-22)\n\n#### Fix\n* Exclude NSButton extension from no related target. [#292](https://github.com/onevcat/Kingfisher/pull/292)\n\n---\n\n## [2.3.0 - Warmly Welcome](https://github.com/onevcat/Kingfisher/releases/tag/2.3.0) (2016-04-21)\n\n#### Add\n* Add support for App Extension target. [#290](https://github.com/onevcat/Kingfisher/pull/290)\n* Add support for NSButton. [#287](https://github.com/onevcat/Kingfisher/pull/287)\n\n---\n\n## [2.2.2 - Spring Bird II](https://github.com/onevcat/Kingfisher/releases/tag/2.2.2) (2016-04-06)\n\n#### Fix\n* Add default values to optional parameters, which should be a part of 2.2.1. [#284](https://github.com/onevcat/Kingfisher/issues/284)\n\n---\n\n## [2.2.1 - Spring Bird](https://github.com/onevcat/Kingfisher/releases/tag/2.2.1) (2016-04-06)\n\n#### Fix\n* A memory leak caused by closure based Generator. [#281](https://github.com/onevcat/Kingfisher/pull/281)\n* Remove duplicated APIs since auto completion gets improved in Swift 2.2. [#283](https://github.com/onevcat/Kingfisher/pull/283)\n* Enable all recongnized format for `UIImage`. [#278](https://github.com/onevcat/Kingfisher/pull/278)\n\n---\n\n## [2.2.0 - Open Source Swift](https://github.com/onevcat/Kingfisher/releases/tag/2.2.0) (2016-03-24)\n\n#### Add\n* Compatible with latest Swift 2.2 and Xcode 7.3. [#270](https://github.com/onevcat/Kingfisher/pull/270). If you need to use Kingfisher in Swift 2.1, please consider to pin to version 2.1.0.\n\n#### Fix\n* A trivial issue that a context holder should not exist when decoding images background.\n\n---\n\n## [2.1.0 - Prefetching](https://github.com/onevcat/Kingfisher/releases/tag/2.1.0) (2016-03-10)\n\n#### Add\n* Add `ImagePrefetcher` and related prefetching methods to allow downloading and caching images before you need to display them. [#249](https://github.com/onevcat/Kingfisher/pull/249)\n* A protocol (`AuthenticationChallengeResponable`) for responsing authentication challenge. You can now set `authenticationChallengeResponder` of `ImageDownloader` and use your own authentication policy. [#226](https://github.com/onevcat/Kingfisher/issues/226)\n* An API (`cachePathForKey(:)`) to get real path for a specified key in a cache. [#256](https://github.com/onevcat/Kingfisher/pull/256)\n\n#### Fix\n* Disable background decoding for images from memory cache. This improves the performance of image loading for in-memory cached images and fix a flicker when you try to load image with background decoding. [#257](https://github.com/onevcat/Kingfisher/pull/257)\n* A potential crash in `ImageCache` when an empty image is passed into.\n\n---\n\n## [2.0.4 - Sorry Pipelining](https://github.com/onevcat/Kingfisher/releases/tag/2.0.4) (2016-02-27)\n\n#### Fix\n* Make pipeling support to be disabled by default since it requiring server support. You can enable it by setting `requestsUsePipeling` in `ImageDownloader`. [#253](https://github.com/onevcat/Kingfisher/pull/253)\n* Image transition now allows user interaction. [#252](https://github.com/onevcat/Kingfisher/pull/252)\n\n---\n\n## [2.0.3 - Holiday Issues](https://github.com/onevcat/Kingfisher/releases/tag/2.0.3) (2016-02-17)\n\n#### Fix\n* A memory leak caused by retain cycle of downloader session and its delegate. [#235](https://github.com/onevcat/Kingfisher/issues/235)\n* Now the `callbackDispatchQueue` in option should be applied to `ImageDownloader` as well. [#238](https://github.com/onevcat/Kingfisher/pull/238) and [#240](https://github.com/onevcat/Kingfisher/pull/240)\n* Fix warnings when the latest version of SwiftLint is used. [#189](https://github.com/onevcat/Kingfisher/issues/189#issuecomment-185205010)\n\n---\n\n## [2.0.2 - Single Frame GIF](https://github.com/onevcat/Kingfisher/releases/tag/2.0.2) (2016-02-14)\n\n#### Fix\n* An issue which causes GIF images with only one frame failing to be loaded correctly. [#231](https://github.com/onevcat/Kingfisher/issues/231)\n\n---\n\n## [2.0.1 - Disk is back](https://github.com/onevcat/Kingfisher/releases/tag/2.0.1) (2016-01-28)\n\n#### Fix\n* An issue which causes the downloaded image not cached in disk. [#224](https://github.com/onevcat/Kingfisher/pull/224)\n\n---\n\n## [2.0.0 - Kingfisher 2](https://github.com/onevcat/Kingfisher/releases/tag/2.0.0) (2016-01-23)\n\n#### Add\n* OS X support. Now Kingfisher can work seamlessly for `NSImage`. [#201](https://github.com/onevcat/Kingfisher/pull/201)\n* watchOS 2.x support. [#210](https://github.com/onevcat/Kingfisher/pull/210)\n* Swift Package Manager support. [#218](https://github.com/onevcat/Kingfisher/issues/218)\n* Unified `KingfisherOptionsInfo` API. Now all options across the framework are represented by `KingfisherOptionsInfo` with type same behavior. [#194](https://github.com/onevcat/Kingfisher/pull/194)\n* API for changing download priority of image download task after the download started. [#73](https://github.com/onevcat/Kingfisher/issues/73)\n* You can cancel image or background image downloading task now for button as well. [#205](https://github.com/onevcat/Kingfisher/issues/205)\n\n#### Fix\n* A potential thread issue when asking for cache state right after downloading finished.\n* Improve MD5 calculating speed. [#220](https://github.com/onevcat/Kingfisher/pull/220)\n* The scale was not correct when processing GIF files.\n\n---\n\n## [1.9.3](https://github.com/onevcat/Kingfisher/releases/tag/1.9.3) (2016-01-22)\n\n#### Fix\n* Stop indicator animation when loading failed. [#215](https://github.com/onevcat/Kingfisher/issues/215)\n\n---\n\n## [1.9.2 - IOIOIO](https://github.com/onevcat/Kingfisher/releases/tag/1.9.2) (2016-01-14)\n\n#### Fix\n* A potential issue causes image cache checking method not working when the image just stored.\n* Better performance and image quality when storing images with original data.\n\n---\n\n## [1.9.1 - You happy, I happy](https://github.com/onevcat/Kingfisher/releases/tag/1.9.1) (2016-01-04)\n\n#### Fix\n* Making SwiftLint happy when building with Carthage. #189\n\n---\n\n## [1.9.0 - What a Task](https://github.com/onevcat/Kingfisher/releases/tag/1.9.0) (2015-12-31)\n\n#### Add\n* Download methods in `ImageDownloader` now returns a cancelable task. So you can cancel the downloading process when using downloader separately.\n* Add a cancelling method in image view extension for easier cancel operation.\n* Mark some properties of downloading task as public.\n\n#### Fix\n* Cancelling of image downloading now triggers completion handler with `NSURLErrorCancelled` correctly now.\n\n---\n\n## [1.8.5 - Single Dog](https://github.com/onevcat/Kingfisher/releases/tag/1.8.5) (2015-12-16)\n\n#### Fix\n* Use single url session to download images.\n* Ignore and return error immediately for empty URL.\n* Internal update for testing stability and code style.\n\n---\n\n## [1.8.4 - GIF is GIF](https://github.com/onevcat/Kingfisher/releases/tag/1.8.4) (2015-12-12)\n\n#### Fix\n* Opt out the normalization and decoding for GIF, which would lead an issue that the GIF images missing.\n* Proper cost count for GIF image.\n\n\n---\n\n## [1.8.3 - Internal beauty](https://github.com/onevcat/Kingfisher/releases/tag/1.8.3) (2015-12-05)\n\n#### Fix\n* Fix for code base styles and formats.\n\n---\n\n## [1.8.2 - Path matters](https://github.com/onevcat/Kingfisher/releases/tag/1.8.2) (2015-11-19)\n\n#### Add\n* Cache path is customizable now. You can use Document folder to cache user generated images (But be caution that the disk cache files might be removed if limitation condition met).\n\n\n---\n\n## [1.8.1 - Transition needs rest](https://github.com/onevcat/Kingfisher/releases/tag/1.8.1) (2015-11-13)\n\n#### Fix\n* Only apply transition when images are downloaded. It will not show transition animation now if images loaded from either memory or disk cache now.\n* Code style.\n\n---\n\n## [1.8.0 - Bigger is Better](https://github.com/onevcat/Kingfisher/releases/tag/1.8.0) (2015-11-07)\n\n#### Add\n* Support for tvOS. Now enjoy downloading and cacheing images in the tvOS.\n\n#### Fix\n* An issue which causes images not stored properly if the original data is not supplied. #142\n\n---\n\n## [1.7.1 - EXIF is JPEG!](https://github.com/onevcat/Kingfisher/releases/tag/1.7.1) (2015-10-27)\n\n#### Fix\n* EXIF JPEG support which was broken in 1.7.0.\n* Correct timing of completion handler for use case with transition of UIImageView extension.\n\n---\n\n## [1.7.0 - Kingfisher with animation](https://github.com/onevcat/Kingfisher/releases/tag/1.7.0) (2015-10-25)\n\n#### Add\n* GIF support. Now you can download and show an animated GIF by Kingfisher `UIImageView` extension.\n\n#### Fix\n* Type safe options.\n* A potential retain of cache in loading task.\n\n---\n\n## [1.6.1 - No More Blinking](https://github.com/onevcat/Kingfisher/releases/tag/1.6.1) (2015-10-09)\n\n#### Fix\n* The blinking when reloading images in a cell.\n* Indicator is now in center of image view.\n\n---\n\n## [1.6.0 - Transition](https://github.com/onevcat/Kingfisher/releases/tag/1.6.0) (2015-09-19)\n\n#### Add\n* Add transition option. You can now use some view transition (like fade in) easier.\n\n#### Fix\n* Image data presenting when storing in disk.\n\n---\n\n## [1.5.0 - Swift 2.0](https://github.com/onevcat/Kingfisher/releases/tag/1.5.0) (2015-09-10)\n\n#### Add\n* Support for Swift 2.0.\n\n#### Fix\n* Remove the disk retrieve task canceling temporarily since there is an issue in Xcode 7 beta.\n* Remove support for watchOS since it now requires a separated framework. It will be added later as a standalone library instead a fat one.\n\n---\n\n## [1.4.5 - Key decoupling](https://github.com/onevcat/Kingfisher/releases/tag/1.4.5) (2015-08-14)\n\n#### Fix\n* Added resource APIs so you can specify a cacheKey for an image. The default implementation will use the URL string as key.\n\n---\n\n## [1.4.4 - Bug fix release](https://github.com/onevcat/Kingfisher/releases/tag/1.4.4) (2015-08-07)\n\n#### Fix\n* Explicitly type casting in ImageCache. #86\n\n---\n\n## [1.4.3](https://github.com/onevcat/Kingfisher/releases/tag/1.4.0) (2015-08-06)\n\n#### Fix\n* Fix orientation of PNG files.\n* Indicator hiding logic.\n\n---\n\n## [1.4.2 - Scaling](https://github.com/onevcat/Kingfisher/releases/tag/1.4.0) (2015-07-09)\n\n#### Add\n* Support for store and decode with scale parameter.\n\n#### Fix\n* A retain cycle which prevents image retrieving task releasing.\n\n---\n\n## [1.4.1](https://github.com/onevcat/Kingfisher/releases/tag/1.4.1) (2015-05-12)\n\n#### Fix\n* Fix library dependency to weak link for WatchKit.\n\n---\n\n## [1.4.0 - Hello, Apple Watch](https://github.com/onevcat/Kingfisher/releases/tag/1.4.0) (2015-05-11)\n\n#### Add\n* Apple Watch support and category on `WKInterfaceImage`.\n\n---\n\n## [1.3.1](https://github.com/onevcat/Kingfisher/releases/tag/1.3.1) (2015-05-06)\n\n#### Fix\n* Fix tests for CI.\n\n---\n\n## [1.3.0 - 304? What is 304?](https://github.com/onevcat/Kingfisher/releases/tag/1.3.0) (2015-05-01)\n\n#### Add\n* ImageDownloaderDelegate for getting information from response.\n* A cacheType key in completion handler to let you know which does the image come from.\n* A notification when disk images are cleaned due to image expired or size exceeded.\n\n#### Fix\n* Changed `ForceRefresh` behavior to respect server response when got a 304.\n* Documentation and test coverage.\n\n---\n\n## [1.2.0 - More, I need more!](https://github.com/onevcat/Kingfisher/releases/tag/1.2.0) (2015-04-24)\n\n#### Add\n* Multiple cache/downloader system. You can know specify the cache/downloader you need to use for each image request. It will be useful if you need different cache or download policy for different images.\n* Changed `Options` to `OptionsInfo` for flexible options passing.\n\n#### Fix\n* An issue which preventing image downloading when modifying the url of request.\n\n### Deprecate\n* All extension methods with `KingfisherOptions` are deprecated now. Use `KingfisherOptionsInfo` instead.\n\n---\n\n## [1.1.3 - Internal is Important](https://github.com/onevcat/Kingfisher/releases/tag/1.1.3) (2015-04-23)\n\n#### Fix\n* Update the naming convention used in internal queues, for easier debug purpose.\n* Fix some tests.\n\n---\n\n## [1.1.2 - Who cares disk size](https://github.com/onevcat/Kingfisher/releases/tag/1.1.1) (2015-04-21)\n\n#### Add\n* API for calculation total disk cache size.\n* API for modifying request before sending it.\n* Handle challenge when accessing a server trust site.\n\n#### Fix\n* Fix grammar in README.\n* Fix demo project to make it simpler.\n\n---\n\n## [1.1.1](https://github.com/onevcat/Kingfisher/releases/tag/1.1.1) (2015-04-17)\n\n#### Fix\n* Update PodSpec version\n\n---\n\n## [1.1.0 - Not only image](https://github.com/onevcat/Kingfisher/releases/tag/1.1.0) (2015-04-17)\n\n#### Add\n* UIButton extension.\n\n#### Fix\n* Fix typo in project.\n* Improve documentation.\n\n---\n\n## [1.0.0 - Kingfisher, take off](https://github.com/onevcat/Kingfisher/releases/tag/1.0.0) (2015-04-13)\n\nFirst public release.\n\n\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribute\n\n## Introduction\n\nFirst, thank you for considering contributing to Kingfisher! It's people like you that make the open source community such a great community! 😊\n\nWe welcome any type of contribution, not only code. You can help with \n- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open)\n- **Marketing**: writing blog posts, howto's, printing stickers, ...\n- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ...\n- **Code**: take a look at the [open issues](https://github.com/onevcat/Kingfisher/issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them.\n- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/Kingfisher).\n\n## Your First Contribution\n\nWorking on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).\n\n## Submitting code\n\nAny code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests.\n\n## Code review process\n\nThe bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge.\nIt is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you?\n\n## Financial contributions\n\nWe also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/Kingfisher).\nAnyone can file an expense. If the expense makes sense for the development of the community, it will be \"merged\" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.\n\n## Questions\n\nIf you have any questions, create an [issue](https://github.com/onevcat/Kingfisher/issues/new) (protip: do a quick search first to see if someone else didn't ask the same question before!).\nYou can also reach us at onev@onevcat.com.\n\n## Credits\n\n### Contributors\n\nThank you to all the people who have already contributed to Kingfisher!\n<a href=\"graphs/contributors\"><img src=\"https://opencollective.com/Kingfisher/contributors.svg?width=890\" /></a>\n\n\n### Backers\n\nThank you to all our backers! [[Become a backer](https://opencollective.com/Kingfisher#backer)]\n\n<a href=\"https://opencollective.com/Kingfisher#backers\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/backers.svg?width=890\"></a>\n\n\n### Sponsors\n\nThank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/Kingfisher#sponsor))\n\n<a href=\"https://opencollective.com/Kingfisher/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/1/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/2/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/3/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/4/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/5/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/6/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/7/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/8/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Kingfisher/sponsor/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/Kingfisher/sponsor/9/avatar.svg\"></a>\n\n<!-- This `CONTRIBUTING.md` is based on @nayafia's template https://github.com/nayafia/contributing-template -->\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/AppDelegate.swift",
    "content": "//\n//  AppDelegate.swift\n//  Kingfisher-Demo\n//\n//  Created by Wei Wang on 15/4/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n    var window: UIWindow?\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {\n        return true\n    }\n    \n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"23727\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" useSafeAreas=\"YES\" colorMatched=\"YES\" initialViewController=\"peg-r0-mlo\">\n    <device id=\"retina5_5\" orientation=\"portrait\" appearance=\"dark\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"23721\"/>\n        <capability name=\"Safe area layout guides\" minToolsVersion=\"9.0\"/>\n        <capability name=\"System colors in document resources\" minToolsVersion=\"11.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--Normal Loading View Controller-->\n        <scene sceneID=\"q9F-GD-CcN\">\n            <objects>\n                <collectionViewController id=\"IdS-po-MDe\" customClass=\"NormalLoadingViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <collectionView key=\"view\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" dataMode=\"prototypes\" id=\"cQA-0g-Ig3\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <collectionViewFlowLayout key=\"collectionViewLayout\" minimumLineSpacing=\"10\" minimumInteritemSpacing=\"10\" id=\"46n-QC-xKN\">\n                            <size key=\"itemSize\" width=\"250\" height=\"250\"/>\n                            <size key=\"headerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <size key=\"footerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <inset key=\"sectionInset\" minX=\"0.0\" minY=\"10\" maxX=\"0.0\" maxY=\"10\"/>\n                        </collectionViewFlowLayout>\n                        <cells>\n                            <collectionViewCell opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" reuseIdentifier=\"collectionViewCell\" id=\"jYH-ix-b6K\" customClass=\"ImageCollectionViewCell\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\">\n                                <rect key=\"frame\" x=\"82\" y=\"10\" width=\"250\" height=\"250\"/>\n                                <autoresizingMask key=\"autoresizingMask\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"5wL-TX-hML\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                        </imageView>\n                                    </subviews>\n                                </view>\n                                <constraints>\n                                    <constraint firstAttribute=\"bottom\" secondItem=\"5wL-TX-hML\" secondAttribute=\"bottom\" id=\"6Fx-JY-GDB\"/>\n                                    <constraint firstAttribute=\"trailing\" secondItem=\"5wL-TX-hML\" secondAttribute=\"trailing\" id=\"Fev-Dq-rlR\"/>\n                                    <constraint firstItem=\"5wL-TX-hML\" firstAttribute=\"top\" secondItem=\"jYH-ix-b6K\" secondAttribute=\"top\" id=\"e9v-9B-3Ji\"/>\n                                    <constraint firstItem=\"5wL-TX-hML\" firstAttribute=\"leading\" secondItem=\"jYH-ix-b6K\" secondAttribute=\"leading\" id=\"rxf-rn-zPF\"/>\n                                </constraints>\n                                <connections>\n                                    <outlet property=\"cellImageView\" destination=\"5wL-TX-hML\" id=\"vlC-e9-WbX\"/>\n                                </connections>\n                            </collectionViewCell>\n                        </cells>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"IdS-po-MDe\" id=\"FRQ-Vg-rwJ\"/>\n                            <outlet property=\"delegate\" destination=\"IdS-po-MDe\" id=\"VdT-PX-8WU\"/>\n                        </connections>\n                    </collectionView>\n                    <navigationItem key=\"navigationItem\" id=\"tBW-1C-FRK\"/>\n                </collectionViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"Djx-Kg-Zzu\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1851\" y=\"-451\"/>\n        </scene>\n        <!--Transition View Controller-->\n        <scene sceneID=\"ovv-gp-rj6\">\n            <objects>\n                <viewController id=\"Faj-ZD-Pmu\" customClass=\"TransitionViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"yOP-AM-REf\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"dJV-kj-nuj\">\n                                <rect key=\"frame\" x=\"82\" y=\"89\" width=\"250\" height=\"250\"/>\n                                <constraints>\n                                    <constraint firstAttribute=\"width\" secondItem=\"dJV-kj-nuj\" secondAttribute=\"height\" multiplier=\"1:1\" id=\"Agq-Nz-czc\"/>\n                                    <constraint firstAttribute=\"width\" constant=\"250\" id=\"Q7x-LI-VtM\"/>\n                                </constraints>\n                            </imageView>\n                            <pickerView contentMode=\"scaleToFill\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"45M-CC-Cpm\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"359\" width=\"414\" height=\"377\"/>\n                                <color key=\"backgroundColor\" systemColor=\"secondarySystemBackgroundColor\"/>\n                                <connections>\n                                    <outlet property=\"dataSource\" destination=\"Faj-ZD-Pmu\" id=\"iJ6-7y-h0M\"/>\n                                    <outlet property=\"delegate\" destination=\"Faj-ZD-Pmu\" id=\"yZp-17-Dfa\"/>\n                                </connections>\n                            </pickerView>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"ZnV-pz-pbK\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"dJV-kj-nuj\" firstAttribute=\"centerX\" secondItem=\"ZnV-pz-pbK\" secondAttribute=\"centerX\" id=\"MyO-mQ-1dN\"/>\n                            <constraint firstItem=\"45M-CC-Cpm\" firstAttribute=\"top\" secondItem=\"dJV-kj-nuj\" secondAttribute=\"bottom\" constant=\"20\" id=\"REq-j4-Dpu\"/>\n                            <constraint firstItem=\"ZnV-pz-pbK\" firstAttribute=\"bottom\" secondItem=\"45M-CC-Cpm\" secondAttribute=\"bottom\" id=\"VaQ-3D-7Fb\"/>\n                            <constraint firstItem=\"ZnV-pz-pbK\" firstAttribute=\"trailing\" secondItem=\"45M-CC-Cpm\" secondAttribute=\"trailing\" id=\"Wum-4v-zxp\"/>\n                            <constraint firstItem=\"45M-CC-Cpm\" firstAttribute=\"leading\" secondItem=\"ZnV-pz-pbK\" secondAttribute=\"leading\" id=\"av8-Va-R0m\"/>\n                            <constraint firstItem=\"dJV-kj-nuj\" firstAttribute=\"top\" secondItem=\"ZnV-pz-pbK\" secondAttribute=\"top\" constant=\"25\" id=\"cXf-Wn-wQi\"/>\n                        </constraints>\n                    </view>\n                    <connections>\n                        <outlet property=\"imageView\" destination=\"dJV-kj-nuj\" id=\"w6N-9J-JvR\"/>\n                        <outlet property=\"transitionPickerView\" destination=\"45M-CC-Cpm\" id=\"n5x-rt-oxf\"/>\n                    </connections>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"pci-sI-9qT\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"2654\" y=\"-452\"/>\n        </scene>\n        <!--Navigation Controller-->\n        <scene sceneID=\"a89-ss-78Y\">\n            <objects>\n                <navigationController automaticallyAdjustsScrollViewInsets=\"NO\" id=\"peg-r0-mlo\" sceneMemberID=\"viewController\">\n                    <toolbarItems/>\n                    <navigationBar key=\"navigationBar\" contentMode=\"scaleToFill\" id=\"ese-eO-cur\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"20\" width=\"414\" height=\"44\"/>\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                    </navigationBar>\n                    <nil name=\"viewControllers\"/>\n                    <connections>\n                        <segue destination=\"iye-ZY-cV2\" kind=\"relationship\" relationship=\"rootViewController\" id=\"m9R-8J-eeg\"/>\n                    </connections>\n                </navigationController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"Ull-Ta-ybR\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"295\" y=\"647\"/>\n        </scene>\n        <!--Main View Controller-->\n        <scene sceneID=\"LOD-Cd-tGg\">\n            <objects>\n                <tableViewController id=\"iye-ZY-cV2\" customClass=\"MainViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <tableView key=\"view\" clipsSubviews=\"YES\" contentMode=\"scaleToFill\" alwaysBounceVertical=\"YES\" dataMode=\"static\" style=\"plain\" separatorStyle=\"default\" rowHeight=\"44\" estimatedRowHeight=\"-1\" sectionHeaderHeight=\"28\" sectionFooterHeight=\"28\" id=\"hLo-Ms-RdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <sections>\n                            <tableViewSection id=\"lxt-1v-e96\">\n                                <cells>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"JVd-9G-6VS\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"50\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"JVd-9G-6VS\" id=\"w4A-r3-gnV\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Basic\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"cit-yK-cGy\">\n                                                    <rect key=\"frame\" x=\"19.999999999999996\" y=\"11.999999999999998\" width=\"41.666666666666657\" height=\"20.333333333333329\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstItem=\"cit-yK-cGy\" firstAttribute=\"leading\" secondItem=\"w4A-r3-gnV\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"LS9-fh-6MS\"/>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"cit-yK-cGy\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"RTg-Oh-9Yc\"/>\n                                                <constraint firstItem=\"cit-yK-cGy\" firstAttribute=\"centerY\" secondItem=\"w4A-r3-gnV\" secondAttribute=\"centerY\" id=\"ed9-Px-ynn\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"IdS-po-MDe\" kind=\"show\" id=\"Pe4-jS-jlT\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"RzH-cg-SVx\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"94\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"RzH-cg-SVx\" id=\"jzm-Or-x8T\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Auto Sizing\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Kez-je-6N4\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.999999999999998\" width=\"86\" height=\"20.333333333333329\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"Kez-je-6N4\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"IQ0-Sm-DPd\"/>\n                                                <constraint firstItem=\"Kez-je-6N4\" firstAttribute=\"leading\" secondItem=\"jzm-Or-x8T\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"zNG-Jc-gx4\"/>\n                                                <constraint firstItem=\"Kez-je-6N4\" firstAttribute=\"centerY\" secondItem=\"jzm-Or-x8T\" secondAttribute=\"centerY\" id=\"zt8-ht-qsY\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"m5I-z1-VYh\" kind=\"show\" id=\"lmo-9Y-vdj\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"Ae7-le-uVE\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"138\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"Ae7-le-uVE\" id=\"Xnt-bt-suE\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Transition\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"7nS-PF-tLF\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"75\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstItem=\"7nS-PF-tLF\" firstAttribute=\"centerY\" secondItem=\"Xnt-bt-suE\" secondAttribute=\"centerY\" id=\"9pn-CG-U5v\"/>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"7nS-PF-tLF\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"Lat-hF-3Aj\"/>\n                                                <constraint firstItem=\"7nS-PF-tLF\" firstAttribute=\"leading\" secondItem=\"Xnt-bt-suE\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"yLH-sn-wsu\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"Faj-ZD-Pmu\" kind=\"show\" id=\"ySQ-qt-802\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"N6z-X5-Zzj\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"182\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"N6z-X5-Zzj\" id=\"0QK-P3-0Ja\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Infinity\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"fea-jH-1o0\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"51\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"fea-jH-1o0\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"9mw-yX-RIO\"/>\n                                                <constraint firstItem=\"fea-jH-1o0\" firstAttribute=\"centerY\" secondItem=\"0QK-P3-0Ja\" secondAttribute=\"centerY\" id=\"dHg-ri-MNi\"/>\n                                                <constraint firstItem=\"fea-jH-1o0\" firstAttribute=\"leading\" secondItem=\"0QK-P3-0Ja\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"w5P-03-uTe\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"cq4-4P-UWj\" kind=\"show\" id=\"tzK-i9-uC0\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"Zeb-qW-dNH\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"226\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"Zeb-qW-dNH\" id=\"nTg-bT-rHb\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Processor\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Y1Q-HH-CK6\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"77\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstItem=\"Y1Q-HH-CK6\" firstAttribute=\"leading\" secondItem=\"nTg-bT-rHb\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"WjH-aQ-7ME\"/>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"Y1Q-HH-CK6\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"tCC-xt-RcW\"/>\n                                                <constraint firstItem=\"Y1Q-HH-CK6\" firstAttribute=\"centerY\" secondItem=\"nTg-bT-rHb\" secondAttribute=\"centerY\" id=\"uiU-Cx-teZ\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"Wo1-7c-HaW\" kind=\"show\" id=\"faG-K3-cUi\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"n85-lO-lGb\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"270\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"n85-lO-lGb\" id=\"xvU-w7-oU0\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"High Resolution\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"jsO-NM-Fpy\">\n                                                    <rect key=\"frame\" x=\"20.000000000000007\" y=\"11.666666666666664\" width=\"119.66666666666669\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"jsO-NM-Fpy\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"aQK-bh-s1E\"/>\n                                                <constraint firstItem=\"jsO-NM-Fpy\" firstAttribute=\"centerY\" secondItem=\"xvU-w7-oU0\" secondAttribute=\"centerY\" id=\"g54-xa-Lal\"/>\n                                                <constraint firstItem=\"jsO-NM-Fpy\" firstAttribute=\"leading\" secondItem=\"xvU-w7-oU0\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"wIj-jL-eVZ\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"YOM-eh-4JU\" kind=\"show\" id=\"qkj-IE-7dx\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"YLN-f7-TZO\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"314\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"YLN-f7-TZO\" id=\"Ju1-mc-CIe\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"GIF\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"LsG-Aa-hzM\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"26\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"LsG-Aa-hzM\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"7Ms-Yd-OtU\"/>\n                                                <constraint firstItem=\"LsG-Aa-hzM\" firstAttribute=\"leading\" secondItem=\"Ju1-mc-CIe\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"NJj-8X-Ipa\"/>\n                                                <constraint firstItem=\"LsG-Aa-hzM\" firstAttribute=\"centerY\" secondItem=\"Ju1-mc-CIe\" secondAttribute=\"centerY\" id=\"kGk-9D-bGY\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"UXA-j3-Wgu\" kind=\"show\" id=\"7cS-7z-3m8\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"24U-er-jWn\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"358\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"24U-er-jWn\" id=\"6Tk-ch-QY4\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"GIF Heavy\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"6fi-xZ-lfj\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"78\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstItem=\"6fi-xZ-lfj\" firstAttribute=\"leading\" secondItem=\"6Tk-ch-QY4\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"W1l-7A-hsC\"/>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"6fi-xZ-lfj\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"lIC-bI-T9j\"/>\n                                                <constraint firstItem=\"6fi-xZ-lfj\" firstAttribute=\"centerY\" secondItem=\"6Tk-ch-QY4\" secondAttribute=\"centerY\" id=\"wzN-WH-KFB\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"nf1-Ij-kCs\" kind=\"show\" id=\"pIB-zv-l3J\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"xxW-va-fvw\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"402\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"xxW-va-fvw\" id=\"JDa-HW-wqO\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Indicator\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"T0i-Bw-xV2\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"67\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstItem=\"T0i-Bw-xV2\" firstAttribute=\"centerY\" secondItem=\"JDa-HW-wqO\" secondAttribute=\"centerY\" id=\"XkL-Wh-0Bp\"/>\n                                                <constraint firstItem=\"T0i-Bw-xV2\" firstAttribute=\"leading\" secondItem=\"JDa-HW-wqO\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"Y4K-vN-KoD\"/>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"T0i-Bw-xV2\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"px4-ph-9kV\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"8Cn-0c-zQX\" kind=\"show\" id=\"CGY-eE-LKY\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"3Fa-Qk-aCx\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"446\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"3Fa-Qk-aCx\" id=\"mHo-fO-aOl\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Image Data Provider\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"N7J-xJ-fQa\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"154.33333333333334\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"N7J-xJ-fQa\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"CEV-eI-30I\"/>\n                                                <constraint firstItem=\"N7J-xJ-fQa\" firstAttribute=\"leading\" secondItem=\"mHo-fO-aOl\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"DE1-mw-JXf\"/>\n                                                <constraint firstItem=\"N7J-xJ-fQa\" firstAttribute=\"centerY\" secondItem=\"mHo-fO-aOl\" secondAttribute=\"centerY\" id=\"nxt-59-lDJ\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"8m3-Cr-mNn\" kind=\"show\" id=\"ArT-mj-SaD\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"cln-Yy-v33\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"490\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"cln-Yy-v33\" id=\"uVh-9Y-8Dr\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Progressive JPEG\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"OU9-Px-L6V\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"135.33333333333334\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstItem=\"OU9-Px-L6V\" firstAttribute=\"leading\" secondItem=\"uVh-9Y-8Dr\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"Cne-mx-kSy\"/>\n                                                <constraint firstItem=\"OU9-Px-L6V\" firstAttribute=\"centerY\" secondItem=\"uVh-9Y-8Dr\" secondAttribute=\"centerY\" id=\"eIS-yb-Vkf\"/>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"OU9-Px-L6V\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"xL2-ku-p7g\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"P3U-9B-Crn\" kind=\"show\" id=\"KUJ-hl-oad\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"KH8-JQ-Aa3\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"534\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"KH8-JQ-Aa3\" id=\"pOx-M5-JGf\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Text Attachment\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"PXn-os-c5b\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"125\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"PXn-os-c5b\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"C4z-Wv-Pux\"/>\n                                                <constraint firstItem=\"PXn-os-c5b\" firstAttribute=\"leading\" secondItem=\"pOx-M5-JGf\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"TWE-Sx-k4H\"/>\n                                                <constraint firstItem=\"PXn-os-c5b\" firstAttribute=\"centerY\" secondItem=\"pOx-M5-JGf\" secondAttribute=\"centerY\" id=\"ypW-fy-egR\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"WgT-E0-jsn\" kind=\"show\" id=\"Sgu-eB-5JV\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"zRe-B9-kAA\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"578\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"zRe-B9-kAA\" id=\"15j-dH-kGc\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Orientation Images \" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"1eJ-mf-O0K\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"148.66666666666666\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstItem=\"1eJ-mf-O0K\" firstAttribute=\"centerY\" secondItem=\"15j-dH-kGc\" secondAttribute=\"centerY\" id=\"izm-X1-7Dl\"/>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"1eJ-mf-O0K\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"m9r-1O-iBa\"/>\n                                                <constraint firstItem=\"1eJ-mf-O0K\" firstAttribute=\"leading\" secondItem=\"15j-dH-kGc\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"p9Q-Jo-ntZ\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"410-HK-f2e\" kind=\"show\" id=\"fLv-pQ-bD5\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"2c2-O7-4OG\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"622\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"2c2-O7-4OG\" id=\"Mjl-hg-ebT\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"AVAsset Image Generator\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"I8e-FH-QjH\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"194.66666666666666\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"I8e-FH-QjH\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"KjO-7Q-h0u\"/>\n                                                <constraint firstItem=\"I8e-FH-QjH\" firstAttribute=\"leading\" secondItem=\"Mjl-hg-ebT\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"Nib-u8-NUb\"/>\n                                                <constraint firstItem=\"I8e-FH-QjH\" firstAttribute=\"centerY\" secondItem=\"Mjl-hg-ebT\" secondAttribute=\"centerY\" id=\"s3P-9n-qo8\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"m5P-35-yHH\" kind=\"show\" id=\"iY4-PO-rZO\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"5xh-yt-GCq\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"666\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"5xh-yt-GCq\" id=\"RYS-B1-kkb\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Picker Result\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"wm0-0Z-bjC\">\n                                                    <rect key=\"frame\" x=\"20.000000000000007\" y=\"11.666666666666664\" width=\"98.666666666666686\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"wm0-0Z-bjC\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"B3I-gk-g0y\"/>\n                                                <constraint firstItem=\"wm0-0Z-bjC\" firstAttribute=\"leading\" secondItem=\"RYS-B1-kkb\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"drj-Dz-zub\"/>\n                                                <constraint firstItem=\"wm0-0Z-bjC\" firstAttribute=\"centerY\" secondItem=\"RYS-B1-kkb\" secondAttribute=\"centerY\" id=\"wFd-yU-MrL\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"wco-eY-gNu\" kind=\"show\" id=\"KCP-ab-diO\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"8uq-CX-Fxl\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"710\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"8uq-CX-Fxl\" id=\"I9G-72-N8J\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Live Photo\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"mXz-QJ-f80\">\n                                                    <rect key=\"frame\" x=\"20\" y=\"11.666666666666664\" width=\"80\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstItem=\"mXz-QJ-f80\" firstAttribute=\"leading\" secondItem=\"I9G-72-N8J\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"1BM-Eu-bcs\"/>\n                                                <constraint firstItem=\"mXz-QJ-f80\" firstAttribute=\"centerY\" secondItem=\"I9G-72-N8J\" secondAttribute=\"centerY\" id=\"Fqb-ZQ-J3L\"/>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"mXz-QJ-f80\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"fAH-HG-EXN\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"nhf-ZY-JwU\" kind=\"show\" id=\"Srz-py-yg9\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"wqA-9Z-1Al\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"754\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"wqA-9Z-1Al\" id=\"zF8-ez-i13\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Network Metrics\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"zRy-GJ-Gal\">\n                                                    <rect key=\"frame\" x=\"19.999999999999993\" y=\"11.666666666666664\" width=\"125.33333333333331\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"zRy-GJ-Gal\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"Z4X-JI-dgy\"/>\n                                                <constraint firstItem=\"zRy-GJ-Gal\" firstAttribute=\"centerY\" secondItem=\"zF8-ez-i13\" secondAttribute=\"centerY\" id=\"Zaf-1G-5CY\"/>\n                                                <constraint firstItem=\"zRy-GJ-Gal\" firstAttribute=\"leading\" secondItem=\"zF8-ez-i13\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"ncb-FX-kMj\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"Pg3-sc-Em9\" kind=\"show\" id=\"aWn-Nh-tPk\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" id=\"TIF-8x-GLM\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"798\" width=\"414\" height=\"44\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"TIF-8x-GLM\" id=\"ykx-Ds-PkP\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"44\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"SwiftUI\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"8BM-EA-MyK\">\n                                                    <rect key=\"frame\" x=\"19.999999999999996\" y=\"11.666666666666664\" width=\"54.666666666666657\" height=\"21\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"8BM-EA-MyK\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"HIp-se-68O\"/>\n                                                <constraint firstItem=\"8BM-EA-MyK\" firstAttribute=\"centerY\" secondItem=\"ykx-Ds-PkP\" secondAttribute=\"centerY\" id=\"YjV-XQ-ZBh\"/>\n                                                <constraint firstItem=\"8BM-EA-MyK\" firstAttribute=\"leading\" secondItem=\"ykx-Ds-PkP\" secondAttribute=\"leading\" constant=\"20\" symbolic=\"YES\" id=\"wxa-xN-XPx\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <segue destination=\"7fx-Ak-QCf\" kind=\"show\" id=\"1FN-t9-66q\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                </cells>\n                            </tableViewSection>\n                        </sections>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"iye-ZY-cV2\" id=\"ZGM-WC-gRM\"/>\n                            <outlet property=\"delegate\" destination=\"iye-ZY-cV2\" id=\"ClJ-Fd-VZp\"/>\n                        </connections>\n                    </tableView>\n                    <navigationItem key=\"navigationItem\" id=\"va1-4x-RE3\"/>\n                </tableViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"ORY-ac-CQz\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1044\" y=\"645\"/>\n        </scene>\n        <!--Infinity Collection View Controller-->\n        <scene sceneID=\"1br-BE-G87\">\n            <objects>\n                <collectionViewController id=\"cq4-4P-UWj\" customClass=\"InfinityCollectionViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <collectionView key=\"view\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" dataMode=\"prototypes\" id=\"7Sv-Aq-e9P\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <collectionViewFlowLayout key=\"collectionViewLayout\" minimumLineSpacing=\"10\" minimumInteritemSpacing=\"10\" id=\"kws-C5-XtN\">\n                            <size key=\"itemSize\" width=\"250\" height=\"250\"/>\n                            <size key=\"headerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <size key=\"footerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <inset key=\"sectionInset\" minX=\"0.0\" minY=\"10\" maxX=\"0.0\" maxY=\"10\"/>\n                        </collectionViewFlowLayout>\n                        <cells>\n                            <collectionViewCell opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" reuseIdentifier=\"InfinityCell\" id=\"KrJ-Zp-fzY\" customClass=\"ImageCollectionViewCell\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\">\n                                <rect key=\"frame\" x=\"82\" y=\"10\" width=\"250\" height=\"250\"/>\n                                <autoresizingMask key=\"autoresizingMask\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"ebU-Ze-jO5\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                        </imageView>\n                                    </subviews>\n                                </view>\n                                <constraints>\n                                    <constraint firstAttribute=\"trailing\" secondItem=\"ebU-Ze-jO5\" secondAttribute=\"trailing\" id=\"KnM-v3-kPW\"/>\n                                    <constraint firstAttribute=\"bottom\" secondItem=\"ebU-Ze-jO5\" secondAttribute=\"bottom\" id=\"Kx9-Jc-nTH\"/>\n                                    <constraint firstItem=\"ebU-Ze-jO5\" firstAttribute=\"top\" secondItem=\"KrJ-Zp-fzY\" secondAttribute=\"top\" id=\"dkK-Zo-dbQ\"/>\n                                    <constraint firstItem=\"ebU-Ze-jO5\" firstAttribute=\"leading\" secondItem=\"KrJ-Zp-fzY\" secondAttribute=\"leading\" id=\"iKU-4D-S50\"/>\n                                </constraints>\n                                <connections>\n                                    <outlet property=\"cellImageView\" destination=\"ebU-Ze-jO5\" id=\"w1R-Ic-sJo\"/>\n                                </connections>\n                            </collectionViewCell>\n                        </cells>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"cq4-4P-UWj\" id=\"lvC-zS-Qxs\"/>\n                            <outlet property=\"delegate\" destination=\"cq4-4P-UWj\" id=\"VLR-0M-Wdn\"/>\n                        </connections>\n                    </collectionView>\n                    <navigationItem key=\"navigationItem\" id=\"EBb-Hq-SH2\"/>\n                </collectionViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"UWY-iA-aq1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"3442\" y=\"-452\"/>\n        </scene>\n        <!--Text Attachment View Controller-->\n        <scene sceneID=\"4xb-Y1-BL3\">\n            <objects>\n                <viewController id=\"WgT-E0-jsn\" customClass=\"TextAttachmentViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"g6d-ek-fNF\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Label\" textAlignment=\"center\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YVS-ul-f2n\">\n                                <rect key=\"frame\" x=\"82\" y=\"375\" width=\"250\" height=\"50\"/>\n                                <constraints>\n                                    <constraint firstAttribute=\"width\" relation=\"greaterThanOrEqual\" constant=\"250\" id=\"hkg-9V-168\"/>\n                                    <constraint firstAttribute=\"height\" constant=\"50\" id=\"oop-I0-zGm\"/>\n                                </constraints>\n                                <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                <nil key=\"textColor\"/>\n                                <nil key=\"highlightedColor\"/>\n                            </label>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"Y2m-JB-R1d\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"YVS-ul-f2n\" firstAttribute=\"centerX\" secondItem=\"Y2m-JB-R1d\" secondAttribute=\"centerX\" id=\"cEB-ZN-gSo\"/>\n                            <constraint firstItem=\"YVS-ul-f2n\" firstAttribute=\"centerY\" secondItem=\"Y2m-JB-R1d\" secondAttribute=\"centerY\" id=\"rCh-46-XpX\"/>\n                        </constraints>\n                    </view>\n                    <navigationItem key=\"navigationItem\" id=\"7PX-Qv-jQm\"/>\n                    <connections>\n                        <outlet property=\"label\" destination=\"YVS-ul-f2n\" id=\"el4-vX-NCR\"/>\n                    </connections>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"ul1-Nb-mLl\" userLabel=\"First Responder\" customClass=\"UIResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"4407\" y=\"-419\"/>\n        </scene>\n        <!--Processor Collection View Controller-->\n        <scene sceneID=\"h0T-TL-iQv\">\n            <objects>\n                <collectionViewController id=\"Wo1-7c-HaW\" customClass=\"ProcessorCollectionViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <collectionView key=\"view\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" dataMode=\"prototypes\" id=\"04w-FS-fin\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <collectionViewFlowLayout key=\"collectionViewLayout\" minimumLineSpacing=\"10\" minimumInteritemSpacing=\"10\" id=\"ZDR-WG-GcY\">\n                            <size key=\"itemSize\" width=\"250\" height=\"250\"/>\n                            <size key=\"headerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <size key=\"footerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <inset key=\"sectionInset\" minX=\"0.0\" minY=\"10\" maxX=\"0.0\" maxY=\"10\"/>\n                        </collectionViewFlowLayout>\n                        <cells>\n                            <collectionViewCell opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" reuseIdentifier=\"ProcessorCell\" id=\"XQp-yE-aYb\" customClass=\"ImageCollectionViewCell\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\">\n                                <rect key=\"frame\" x=\"82\" y=\"10\" width=\"250\" height=\"250\"/>\n                                <autoresizingMask key=\"autoresizingMask\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"h53-3h-nDR\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                        </imageView>\n                                    </subviews>\n                                </view>\n                                <constraints>\n                                    <constraint firstItem=\"h53-3h-nDR\" firstAttribute=\"leading\" secondItem=\"XQp-yE-aYb\" secondAttribute=\"leading\" id=\"4pa-OL-LEW\"/>\n                                    <constraint firstAttribute=\"trailing\" secondItem=\"h53-3h-nDR\" secondAttribute=\"trailing\" id=\"7aL-Xj-TUb\"/>\n                                    <constraint firstAttribute=\"bottom\" secondItem=\"h53-3h-nDR\" secondAttribute=\"bottom\" id=\"qiI-2j-JRi\"/>\n                                    <constraint firstItem=\"h53-3h-nDR\" firstAttribute=\"top\" secondItem=\"XQp-yE-aYb\" secondAttribute=\"top\" id=\"yB5-ZY-GM8\"/>\n                                </constraints>\n                                <connections>\n                                    <outlet property=\"cellImageView\" destination=\"h53-3h-nDR\" id=\"2Pc-nF-eqL\"/>\n                                </connections>\n                            </collectionViewCell>\n                        </cells>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"Wo1-7c-HaW\" id=\"k1T-i4-8LK\"/>\n                            <outlet property=\"delegate\" destination=\"Wo1-7c-HaW\" id=\"8Li-em-4Nx\"/>\n                        </connections>\n                    </collectionView>\n                    <navigationItem key=\"navigationItem\" id=\"8iS-8I-zoQ\"/>\n                </collectionViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"DAi-LU-A26\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1851\" y=\"316\"/>\n        </scene>\n        <!--High Resolution Collection View Controller-->\n        <scene sceneID=\"EfE-0M-2tp\">\n            <objects>\n                <collectionViewController id=\"YOM-eh-4JU\" customClass=\"HighResolutionCollectionViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <collectionView key=\"view\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" dataMode=\"prototypes\" id=\"NsZ-zd-xkw\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <collectionViewFlowLayout key=\"collectionViewLayout\" minimumLineSpacing=\"10\" minimumInteritemSpacing=\"10\" id=\"rkX-qy-O8t\">\n                            <size key=\"itemSize\" width=\"250\" height=\"250\"/>\n                            <size key=\"headerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <size key=\"footerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <inset key=\"sectionInset\" minX=\"0.0\" minY=\"10\" maxX=\"0.0\" maxY=\"10\"/>\n                        </collectionViewFlowLayout>\n                        <cells>\n                            <collectionViewCell opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" reuseIdentifier=\"HighResolution\" id=\"NaD-FY-St8\" customClass=\"ImageCollectionViewCell\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\">\n                                <rect key=\"frame\" x=\"82\" y=\"10\" width=\"250\" height=\"250\"/>\n                                <autoresizingMask key=\"autoresizingMask\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleAspectFit\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"LR1-7T-gfM\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                        </imageView>\n                                    </subviews>\n                                </view>\n                                <constraints>\n                                    <constraint firstAttribute=\"bottom\" secondItem=\"LR1-7T-gfM\" secondAttribute=\"bottom\" id=\"Z8r-gK-A8V\"/>\n                                    <constraint firstItem=\"LR1-7T-gfM\" firstAttribute=\"top\" secondItem=\"NaD-FY-St8\" secondAttribute=\"top\" id=\"gUe-9T-wHZ\"/>\n                                    <constraint firstItem=\"LR1-7T-gfM\" firstAttribute=\"leading\" secondItem=\"NaD-FY-St8\" secondAttribute=\"leading\" id=\"gWw-cE-B8T\"/>\n                                    <constraint firstAttribute=\"trailing\" secondItem=\"LR1-7T-gfM\" secondAttribute=\"trailing\" id=\"uGw-y1-r7m\"/>\n                                </constraints>\n                                <connections>\n                                    <outlet property=\"cellImageView\" destination=\"LR1-7T-gfM\" id=\"bxW-MU-FLE\"/>\n                                    <segue destination=\"zDE-dW-hUu\" kind=\"show\" identifier=\"showImage\" id=\"c5i-ad-AYE\"/>\n                                </connections>\n                            </collectionViewCell>\n                        </cells>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"YOM-eh-4JU\" id=\"Ubo-Bg-pMu\"/>\n                            <outlet property=\"delegate\" destination=\"YOM-eh-4JU\" id=\"zVh-Rs-hHs\"/>\n                        </connections>\n                    </collectionView>\n                    <navigationItem key=\"navigationItem\" id=\"z6m-x0-MTA\"/>\n                </collectionViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"Edl-Vs-bWD\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"2654\" y=\"315\"/>\n        </scene>\n        <!--View Controller-->\n        <scene sceneID=\"3Ke-rL-QF2\">\n            <objects>\n                <viewController id=\"UXA-j3-Wgu\" customClass=\"GIFViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"BJj-D6-yMO\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"UIImageView\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"UnC-f9-f1r\">\n                                <rect key=\"frame\" x=\"16\" y=\"72\" width=\"99\" height=\"21\"/>\n                                <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                <nil key=\"textColor\"/>\n                                <nil key=\"highlightedColor\"/>\n                            </label>\n                            <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"t8A-JA-4Y6\">\n                                <rect key=\"frame\" x=\"89\" y=\"101\" width=\"236\" height=\"236\"/>\n                                <constraints>\n                                    <constraint firstAttribute=\"width\" constant=\"236\" id=\"Qlt-9V-s5a\"/>\n                                    <constraint firstAttribute=\"height\" constant=\"236\" id=\"cFv-nk-BlZ\"/>\n                                </constraints>\n                            </imageView>\n                            <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"AnimatedImageView\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"QoV-VF-yGe\">\n                                <rect key=\"frame\" x=\"20\" y=\"357\" width=\"156\" height=\"21\"/>\n                                <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                <nil key=\"textColor\"/>\n                                <nil key=\"highlightedColor\"/>\n                            </label>\n                            <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YLY-GE-UZC\" customClass=\"AnimatedImageView\" customModule=\"Kingfisher\">\n                                <rect key=\"frame\" x=\"89\" y=\"386\" width=\"236\" height=\"236\"/>\n                                <constraints>\n                                    <constraint firstAttribute=\"width\" constant=\"236\" id=\"Hqy-Ab-hI1\"/>\n                                    <constraint firstAttribute=\"height\" constant=\"236\" id=\"pjP-Vf-Z7o\"/>\n                                </constraints>\n                            </imageView>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"FHS-wj-J4H\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"t8A-JA-4Y6\" firstAttribute=\"centerX\" secondItem=\"FHS-wj-J4H\" secondAttribute=\"centerX\" id=\"Bdk-FX-Ld7\"/>\n                            <constraint firstItem=\"t8A-JA-4Y6\" firstAttribute=\"top\" secondItem=\"UnC-f9-f1r\" secondAttribute=\"bottom\" constant=\"8\" id=\"EHT-E1-E0N\"/>\n                            <constraint firstItem=\"QoV-VF-yGe\" firstAttribute=\"leading\" secondItem=\"FHS-wj-J4H\" secondAttribute=\"leading\" constant=\"20\" id=\"TZZ-IF-G2M\"/>\n                            <constraint firstItem=\"UnC-f9-f1r\" firstAttribute=\"leading\" secondItem=\"FHS-wj-J4H\" secondAttribute=\"leading\" constant=\"16\" id=\"hCm-QI-DeK\"/>\n                            <constraint firstItem=\"YLY-GE-UZC\" firstAttribute=\"centerX\" secondItem=\"FHS-wj-J4H\" secondAttribute=\"centerX\" id=\"jOD-Cv-2nl\"/>\n                            <constraint firstItem=\"FHS-wj-J4H\" firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"QoV-VF-yGe\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"ow9-TC-KrI\"/>\n                            <constraint firstItem=\"UnC-f9-f1r\" firstAttribute=\"top\" secondItem=\"FHS-wj-J4H\" secondAttribute=\"top\" constant=\"8\" id=\"tXb-Ji-ePH\"/>\n                            <constraint firstItem=\"YLY-GE-UZC\" firstAttribute=\"top\" secondItem=\"QoV-VF-yGe\" secondAttribute=\"bottom\" constant=\"8\" id=\"tjb-jD-W6j\"/>\n                            <constraint firstItem=\"QoV-VF-yGe\" firstAttribute=\"top\" secondItem=\"t8A-JA-4Y6\" secondAttribute=\"bottom\" constant=\"20\" id=\"vDb-2h-8uB\"/>\n                            <constraint firstItem=\"FHS-wj-J4H\" firstAttribute=\"trailing\" relation=\"greaterThanOrEqual\" secondItem=\"UnC-f9-f1r\" secondAttribute=\"trailing\" constant=\"20\" symbolic=\"YES\" id=\"xUe-Ax-iVT\"/>\n                        </constraints>\n                    </view>\n                    <connections>\n                        <outlet property=\"animatedImageView\" destination=\"YLY-GE-UZC\" id=\"Viu-LB-DLp\"/>\n                        <outlet property=\"imageView\" destination=\"t8A-JA-4Y6\" id=\"Ee2-lN-97n\"/>\n                    </connections>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"hAo-Ts-6oW\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"4206\" y=\"646\"/>\n        </scene>\n        <!--Heavy View Controller-->\n        <scene sceneID=\"dgP-4L-Cr8\">\n            <objects>\n                <viewController id=\"nf1-Ij-kCs\" customClass=\"GIFHeavyViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"oSl-Bn-mXh\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <viewLayoutGuide key=\"safeArea\" id=\"Mz6-v4-I52\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                    </view>\n                    <navigationItem key=\"navigationItem\" id=\"vKv-4p-kQh\"/>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"596-19-5UM\" userLabel=\"First Responder\" customClass=\"UIResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"5070\" y=\"679\"/>\n        </scene>\n        <!--Detail Image View Controller-->\n        <scene sceneID=\"wpT-qC-cq3\">\n            <objects>\n                <viewController id=\"zDE-dW-hUu\" customClass=\"DetailImageViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"0Gi-1l-eXz\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <scrollView clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" showsHorizontalScrollIndicator=\"NO\" showsVerticalScrollIndicator=\"NO\" minimumZoomScale=\"0.10000000000000001\" maximumZoomScale=\"20\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"ysc-wr-YIy\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                                <subviews>\n                                    <view contentMode=\"scaleToFill\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"y9Y-32-dYG\" userLabel=\"Content View\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                                        <subviews>\n                                            <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"K6z-gh-7q1\">\n                                                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                                            </imageView>\n                                        </subviews>\n                                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                                        <constraints>\n                                            <constraint firstAttribute=\"bottom\" secondItem=\"K6z-gh-7q1\" secondAttribute=\"bottom\" id=\"Kj4-tR-QNZ\"/>\n                                            <constraint firstItem=\"K6z-gh-7q1\" firstAttribute=\"leading\" secondItem=\"y9Y-32-dYG\" secondAttribute=\"leading\" id=\"Zqb-lf-AQK\"/>\n                                            <constraint firstAttribute=\"trailing\" secondItem=\"K6z-gh-7q1\" secondAttribute=\"trailing\" id=\"hpP-8e-Y3h\"/>\n                                            <constraint firstItem=\"K6z-gh-7q1\" firstAttribute=\"top\" secondItem=\"y9Y-32-dYG\" secondAttribute=\"top\" id=\"idV-w1-DmK\"/>\n                                        </constraints>\n                                    </view>\n                                </subviews>\n                                <constraints>\n                                    <constraint firstItem=\"y9Y-32-dYG\" firstAttribute=\"top\" secondItem=\"ysc-wr-YIy\" secondAttribute=\"top\" id=\"94C-ZE-5D8\"/>\n                                    <constraint firstAttribute=\"bottom\" secondItem=\"y9Y-32-dYG\" secondAttribute=\"bottom\" id=\"9NX-9K-Pu6\"/>\n                                    <constraint firstAttribute=\"trailing\" secondItem=\"y9Y-32-dYG\" secondAttribute=\"trailing\" id=\"Edp-27-Tfx\"/>\n                                    <constraint firstItem=\"y9Y-32-dYG\" firstAttribute=\"height\" secondItem=\"ysc-wr-YIy\" secondAttribute=\"height\" priority=\"250\" id=\"YML-jb-rDf\"/>\n                                    <constraint firstItem=\"y9Y-32-dYG\" firstAttribute=\"width\" secondItem=\"ysc-wr-YIy\" secondAttribute=\"width\" priority=\"250\" id=\"l8X-MW-1jI\"/>\n                                    <constraint firstItem=\"y9Y-32-dYG\" firstAttribute=\"leading\" secondItem=\"ysc-wr-YIy\" secondAttribute=\"leading\" id=\"qVp-ak-3Lf\"/>\n                                </constraints>\n                            </scrollView>\n                            <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"sOP-9B-hsj\">\n                                <rect key=\"frame\" x=\"207\" y=\"716\" width=\"0.0\" height=\"0.0\"/>\n                                <color key=\"backgroundColor\" systemColor=\"secondarySystemBackgroundColor\"/>\n                                <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                <nil key=\"textColor\"/>\n                                <color key=\"highlightedColor\" systemColor=\"secondaryLabelColor\"/>\n                            </label>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"7CV-BV-qfV\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"7CV-BV-qfV\" firstAttribute=\"trailing\" secondItem=\"ysc-wr-YIy\" secondAttribute=\"trailing\" id=\"1C1-WZ-WlV\"/>\n                            <constraint firstAttribute=\"bottom\" secondItem=\"ysc-wr-YIy\" secondAttribute=\"bottom\" id=\"36E-qx-dNi\"/>\n                            <constraint firstItem=\"ysc-wr-YIy\" firstAttribute=\"top\" secondItem=\"0Gi-1l-eXz\" secondAttribute=\"top\" id=\"ErP-w4-6bK\"/>\n                            <constraint firstItem=\"sOP-9B-hsj\" firstAttribute=\"centerX\" secondItem=\"7CV-BV-qfV\" secondAttribute=\"centerX\" id=\"JDr-B0-ZTD\"/>\n                            <constraint firstItem=\"ysc-wr-YIy\" firstAttribute=\"leading\" secondItem=\"7CV-BV-qfV\" secondAttribute=\"leading\" id=\"RFB-dT-sJs\"/>\n                            <constraint firstItem=\"7CV-BV-qfV\" firstAttribute=\"bottom\" secondItem=\"sOP-9B-hsj\" secondAttribute=\"bottom\" constant=\"20\" id=\"gZK-6r-GPm\"/>\n                        </constraints>\n                    </view>\n                    <connections>\n                        <outlet property=\"imageView\" destination=\"K6z-gh-7q1\" id=\"NW3-eU-ddR\"/>\n                        <outlet property=\"infoLabel\" destination=\"sOP-9B-hsj\" id=\"Rqt-Pg-eUg\"/>\n                        <outlet property=\"scrollView\" destination=\"ysc-wr-YIy\" id=\"pkk-ec-Xcx\"/>\n                    </connections>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"Jcl-EP-bcn\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"3419\" y=\"315\"/>\n        </scene>\n        <!--Indicator Collection View Controller-->\n        <scene sceneID=\"WgK-pr-9tS\">\n            <objects>\n                <collectionViewController id=\"8Cn-0c-zQX\" customClass=\"IndicatorCollectionViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <collectionView key=\"view\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" dataMode=\"prototypes\" id=\"KHw-Ia-NZ0\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <collectionViewFlowLayout key=\"collectionViewLayout\" minimumLineSpacing=\"10\" minimumInteritemSpacing=\"10\" id=\"QR6-EV-Mcm\">\n                            <size key=\"itemSize\" width=\"250\" height=\"250\"/>\n                            <size key=\"headerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <size key=\"footerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <inset key=\"sectionInset\" minX=\"0.0\" minY=\"10\" maxX=\"0.0\" maxY=\"10\"/>\n                        </collectionViewFlowLayout>\n                        <cells>\n                            <collectionViewCell opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" reuseIdentifier=\"IndicatorCell\" id=\"oic-g0-0T7\" customClass=\"ImageCollectionViewCell\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\">\n                                <rect key=\"frame\" x=\"82\" y=\"10\" width=\"250\" height=\"250\"/>\n                                <autoresizingMask key=\"autoresizingMask\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"AHa-W9-ydh\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                        </imageView>\n                                    </subviews>\n                                </view>\n                                <constraints>\n                                    <constraint firstAttribute=\"trailing\" secondItem=\"AHa-W9-ydh\" secondAttribute=\"trailing\" id=\"Aki-5h-QfA\"/>\n                                    <constraint firstItem=\"AHa-W9-ydh\" firstAttribute=\"top\" secondItem=\"oic-g0-0T7\" secondAttribute=\"top\" id=\"DrB-e3-ans\"/>\n                                    <constraint firstAttribute=\"bottom\" secondItem=\"AHa-W9-ydh\" secondAttribute=\"bottom\" id=\"Uxz-bA-hUp\"/>\n                                    <constraint firstItem=\"AHa-W9-ydh\" firstAttribute=\"leading\" secondItem=\"oic-g0-0T7\" secondAttribute=\"leading\" id=\"fdr-x5-S6s\"/>\n                                </constraints>\n                                <connections>\n                                    <outlet property=\"cellImageView\" destination=\"AHa-W9-ydh\" id=\"bnE-ur-ydz\"/>\n                                </connections>\n                            </collectionViewCell>\n                        </cells>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"8Cn-0c-zQX\" id=\"tbc-LR-q5V\"/>\n                            <outlet property=\"delegate\" destination=\"8Cn-0c-zQX\" id=\"jnN-LL-oxb\"/>\n                        </connections>\n                    </collectionView>\n                    <navigationItem key=\"navigationItem\" id=\"mbM-AR-OT6\"/>\n                </collectionViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"sV2-nB-Yrc\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1851\" y=\"1062\"/>\n        </scene>\n        <!--ProgressiveJPEG View Controller-->\n        <scene sceneID=\"aU5-xT-lmw\">\n            <objects>\n                <viewController id=\"P3U-9B-Crn\" customClass=\"ProgressiveJPEGViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"cGe-Lq-jhl\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"scaleAspectFit\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"akD-zs-eTn\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"74\" width=\"414\" height=\"414\"/>\n                                <constraints>\n                                    <constraint firstAttribute=\"width\" secondItem=\"akD-zs-eTn\" secondAttribute=\"height\" multiplier=\"1:1\" id=\"bvS-PE-dXe\"/>\n                                </constraints>\n                            </imageView>\n                            <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"\" textAlignment=\"center\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"izF-cD-0cm\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"518\" width=\"414\" height=\"0.0\"/>\n                                <fontDescription key=\"fontDescription\" name=\"DINAlternate-Bold\" family=\"DIN Alternate\" pointSize=\"17\"/>\n                                <nil key=\"textColor\"/>\n                                <nil key=\"highlightedColor\"/>\n                            </label>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"1aF-Wq-oWH\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"akD-zs-eTn\" firstAttribute=\"centerX\" secondItem=\"1aF-Wq-oWH\" secondAttribute=\"centerX\" id=\"5kh-xV-bka\"/>\n                            <constraint firstItem=\"izF-cD-0cm\" firstAttribute=\"width\" secondItem=\"cGe-Lq-jhl\" secondAttribute=\"width\" id=\"Aut-Lw-Huc\"/>\n                            <constraint firstItem=\"akD-zs-eTn\" firstAttribute=\"top\" secondItem=\"1aF-Wq-oWH\" secondAttribute=\"top\" constant=\"10\" id=\"HtJ-QY-RlR\"/>\n                            <constraint firstItem=\"1aF-Wq-oWH\" firstAttribute=\"trailing\" secondItem=\"akD-zs-eTn\" secondAttribute=\"trailing\" id=\"IbZ-qg-qTg\"/>\n                            <constraint firstItem=\"akD-zs-eTn\" firstAttribute=\"leading\" secondItem=\"1aF-Wq-oWH\" secondAttribute=\"leading\" id=\"d4F-SH-Rkc\"/>\n                            <constraint firstItem=\"izF-cD-0cm\" firstAttribute=\"centerX\" secondItem=\"1aF-Wq-oWH\" secondAttribute=\"centerX\" id=\"i2T-YC-hzq\"/>\n                            <constraint firstItem=\"izF-cD-0cm\" firstAttribute=\"top\" secondItem=\"akD-zs-eTn\" secondAttribute=\"bottom\" constant=\"30\" id=\"oD0-d7-geg\"/>\n                        </constraints>\n                    </view>\n                    <connections>\n                        <outlet property=\"imageView\" destination=\"akD-zs-eTn\" id=\"w4n-Wq-2gb\"/>\n                        <outlet property=\"progressLabel\" destination=\"izF-cD-0cm\" id=\"Pg5-6G-GQa\"/>\n                    </connections>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"TkP-Lx-DcG\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"3441\" y=\"1074\"/>\n        </scene>\n        <!--Image Data Provider Collection View Controller-->\n        <scene sceneID=\"LEm-4i-lfK\">\n            <objects>\n                <collectionViewController id=\"8m3-Cr-mNn\" customClass=\"ImageDataProviderCollectionViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <collectionView key=\"view\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" dataMode=\"prototypes\" id=\"hcT-OT-yiP\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <collectionViewFlowLayout key=\"collectionViewLayout\" minimumLineSpacing=\"10\" minimumInteritemSpacing=\"10\" id=\"1Lb-Fd-Ckf\">\n                            <size key=\"itemSize\" width=\"250\" height=\"250\"/>\n                            <size key=\"headerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <size key=\"footerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <inset key=\"sectionInset\" minX=\"0.0\" minY=\"10\" maxX=\"0.0\" maxY=\"10\"/>\n                        </collectionViewFlowLayout>\n                        <cells>\n                            <collectionViewCell opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" reuseIdentifier=\"ImageDataProviderCell\" id=\"qox-Vm-2MG\" customClass=\"ImageCollectionViewCell\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\">\n                                <rect key=\"frame\" x=\"82\" y=\"10\" width=\"250\" height=\"250\"/>\n                                <autoresizingMask key=\"autoresizingMask\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Gtp-fM-cpb\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                        </imageView>\n                                    </subviews>\n                                </view>\n                                <constraints>\n                                    <constraint firstAttribute=\"trailing\" secondItem=\"Gtp-fM-cpb\" secondAttribute=\"trailing\" id=\"6aF-90-gxG\"/>\n                                    <constraint firstItem=\"Gtp-fM-cpb\" firstAttribute=\"leading\" secondItem=\"qox-Vm-2MG\" secondAttribute=\"leading\" id=\"Kqs-p5-fJC\"/>\n                                    <constraint firstAttribute=\"bottom\" secondItem=\"Gtp-fM-cpb\" secondAttribute=\"bottom\" id=\"QfU-yK-BFd\"/>\n                                    <constraint firstItem=\"Gtp-fM-cpb\" firstAttribute=\"top\" secondItem=\"qox-Vm-2MG\" secondAttribute=\"top\" id=\"eDf-je-LUn\"/>\n                                </constraints>\n                                <connections>\n                                    <outlet property=\"cellImageView\" destination=\"Gtp-fM-cpb\" id=\"JzE-nY-wbN\"/>\n                                </connections>\n                            </collectionViewCell>\n                        </cells>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"8m3-Cr-mNn\" id=\"bEy-UU-XQX\"/>\n                            <outlet property=\"delegate\" destination=\"8m3-Cr-mNn\" id=\"Rex-yK-zSr\"/>\n                        </connections>\n                    </collectionView>\n                    <navigationItem key=\"navigationItem\" id=\"Tyg-JC-TAo\"/>\n                </collectionViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"a3K-Ir-TOf\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"2654\" y=\"1061\"/>\n        </scene>\n        <!--Live Photo View Controller-->\n        <scene sceneID=\"50O-N8-WQ3\">\n            <objects>\n                <viewController id=\"nhf-ZY-JwU\" customClass=\"LivePhotoViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"G81-wm-Fnw\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <viewLayoutGuide key=\"safeArea\" id=\"12L-DC-epo\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                    </view>\n                    <navigationItem key=\"navigationItem\" id=\"bYb-CA-CnJ\"/>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"y5S-MW-IuF\" userLabel=\"First Responder\" customClass=\"UIResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1851\" y=\"2398\"/>\n        </scene>\n        <!--Asset Image Generator View Controller-->\n        <scene sceneID=\"PCw-9j-oCu\">\n            <objects>\n                <viewController id=\"m5P-35-yHH\" customClass=\"AVAssetImageGeneratorViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Hqg-bo-3DO\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"scaleAspectFit\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"lTP-vt-DdF\">\n                                <rect key=\"frame\" x=\"87\" y=\"114\" width=\"240\" height=\"240\"/>\n                                <constraints>\n                                    <constraint firstAttribute=\"height\" constant=\"240\" id=\"PRd-E7-RiS\"/>\n                                    <constraint firstAttribute=\"width\" constant=\"240\" id=\"sQj-Or-zGO\"/>\n                                </constraints>\n                            </imageView>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"kpf-7x-2lo\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"lTP-vt-DdF\" firstAttribute=\"centerX\" secondItem=\"kpf-7x-2lo\" secondAttribute=\"centerX\" id=\"2MT-0R-oyk\"/>\n                            <constraint firstItem=\"lTP-vt-DdF\" firstAttribute=\"top\" secondItem=\"kpf-7x-2lo\" secondAttribute=\"top\" constant=\"50\" id=\"mff-5v-1Z1\"/>\n                        </constraints>\n                    </view>\n                    <navigationItem key=\"navigationItem\" id=\"7AD-kL-ZIP\"/>\n                    <connections>\n                        <outlet property=\"imageView\" destination=\"lTP-vt-DdF\" id=\"fk4-PV-mkj\"/>\n                    </connections>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"Idc-Kb-7jc\" userLabel=\"First Responder\" customClass=\"UIResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1850.7246376811595\" y=\"1735.5978260869567\"/>\n        </scene>\n        <!--Network Metrics View Controller-->\n        <scene sceneID=\"zOx-Gp-KL6\">\n            <objects>\n                <viewController id=\"Pg3-sc-Em9\" customClass=\"NetworkMetricsViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"PY3-v0-EIu\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <viewLayoutGuide key=\"safeArea\" id=\"5i9-7s-TjX\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                    </view>\n                    <navigationItem key=\"navigationItem\" id=\"aD3-6d-iIp\"/>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"K6C-pd-2au\" userLabel=\"First Responder\" customClass=\"UIResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"2654\" y=\"-1114\"/>\n        </scene>\n        <!--SwiftUI View Controller-->\n        <scene sceneID=\"rw7-vi-6Ep\">\n            <objects>\n                <hostingController id=\"7fx-Ak-QCf\" customClass=\"SwiftUIViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <navigationItem key=\"navigationItem\" id=\"TII-JX-FOz\"/>\n                </hostingController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"ep4-jy-p4V\" userLabel=\"First Responder\" customClass=\"UIResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1043\" y=\"1452\"/>\n        </scene>\n        <!--Auto Sizing Table View Controller-->\n        <scene sceneID=\"FnD-cb-mfl\">\n            <objects>\n                <viewController id=\"m5I-z1-VYh\" customClass=\"AutoSizingTableViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"XGI-dp-ihR\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <tableView clipsSubviews=\"YES\" contentMode=\"scaleToFill\" alwaysBounceVertical=\"YES\" dataMode=\"prototypes\" style=\"plain\" separatorStyle=\"default\" rowHeight=\"-1\" estimatedRowHeight=\"-1\" sectionHeaderHeight=\"28\" sectionFooterHeight=\"28\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"LCB-9d-5gc\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"64\" width=\"414\" height=\"672\"/>\n                                <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                                <prototypes>\n                                    <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" reuseIdentifier=\"AutoSizingTableViewCell\" id=\"pk1-lS-QVX\" customClass=\"AutoSizingTableViewCell\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\">\n                                        <rect key=\"frame\" x=\"0.0\" y=\"50\" width=\"414\" height=\"30.333333969116211\"/>\n                                        <autoresizingMask key=\"autoresizingMask\"/>\n                                        <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"pk1-lS-QVX\" id=\"bKc-TZ-DSQ\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"30.333333969116211\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <subviews>\n                                                <imageView clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"scaleAspectFit\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Rpq-bG-bmb\">\n                                                    <rect key=\"frame\" x=\"16\" y=\"15\" width=\"374\" height=\"0.33333333333333393\"/>\n                                                </imageView>\n                                                <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YvM-pn-sx4\">\n                                                    <rect key=\"frame\" x=\"398\" y=\"15.333333333333334\" width=\"0.0\" height=\"0.0\"/>\n                                                    <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                                    <nil key=\"textColor\"/>\n                                                    <nil key=\"highlightedColor\"/>\n                                                </label>\n                                            </subviews>\n                                            <constraints>\n                                                <constraint firstAttribute=\"bottom\" secondItem=\"Rpq-bG-bmb\" secondAttribute=\"bottom\" constant=\"15\" id=\"F3I-dF-lHL\"/>\n                                                <constraint firstAttribute=\"trailing\" secondItem=\"YvM-pn-sx4\" secondAttribute=\"trailing\" constant=\"16\" id=\"IzX-iB-fm0\"/>\n                                                <constraint firstItem=\"Rpq-bG-bmb\" firstAttribute=\"top\" secondItem=\"bKc-TZ-DSQ\" secondAttribute=\"top\" constant=\"15\" id=\"bb5-Dp-AYe\"/>\n                                                <constraint firstItem=\"YvM-pn-sx4\" firstAttribute=\"leading\" secondItem=\"Rpq-bG-bmb\" secondAttribute=\"trailing\" constant=\"8\" id=\"bwG-CX-wW5\"/>\n                                                <constraint firstItem=\"YvM-pn-sx4\" firstAttribute=\"centerY\" secondItem=\"bKc-TZ-DSQ\" secondAttribute=\"centerY\" id=\"cdg-sQ-7wV\"/>\n                                                <constraint firstItem=\"Rpq-bG-bmb\" firstAttribute=\"leading\" secondItem=\"bKc-TZ-DSQ\" secondAttribute=\"leading\" constant=\"16\" id=\"oUL-9L-IVr\"/>\n                                                <constraint firstItem=\"Rpq-bG-bmb\" firstAttribute=\"centerY\" secondItem=\"bKc-TZ-DSQ\" secondAttribute=\"centerY\" id=\"rV6-RS-Gxv\"/>\n                                            </constraints>\n                                        </tableViewCellContentView>\n                                        <connections>\n                                            <outlet property=\"leadingImageView\" destination=\"Rpq-bG-bmb\" id=\"FTX-Be-6tF\"/>\n                                            <outlet property=\"sizeLabel\" destination=\"YvM-pn-sx4\" id=\"5mC-N9-z6a\"/>\n                                        </connections>\n                                    </tableViewCell>\n                                </prototypes>\n                                <connections>\n                                    <outlet property=\"dataSource\" destination=\"m5I-z1-VYh\" id=\"pXN-uz-AMU\"/>\n                                </connections>\n                            </tableView>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"OuK-nI-agU\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstAttribute=\"bottom\" secondItem=\"LCB-9d-5gc\" secondAttribute=\"bottom\" id=\"DCg-yZ-9JS\"/>\n                            <constraint firstItem=\"OuK-nI-agU\" firstAttribute=\"trailing\" secondItem=\"LCB-9d-5gc\" secondAttribute=\"trailing\" id=\"Dnp-fD-Vi4\"/>\n                            <constraint firstItem=\"LCB-9d-5gc\" firstAttribute=\"top\" secondItem=\"OuK-nI-agU\" secondAttribute=\"top\" id=\"HdH-96-NMP\"/>\n                            <constraint firstItem=\"LCB-9d-5gc\" firstAttribute=\"leading\" secondItem=\"OuK-nI-agU\" secondAttribute=\"leading\" id=\"XfE-y9-kzU\"/>\n                        </constraints>\n                    </view>\n                    <navigationItem key=\"navigationItem\" id=\"BHi-j8-a1W\"/>\n                    <connections>\n                        <outlet property=\"tableView\" destination=\"LCB-9d-5gc\" id=\"8cI-EW-YnN\"/>\n                    </connections>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"LBt-0J-SXS\" userLabel=\"First Responder\" customClass=\"UIResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"2653.6231884057975\" y=\"1735.5978260869567\"/>\n        </scene>\n        <!--Orientation Images View Controller-->\n        <scene sceneID=\"Fv8-ps-w1h\">\n            <objects>\n                <collectionViewController id=\"410-HK-f2e\" customClass=\"OrientationImagesViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <collectionView key=\"view\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" dataMode=\"prototypes\" id=\"15C-rf-1z2\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <collectionViewFlowLayout key=\"collectionViewLayout\" minimumLineSpacing=\"10\" minimumInteritemSpacing=\"10\" id=\"ue3-LS-54W\">\n                            <size key=\"itemSize\" width=\"250\" height=\"250\"/>\n                            <size key=\"headerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <size key=\"footerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <inset key=\"sectionInset\" minX=\"0.0\" minY=\"10\" maxX=\"0.0\" maxY=\"10\"/>\n                        </collectionViewFlowLayout>\n                        <cells>\n                            <collectionViewCell opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" reuseIdentifier=\"collectionViewCell\" id=\"0St-Ht-XVs\" customClass=\"ImageCollectionViewCell\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\">\n                                <rect key=\"frame\" x=\"82\" y=\"10\" width=\"250\" height=\"250\"/>\n                                <autoresizingMask key=\"autoresizingMask\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"D11-CU-1Qw\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                                        </imageView>\n                                    </subviews>\n                                </view>\n                                <constraints>\n                                    <constraint firstAttribute=\"trailing\" secondItem=\"D11-CU-1Qw\" secondAttribute=\"trailing\" id=\"3ec-K2-bVY\"/>\n                                    <constraint firstItem=\"D11-CU-1Qw\" firstAttribute=\"top\" secondItem=\"0St-Ht-XVs\" secondAttribute=\"top\" id=\"Cwm-yi-X3c\"/>\n                                    <constraint firstAttribute=\"bottom\" secondItem=\"D11-CU-1Qw\" secondAttribute=\"bottom\" id=\"jnN-ub-UHz\"/>\n                                    <constraint firstItem=\"D11-CU-1Qw\" firstAttribute=\"leading\" secondItem=\"0St-Ht-XVs\" secondAttribute=\"leading\" id=\"u6e-qy-6LO\"/>\n                                </constraints>\n                                <connections>\n                                    <outlet property=\"cellImageView\" destination=\"D11-CU-1Qw\" id=\"lAR-SF-ZOb\"/>\n                                </connections>\n                            </collectionViewCell>\n                        </cells>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"410-HK-f2e\" id=\"T2p-q6-Xvz\"/>\n                            <outlet property=\"delegate\" destination=\"410-HK-f2e\" id=\"0pk-FT-4kh\"/>\n                        </connections>\n                    </collectionView>\n                    <navigationItem key=\"navigationItem\" id=\"g1e-iY-co5\"/>\n                </collectionViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"HkY-OQ-ush\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1851\" y=\"-1114\"/>\n        </scene>\n        <!--Picker Result View Controller-->\n        <scene sceneID=\"JMu-F0-c9u\">\n            <objects>\n                <viewController id=\"wco-eY-gNu\" customClass=\"PHPickerResultViewController\" customModule=\"Kingfisher_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"22H-rk-RMJ\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"736\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"scaleAspectFit\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"1LX-Oj-TiN\">\n                                <rect key=\"frame\" x=\"87\" y=\"114\" width=\"240\" height=\"240\"/>\n                                <constraints>\n                                    <constraint firstAttribute=\"height\" constant=\"240\" id=\"bGC-o1-bQV\"/>\n                                    <constraint firstAttribute=\"width\" constant=\"240\" id=\"bya-2p-YYh\"/>\n                                </constraints>\n                            </imageView>\n                            <button opaque=\"NO\" contentMode=\"scaleToFill\" contentHorizontalAlignment=\"center\" contentVerticalAlignment=\"center\" buttonType=\"system\" lineBreakMode=\"middleTruncation\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"ySI-TE-gHl\">\n                                <rect key=\"frame\" x=\"167.33333333333334\" y=\"404\" width=\"79.666666666666657\" height=\"34.333333333333314\"/>\n                                <inset key=\"imageEdgeInsets\" minX=\"0.0\" minY=\"0.0\" maxX=\"2.2250738585072014e-308\" maxY=\"0.0\"/>\n                                <state key=\"normal\" title=\"Tap Me\"/>\n                                <buttonConfiguration key=\"configuration\" style=\"plain\" title=\"Tap Me\"/>\n                                <connections>\n                                    <action selector=\"onTapButton\" destination=\"wco-eY-gNu\" eventType=\"touchUpInside\" id=\"QyB-8Y-1Oc\"/>\n                                </connections>\n                            </button>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"BKk-eJ-kIo\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"ySI-TE-gHl\" firstAttribute=\"centerX\" secondItem=\"1LX-Oj-TiN\" secondAttribute=\"centerX\" id=\"Sm9-p7-uMP\"/>\n                            <constraint firstItem=\"ySI-TE-gHl\" firstAttribute=\"top\" secondItem=\"1LX-Oj-TiN\" secondAttribute=\"bottom\" constant=\"50\" id=\"gbM-rb-Ok8\"/>\n                            <constraint firstItem=\"1LX-Oj-TiN\" firstAttribute=\"centerX\" secondItem=\"BKk-eJ-kIo\" secondAttribute=\"centerX\" id=\"qyZ-NL-vmv\"/>\n                            <constraint firstItem=\"1LX-Oj-TiN\" firstAttribute=\"top\" secondItem=\"BKk-eJ-kIo\" secondAttribute=\"top\" constant=\"50\" id=\"x3D-dH-ynU\"/>\n                        </constraints>\n                    </view>\n                    <navigationItem key=\"navigationItem\" id=\"r2f-l4-Rbs\"/>\n                    <connections>\n                        <outlet property=\"imageView\" destination=\"1LX-Oj-TiN\" id=\"ni0-hA-iSX\"/>\n                    </connections>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"hjI-mE-I0S\" userLabel=\"First Responder\" customClass=\"UIResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"1850.7246376811595\" y=\"1735.5978260869567\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <systemColor name=\"secondaryLabelColor\">\n            <color red=\"0.23529411764705882\" green=\"0.23529411764705882\" blue=\"0.2627450980392157\" alpha=\"0.59999999999999998\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n        </systemColor>\n        <systemColor name=\"secondarySystemBackgroundColor\">\n            <color red=\"0.94901960784313721\" green=\"0.94901960784313721\" blue=\"0.96862745098039216\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n        </systemColor>\n        <systemColor name=\"systemBackgroundColor\">\n            <color white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"genericGamma22GrayColorSpace\"/>\n        </systemColor>\n    </resources>\n</document>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/Extensions/UIViewController+KingfisherOperation.swift",
    "content": "//\n//  UIViewController+KingfisherOperation.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/18.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nprotocol MainDataViewReloadable {\n    @MainActor func reload()\n}\n\nextension UITableViewController: MainDataViewReloadable {\n    func reload() {\n        tableView.reloadData()\n    }\n}\n\nextension UICollectionViewController: MainDataViewReloadable {\n    func reload() {\n        collectionView.reloadData()\n    }\n}\n\nprotocol KingfisherActionAlertPopup {\n    @MainActor\n    func alertPopup(_ sender: Any) -> UIAlertController\n}\n\n@MainActor func cleanCacheAction() -> UIAlertAction {\n    return UIAlertAction(title: \"Clean Cache\", style: .default) { _ in\n        KingfisherManager.shared.cache.clearMemoryCache()\n        KingfisherManager.shared.cache.clearDiskCache()\n    }\n}\n\n@MainActor func reloadAction(_ reloadable: any MainDataViewReloadable) -> UIAlertAction {\n    return UIAlertAction(title: \"Reload\", style: .default) { _ in\n        reloadable.reload()\n    }\n}\n\n@MainActor let cancelAction = UIAlertAction(title: \"Cancel\", style: .cancel)\n\n@MainActor func createAlert(_ sender: Any, actions: [UIAlertAction]) -> UIAlertController {\n    let alert = UIAlertController(title: \"Action\", message: nil, preferredStyle: .actionSheet)\n    alert.popoverPresentationController?.barButtonItem = sender as? UIBarButtonItem\n    alert.popoverPresentationController?.permittedArrowDirections = .any\n    \n    actions.forEach { alert.addAction($0) }\n    \n    return alert\n}\n\nextension UIViewController: KingfisherActionAlertPopup {\n    @objc func alertPopup(_ sender: Any) -> UIAlertController {\n        let alert = createAlert(sender, actions: [cleanCacheAction(), cancelAction])\n        if let r = self as? any MainDataViewReloadable {\n            alert.addAction(reloadAction(r))\n        }\n        return alert\n    }\n}\n\nextension UIViewController  {\n    func setupOperationNavigationBar() {\n        navigationItem.rightBarButtonItem =\n            UIBarButtonItem(title: \"Action\", style: .plain, target: self, action: #selector(performKingfisherAction))\n    }\n    \n    @objc func performKingfisherAction(_ sender: Any) {\n        present(alertPopup(sender), animated: true)\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/Images.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"20x20\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"20x20\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"29x29\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"29x29\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"40x40\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"40x40\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"60x60\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"60x60\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"20x20\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"20x20\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"29x29\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"29x29\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"40x40\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"40x40\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"76x76\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"76x76\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"83.5x83.5\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ios-marketing\",\n      \"size\" : \"1024x1024\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/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>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>4.6.2</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>1244</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>armv7</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"17147\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" useTraitCollections=\"YES\" useSafeAreas=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <device id=\"retina6_1\" orientation=\"portrait\" appearance=\"dark\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"17120\"/>\n        <capability name=\"Safe area layout guides\" minToolsVersion=\"9.0\"/>\n        <capability name=\"System colors in document resources\" minToolsVersion=\"11.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"896\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <label opaque=\"NO\" clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" textAlignment=\"right\" lineBreakMode=\"tailTruncation\" numberOfLines=\"2\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"obG-Y5-kRd\">\n                                <rect key=\"frame\" x=\"30\" y=\"824.5\" width=\"354\" height=\"31.5\"/>\n                                <string key=\"text\">  Copyright (c) 2020 Wei Wang (onevcat)\nAll rights reserved.</string>\n                                <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"13\"/>\n                                <color key=\"textColor\" systemColor=\"secondaryLabelColor\"/>\n                                <nil key=\"highlightedColor\"/>\n                            </label>\n                            <label opaque=\"NO\" clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" textAlignment=\"center\" lineBreakMode=\"middleTruncation\" numberOfLines=\"0\" baselineAdjustment=\"alignBaselines\" minimumFontSize=\"18\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"GJd-Yh-RWb\">\n                                <rect key=\"frame\" x=\"123.5\" y=\"253\" width=\"167.5\" height=\"91\"/>\n                                <string key=\"text\">Kingfisher \nSample</string>\n                                <fontDescription key=\"fontDescription\" type=\"system\" weight=\"light\" pointSize=\"38\"/>\n                                <nil key=\"highlightedColor\"/>\n                            </label>\n                        </subviews>\n                        <viewLayoutGuide key=\"safeArea\" id=\"Bcu-3y-fUS\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"Bcu-3y-fUS\" firstAttribute=\"centerX\" secondItem=\"obG-Y5-kRd\" secondAttribute=\"centerX\" id=\"5cz-MP-9tL\"/>\n                            <constraint firstItem=\"Bcu-3y-fUS\" firstAttribute=\"centerX\" secondItem=\"GJd-Yh-RWb\" secondAttribute=\"centerX\" id=\"Q3B-4B-g5h\"/>\n                            <constraint firstItem=\"obG-Y5-kRd\" firstAttribute=\"leading\" secondItem=\"Bcu-3y-fUS\" secondAttribute=\"leading\" constant=\"30\" id=\"SfN-ll-jLj\"/>\n                            <constraint firstAttribute=\"bottom\" secondItem=\"obG-Y5-kRd\" secondAttribute=\"bottom\" constant=\"40\" id=\"Y44-ml-fuU\"/>\n                            <constraint firstItem=\"GJd-Yh-RWb\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"bottom\" multiplier=\"1/3\" id=\"moa-c2-u7t\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"52.173913043478265\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <systemColor name=\"secondaryLabelColor\">\n            <color red=\"0.23529411764705882\" green=\"0.23529411764705882\" blue=\"0.2627450980392157\" alpha=\"0.59999999999999998\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n        </systemColor>\n        <systemColor name=\"systemBackgroundColor\">\n            <color white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"genericGamma22GrayColorSpace\"/>\n        </systemColor>\n    </resources>\n</document>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/Resources/ImageLoader.swift",
    "content": "//\n//  ImageLoader.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/18.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\nstruct ImageLoader {\n    static let sampleImageURLs: [URL] = {\n        let prefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading\"\n        return (1...10).map { URL(string: \"\\(prefix)/kingfisher-\\($0).jpg\")! }\n    }()\n    \n    static let orientationImageURLs: [URL] = {\n        let prefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Orientation\"\n        return (1...16).map { URL(string: \"\\(prefix)/\\($0).jpg\")! }\n    }()\n\n    static let highResolutionImageURLs: [URL] = {\n        let prefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/HighResolution\"\n        return (1...20).map { URL(string: \"\\(prefix)/\\($0).jpg\")! }\n    }()\n    \n    static let gifImageURLs: [URL] = {\n        let prefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF\"\n        return (1...3).map { URL(string: \"\\(prefix)/\\($0).gif\")! }\n    }()\n\n    static let progressiveImageURL: URL = {\n        let prefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Progressive\"\n        return URL(string: \"\\(prefix)/progressive.jpg\")!\n    }()\n    \n    static func roseImage(index: Int) -> URL {\n        return URL(string: \"https://github.com/onevcat/Flower-Data-Set/raw/master/rose/rose-\\(index).jpg\")!\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/AnimatedImageDemo.swift",
    "content": "//\n//  AnimatedImageDemo.swift\n//  Kingfisher\n//\n//  Created by wangxingbin on 2021/4/27.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct AnimatedImageDemo: View {\n    \n    @State private var index = 1\n        \n    var url: URL {\n        ImageLoader.gifImageURLs[index - 1]\n    }\n    \n    var body: some View {\n        VStack {\n            KFAnimatedImage(url)\n                .configure { view in\n                    view.framePreloadCount = 3\n                }\n                .cacheOriginalImage()\n                .onSuccess { r in\n                    print(\"suc: \\(r)\")\n                }\n                .onFailure { e in\n                    print(\"err: \\(e)\")\n                }\n                .placeholder { p in\n                    ProgressView(p)\n                }\n                .fade(duration: 1)\n                .forceTransition()\n                .aspectRatio(contentMode: .fill)\n                .frame(width: 300, height: 300)\n                .cornerRadius(20)\n                .shadow(radius: 5)\n                .frame(width: 320, height: 320)\n\n            Button(action: {\n                self.index = (self.index % 3) + 1\n            }) { Text(\"Next Image\") }\n        }.navigationBarTitle(Text(\"Basic Image\"), displayMode: .inline)\n    }\n    \n}\n\n@available(iOS 14.0, *)\nstruct AnimatedImageDemo_Previews: PreviewProvider {\n    \n    static var previews: some View {\n        AnimatedImageDemo()\n    }\n    \n}\n\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/GeometryReaderDemo.swift",
    "content": "//\n//  GeometryReaderDemo.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/06/12.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct GeometryReaderDemo: View {\n    var body: some View {\n        GeometryReader { geo in\n            KFImage(\n                ImageLoader.sampleImageURLs.first\n            )\n                .placeholder { ProgressView() }\n                .forceRefresh()\n                .resizable()\n                .scaledToFit()\n                .frame(width: geo.size.width)\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct GeometryReaderDemo_Previews: PreviewProvider {\n    static var previews: some View {\n        GeometryReaderDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/GridDemo.swift",
    "content": "//\n//  GridDemo.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/03/02.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\n\n@available(iOS 14.0, *)\nstruct GridDemo: View {\n\n    @State var columns = [\n        GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())\n    ]\n    var body: some View {\n        ScrollView {\n            LazyVGrid(columns: columns) {\n                ForEach(1..<700) { i in\n                    ImageCell(index: i).frame(height: columns.count == 1 ? 300 : 150)\n                }\n            }\n        }.navigationBarTitle(Text(\"Grid\"))\n        .toolbar {\n            ToolbarItem(placement: .navigationBarTrailing) {\n                Button(action: {\n\n                    withAnimation(Animation.easeInOut(duration: 0.25)) {\n                        self.columns = Array(repeating: .init(.flexible()), count: self.columns.count % 4 + 1)\n                    }\n                }) {\n                    Image(systemName: \"square.grid.2x2\")\n                        .font(.title)\n                        .foregroundColor(.primary)\n                }\n            }\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct GridDemo_Previews: PreviewProvider {\n    static var previews: some View {\n        GridDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/LazyVStackDemo.swift",
    "content": "//\n//  LazyVStackDemo.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/03/02.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct LazyVStackDemo: View {\n    @State private var singleImage = false\n    \n    var body: some View {\n        ScrollView {\n            // Checking for #1839\n            Toggle(\"Single Image\", isOn: $singleImage).padding()\n            LazyVStack {\n                ForEach(1..<700) { i in\n                    if singleImage {\n                        KFImage.url(ImageLoader.roseImage(index: 1))\n                    } else {\n                        ImageCell(index: i).frame(width: 300, height: 300)\n                    }\n                }\n            }\n        }.navigationBarTitle(Text(\"Lazy Stack\"), displayMode: .inline)\n    }\n}\n\n@available(iOS 14.0, *)\nstruct LazyVStackDemo_Previews: PreviewProvider {\n    static var previews: some View {\n        LazyVStackDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/ListDemo.swift",
    "content": "//\n//  ListDemo.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2019/06/18.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Kingfisher\nimport SwiftUI\n\n@available(iOS 14.0, *)\nstruct ListDemo : View {\n\n    var body: some View {\n        List(1 ..< 700) { i in\n            ImageCell(index: i)\n                .frame(height: 300)\n        }.navigationBarTitle(Text(\"SwiftUI List\"), displayMode: .inline)\n    }\n}\n\n@available(iOS 14.0, *)\nstruct ImageCell: View {\n\n    var alreadyCached: Bool {\n        ImageCache.default.isCached(forKey: url.absoluteString)\n    }\n\n    let index: Int\n    var url: URL {\n        URL(string: \"https://github.com/onevcat/Flower-Data-Set/raw/master/rose/rose-\\(index).jpg\")!\n    }\n\n    var body: some View {\n        HStack(alignment: .center) {\n            Spacer()\n            KFImage.url(url)\n                .resizable()\n                .onSuccess { r in\n                    print(\"Success: \\(self.index) - \\(r.cacheType)\")\n                }\n                .onFailure { e in\n                    print(\"Error \\(self.index): \\(e)\")\n                }\n                .onProgress { downloaded, total in\n                    print(\"\\(downloaded) / \\(total))\")\n                }\n                .placeholder {\n                    HStack {\n                        Image(systemName: \"arrow.2.circlepath.circle\")\n                            .resizable()\n                            .frame(width: 50, height: 50)\n                            .padding(10)\n                        Text(\"Loading...\").font(.title)\n                    }\n                    .foregroundColor(.gray)\n                }\n                .fade(duration: 1)\n                .cancelOnDisappear(true)\n                .aspectRatio(contentMode: .fit)\n                .cornerRadius(20)\n\n            Spacer()\n        }.padding(.vertical, 12)\n    }\n\n}\n\n@available(iOS 14.0, *)\nstruct SwiftUIList_Previews : PreviewProvider {\n    static var previews: some View {\n        ListDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/LoadTransitionDemo.swift",
    "content": "//\n//  LoadTransitionDemo.swift\n//  Kingfisher\n//\n//  Copyright (c) 2025 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct LoadTransitionDemo: View {\n    @State private var imageIndex = 0\n    @State private var currentTransition: TransitionType = .none\n\n    let columns = [\n        GridItem(.flexible(), spacing: 10),\n        GridItem(.flexible(), spacing: 10),\n        GridItem(.flexible(), spacing: 10)\n    ]\n\n    var body: some View {\n        VStack(spacing: 20) {\n            // Image display area\n            Group {\n                switch currentTransition {\n                case .none:\n                    KFImage(currentTransition.url)\n                        .placeholder { placeholderView }\n                        .contentConfigure { content in\n                            content\n                                .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))\n                        }\n                        .forceTransition()\n                        .resizable()\n                        .aspectRatio(contentMode: .fit)\n                case .fade:\n                    KFImage(currentTransition.url)\n                        .placeholder { placeholderView }\n                        .contentConfigure { content in\n                            content\n                                .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))\n                        }\n                        .forceTransition()\n                        .fade(duration: 0.5)\n                        .resizable()\n                        .aspectRatio(contentMode: .fit)\n                default:\n                    KFImage(currentTransition.url)\n                        .placeholder { placeholderView }\n                        .contentConfigure { content in\n                            content\n                                .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))\n                        }\n                        .forceTransition()\n                        .loadTransition(currentTransition.transition, animation: currentTransition.animation)\n                        .resizable()\n                        .aspectRatio(contentMode: .fit)\n                }\n            }\n            .padding(16)\n            .frame(width: 300, height: 300)\n            .background(Color.gray.opacity(0.3))\n            .cornerRadius(16)\n            .shadow(radius: 5)\n\n            Spacer()\n\n            // Transition buttons\n            LazyVGrid(columns: columns, spacing: 15) {\n                ForEach(TransitionType.allCases, id: \\.self) { type in\n                    Button(action: {\n                        // Clear cache to ensure transition is visible\n                        if let currentURL = URL(string: currentTransition.urlString) {\n                            KingfisherManager.shared.cache.removeImage(forKey: currentURL.absoluteString)\n                        }\n                        currentTransition = type\n                    }) {\n                        Text(type.rawValue)\n                            .font(.system(size: 14, weight: .medium))\n                            .frame(maxWidth: .infinity)\n                            .padding(.vertical, 12)\n                            .background(currentTransition == type ? Color.blue : Color.gray)\n                            .foregroundColor(.white)\n                            .cornerRadius(8)\n                    }\n                }\n            }\n            .padding(.horizontal)\n            \n            Spacer()\n        }\n        .navigationBarTitle(\"Load Transition\", displayMode: .inline)\n    }\n    \n    private var placeholderView: some View {\n        RoundedRectangle(cornerRadius: 8)\n            .fill(Color.gray.opacity(0.2))\n            .overlay(ProgressView())\n    }\n\n    enum TransitionType: String, CaseIterable {\n        case none = \"None\"\n        case fade = \"Fade\"\n        case slide = \"Slide\"\n        case scale = \"Scale\"\n        case opacity = \"Opacity\"\n        case blurReplace = \"Blur\"\n\n        @MainActor\n        var transition: AnyTransition {\n            switch self {\n            case .none, .fade:\n                return .identity\n            case .slide:\n                return .slide\n            case .scale:\n                return .scale\n            case .opacity:\n                return .opacity\n            case .blurReplace:\n                if #available(iOS 17.0, *) {\n                    return AnyTransition(.blurReplace())\n                } else {\n                    return .scale  // Fallback for iOS < 17\n                }\n            }\n        }\n        \n        var animation: Animation? {\n            switch self {\n            case .none, .fade:\n                return nil\n            case .slide:\n                return .easeInOut(duration: 0.5)\n            case .scale:\n                return .spring()\n            case .opacity:\n                return .easeInOut(duration: 0.4)\n            case .blurReplace:\n                if #available(iOS 17.0, *) {\n                    return .bouncy(duration: 0.8)\n                } else {\n                    return .spring()\n                }\n            }\n        }\n        \n        var urlString: String {\n            let index = TransitionType.allCases.firstIndex(of: self) ?? 0\n            let urls = ImageLoader.sampleImageURLs\n            return urls[index % urls.count].absoluteString\n        }\n        \n        var url: URL {\n            URL(string: urlString)!\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct LoadTransitionDemo_Previews: PreviewProvider {\n    static var previews: some View {\n        LoadTransitionDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/LoadingFailureDemo.swift",
    "content": "//\n//  LoadingFailureDemo.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2025/06/29.\n//\n//  Copyright (c) 2025 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct LoadingFailureDemo: View {\n\n    var url: URL {\n        URL(string: \"https://example.com\")!\n    }\n    \n    var warningImage: UIImage {\n        let config = UIImage.SymbolConfiguration(pointSize: 50)\n        return UIImage(\n            systemName: \"wrongwaysign\",\n            withConfiguration: config\n        )!\n    }\n    \n    var body: some View {\n        VStack {\n            KFImage(url)\n                .onFailureView {\n                    ZStack {\n                        RoundedRectangle(cornerRadius: 20)\n                            .fill(Color.red.opacity(0.5))\n                        Image(systemName: \"exclamationmark.triangle.fill\")\n                            .resizable()\n                            .frame(width: 50, height: 47)\n                            .foregroundColor(.yellow)\n                    }\n                }\n                .frame(width: 200, height: 200)\n            Text(\"onFailureView\")\n            Spacer().frame(height: 20)\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct LoadingFailureDemo_Previews: PreviewProvider {\n    static var previews: some View {\n        LoadingFailureDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/MainView.swift",
    "content": "//\n//  MainView.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2019/08/07.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct MainView: View {\n    var body: some View {\n        List {\n            Section {\n                Button(\n                    action: {\n                        KingfisherManager.shared.cache.clearMemoryCache()\n                        KingfisherManager.shared.cache.clearDiskCache()\n                    },\n                    label: {\n                        Text(\"Clear Cache\").foregroundColor(.blue)\n                    }\n                )\n            }\n            \n            Section(header: Text(\"Demo\")) {\n                NavigationLink(destination: SingleViewDemo()) { Text(\"Basic Image\") }\n                NavigationLink(destination: SizingAnimationDemo()) { Text(\"Sizing Toggle\") }\n                NavigationLink(destination: ListDemo()) { Text(\"List\") }\n                NavigationLink(destination: LazyVStackDemo()) { Text(\"Stack\") }\n                NavigationLink(destination: GridDemo()) { Text(\"Grid\") }\n                NavigationLink(destination: AnimatedImageDemo()) { Text(\"Animated Image\") }\n                NavigationLink(destination: GeometryReaderDemo()) { Text(\"Geometry Reader\") }\n                NavigationLink(destination: TransitionViewDemo()) { Text(\"Transition\") }\n                NavigationLink(destination: LoadTransitionDemo()) { Text(\"Load Transition\") }\n                NavigationLink(destination: ProgressiveJPEGDemo()) { Text(\"Progressive JPEG\") }\n                NavigationLink(destination: LoadingFailureDemo()) { Text(\"Loading Failure\") }\n                NavigationLink(destination: PhotosPickerDemo()) { Text(\"Photos Picker\") }\n            }\n            \n            Section(header: Text(\"Regression Cases\")) {\n                NavigationLink(destination: Issue1998View()) { Text(\"#1998\") }\n                NavigationLink(destination: Issue2035View()) { Text(\"#2035\") }\n                NavigationLink(destination: Issue2295View()) { Text(\"#2295\") }\n                NavigationLink(destination: Issue2352View()) { Text(\"#2352\") }\n            }\n        }.navigationBarTitle(Text(\"SwiftUI Sample\"))\n    }\n}\n\n@available(iOS 14.0, *)\nstruct MainView_Previews: PreviewProvider {\n    static var previews: some View {\n        MainView()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/PhotosPickerDemo.swift",
    "content": "//\n//  PhotosPickerDemo.swift\n//  Kingfisher\n//\n//  Created by nuomi1 on 2026/1/7.\n//\n//  Copyright (c) 2026 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport Kingfisher\nimport PhotosUI\nimport SwiftUI\n\nstruct PhotosPickerDemo: View {\n    @State private var isPresented = false\n\n    var body: some View {\n        if #available(iOS 16.0, *) {\n            PhotosPickerRealDemo()\n        } else {\n            Button {\n                isPresented = true\n            } label: {\n                Text(\"Tap Me\")\n            }\n            .alert(isPresented: $isPresented) {\n                Alert(title: Text(\"Warning!\"), message: Text(\"Only supports iOS 16+\"))\n            }\n        }\n    }\n}\n\n@available(iOS 16.0, *)\nprivate struct PhotosPickerRealDemo: View {\n    @State private var pickerItem: PhotosPickerItem?\n\n    var body: some View {\n        if let pickerItem {\n            KFImage\n                .dataProvider(PhotosPickerItemImageDataProvider(pickerItem: pickerItem))\n        }\n\n        PhotosPicker(\n            \"Tap Me\",\n            selection: $pickerItem,\n            matching: .images,\n            photoLibrary: .shared()\n        )\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/ProgressiveJPEGDemo.swift",
    "content": "//\n//  ProgressiveJPEGDemo.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2025/03/03.\n//\n//  Copyright (c) 2025 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Kingfisher\nimport SwiftUI\n\n@available(iOS 14.0, *)\nstruct ProgressiveJPEGDemo: View {\n    \n    @State private var totalSize: Int64?\n    @State private var receivedSize: Int64?\n    \n    var body: some View {\n        KFImage(ImageLoader.progressiveImageURL)\n            .progressiveJPEG()\n            .onProgress({ receivedSize, totalSize in\n                self.totalSize = totalSize\n                self.receivedSize = receivedSize\n            })\n            .resizable()\n            .frame(width: 300, height: 300)\n        if let totalSize = totalSize, let receivedSize = receivedSize {\n            Text(\"Received: \\(receivedSize) / \\(totalSize)\")\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct ProgressiveJPEGDemo_Previews : PreviewProvider {\n    static var previews: some View {\n        ProgressiveJPEGDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/Regression/Issue1998View.swift",
    "content": "//\n//  SingleListDemo.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2022/09/21.\n//\n//  Copyright (c) 2022 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct Issue1998View: View {\n    var body: some View {\n        Text(\"This is a test case for #1988\")\n        \n        List {\n            ForEach(1...100, id: \\.self) { idx in\n                KFImage(ImageLoader.sampleImageURLs.first)\n                    .startLoadingBeforeViewAppear()\n                    .resizable()\n                    .frame(width: 48, height: 48)\n            }\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct SingleListDemo_Previews: PreviewProvider {\n    static var previews: some View {\n        Issue1998View()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/Regression/Issue2035View.swift",
    "content": "//\n//  Issue2035View.swift\n//  Kingfisher\n//\n//  Created by jp20028 on 2023/02/23.\n//\n//  Copyright (c) 2023 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct Issue2035View: View {\n    var body: some View {\n        KFImage(nil)\n            .startLoadingBeforeViewAppear()\n            .onSuccess { _ in\n                print(\"Done\")\n            }\n            .onFailure { err in\n                print(err)\n            }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct Issue2035View_Previews: PreviewProvider {\n    static var previews: some View {\n        Issue2035View()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/Regression/Issue2295View.swift",
    "content": "//\n//  Issue2295View.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2024/09/21.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct Issue2295View: View {\n    \n    @State private var count = 0\n    \n    var body: some View {\n        Text(\"This is a test case for #2295\")\n        Text(\"Count: \\(count)\")\n        ScrollView {\n            VStack {\n                Text(\"Tapping these to add count.\")\n                HStack {\n                    KFImage(ImageLoader.sampleImageURLs.first)\n                        .resizable()\n                        .frame(width: 150, height: 150)\n                        .onTapGesture {\n                            count += 1\n                        }\n                    KFAnimatedImage(ImageLoader.sampleImageURLs.first)\n                        .frame(width: 150, height: 150)\n                        .onTapGesture {\n                            count += 1\n                        }\n                }\n            }\n            Divider()\n            VStack {\n                Text(\"These are not tappable.\")\n                HStack {\n                    KFImage(ImageLoader.sampleImageURLs.first)\n                        .resizable()\n                        .frame(width: 150, height: 150)\n                        .allowsHitTesting(false)\n                        .onTapGesture {\n                            count += 1\n                        }\n                    KFAnimatedImage(ImageLoader.sampleImageURLs.first)\n                        .frame(width: 150, height: 150)\n                        .allowsHitTesting(false)\n                        .onTapGesture {\n                            count += 1\n                        }\n                }\n            }\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct Issue2295View_Previews: PreviewProvider {\n    static var previews: some View {\n        Issue1998View()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/Regression/Issue2352View.swift",
    "content": "//\n//  Issue2352View.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2025/02/04.\n//\n//  Copyright (c) 2025 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct Issue2352View: View {\n    var body: some View {\n        List {\n            ForEach(0..<40, id: \\.self) { row in\n                KFAnimatedImage\n                    .url(\n                        URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/refs/heads/master/DemoAppImage/GIF/jumping.gif\")!\n                    )\n                    .backgroundDecode()\n                    .scaleFactor(UIScreen.main.scale)\n                    .scaledToFill()\n                    .frame(width: 50, height: 50)\n                    .clipShape(.circle)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/SingleViewDemo.swift",
    "content": "//\n//  SingleViewDemo.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2019/06/18.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Kingfisher\nimport SwiftUI\n\n@available(iOS 14.0, *)\nstruct SingleViewDemo : View {\n\n    @State private var index = 1\n    @State private var blackWhite = false\n    @State private var forceTransition = true\n\n    var url: URL {\n        URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher-\\(self.index).jpg\")!\n    }\n\n    var body: some View {\n        VStack {\n            KFImage(url)\n                .cacheOriginalImage()\n                .setProcessor(blackWhite ? BlackWhiteProcessor() : DefaultImageProcessor())\n                .onSuccess { r in\n                    print(\"suc: \\(r)\")\n                }\n                .onFailure { e in\n                    print(\"err: \\(e)\")\n                }\n                .placeholder { progress in\n                    ProgressView(progress).frame(width: 100, height: 100)\n                        .border(Color.blue)\n                }\n                .fade(duration: index == 1 ? 0 : 1) // Do not animate for the first image. Otherwise it causes an unwanted animation when the page is shown.\n                .forceTransition(forceTransition)\n                .resizable()\n                .frame(width: 300, height: 300)\n                .cornerRadius(20)\n                .border(Color.red)\n                .shadow(radius: 5)\n                .frame(width: 320, height: 320)\n\n            Button(action: {\n                self.index = (self.index % 10) + 1\n            }) { Text(\"Next Image\") }\n            Button(action: {\n                self.blackWhite.toggle()\n            }) { Text(\"Black & White\") }\n            Toggle(\"Force Transition?\", isOn: $forceTransition)\n                .frame(width: 300)\n\n        }.navigationBarTitle(Text(\"Basic Image\"), displayMode: .inline)\n    }\n}\n\n@available(iOS 14.0, *)\nstruct SingleViewDemo_Previews : PreviewProvider {\n    static var previews: some View {\n        SingleViewDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/SizingAnimationDemo.swift",
    "content": "//\n//  SizingAnimationDemo.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/03/02.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct SizingAnimationDemo: View {\n    @State var imageSize: CGFloat = 250\n    @State var isPlaying = false\n\n    var body: some View {\n        VStack {\n            KFImage(URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher-1.jpg\")!)\n                .resizable()\n                .aspectRatio(contentMode: .fill)\n                .frame(width: imageSize, height: imageSize)\n                .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))\n                .frame(width: 350, height: 350)\n            Button(action: {\n                playButtonAction()\n            }) {\n                Image(systemName: self.isPlaying ? \"pause.fill\" : \"play.fill\")\n                    .font(.system(size: 60))\n            }\n        }\n\n    }\n    func playButtonAction() {\n        withAnimation(Animation.spring(response: 0.45, dampingFraction: 0.475, blendDuration: 0)) {\n            if self.imageSize == 250 {\n                self.imageSize = 350\n            } else {\n                self.imageSize = 250\n            }\n            self.isPlaying.toggle()\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nstruct SizingAnimationDemo_Previews: PreviewProvider {\n    static var previews: some View {\n        SizingAnimationDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/SwiftUIViews/TransitionViewDemo.swift",
    "content": "//\n//  TransitionViewDemo.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/08/03.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(iOS 14.0, *)\nstruct TransitionViewDemo: View {\n    @State private var showDetails = false\n    \n    var body: some View {\n        VStack {\n            Button(showDetails ? \"Hide\" : \"Show\") {\n                withAnimation {\n                    showDetails.toggle()\n                }\n            }\n            if showDetails {\n                KFImage(ImageLoader.sampleImageURLs.first)\n                    .transition(.slide)\n            }\n            Spacer()\n        }.frame(height: 500)\n    }\n}\n\n@available(iOS 14.0, *)\nstruct TransitionViewDemo_Previews: PreviewProvider {\n    static var previews: some View {\n        TransitionViewDemo()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/AVAssetImageGeneratorViewController.swift",
    "content": "//\n//  AVAssetImageGeneratorViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/08/09.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport AVKit\nimport Kingfisher\n\nclass AVAssetImageGeneratorViewController: UIViewController {\n    @IBOutlet weak var imageView: UIImageView!\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        let provider = AVAssetImageDataProvider(\n            assetURL: URL(string: \"https://github.com/onevcat/sample-files/raw/main/video/mp4/astronaut_flying_fantasy.mp4\")!,\n            seconds: 6.0\n        )\n        KF.dataProvider(provider).set(to: imageView)\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/AutoSizingTableViewController.swift",
    "content": "//\n//  AutoSizingTableViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/03/15.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\n// Cell with an image view (loading by Kingfisher) with fix width and dynamic height which keeps the image with aspect ratio.\nclass AutoSizingTableViewCell: UITableViewCell {\n    \n    static let p = ResizingImageProcessor(referenceSize: .init(width: 200, height: CGFloat.infinity), mode: .aspectFit)\n    \n    @IBOutlet weak var leadingImageView: UIImageView!\n    @IBOutlet weak var sizeLabel: UILabel!\n    \n    var updateLayout: (() -> Void)?\n    \n    func set(with url: URL) {\n        leadingImageView.kf.setImage(with: url, options: [.processor(AutoSizingTableViewCell.p), .transition(.fade(1))]) { r in\n            if case .success(let value) = r {\n                self.sizeLabel.text = \"\\(value.image.size.width) x \\(value.image.size.height)\"\n                self.updateLayout?()\n            } else {\n                self.sizeLabel.text = \"\"\n            }\n        }\n    }\n}\n\nclass AutoSizingTableViewController: UIViewController {\n    @IBOutlet weak var tableView: UITableView!\n    var data: [Int] = Array(1..<700)\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        tableView.estimatedRowHeight = 150\n    }\n    \n    override func viewDidAppear(_ animated: Bool) {\n        super.viewDidAppear(animated)\n        UIView.setAnimationsEnabled(false)\n    }\n    \n    override func viewWillDisappear(_ animated: Bool) {\n        super.viewWillDisappear(animated)\n        UIView.setAnimationsEnabled(true)\n    }\n}\n\nextension AutoSizingTableViewController: UITableViewDataSource {\n    private func updateLayout() {\n        tableView.beginUpdates()\n        tableView.endUpdates()\n    }\n    \n    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        return data.count\n    }\n    \n    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"AutoSizingTableViewCell\", for: indexPath) as! AutoSizingTableViewCell\n        cell.set(with: ImageLoader.roseImage(index: data[indexPath.row]))\n        cell.updateLayout = { [weak self] in\n            self?.updateLayout()\n        }\n        return cell\n    }\n    \n    \n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/DetailImageViewController.swift",
    "content": "//\n//  DetailImageViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/25.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\n\nclass DetailImageViewController: UIViewController {\n\n    var imageURL: URL!\n    @IBOutlet weak var imageView: UIImageView!\n    @IBOutlet weak var scrollView: UIScrollView!\n    @IBOutlet weak var infoLabel: UILabel!\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        scrollView.delegate = self\n        \n        imageView.kf.setImage(with: imageURL, options: [.memoryCacheExpiration(.expired)]) { result in\n            guard let image = try? result.get().image else {\n                return\n            }\n            let scrollViewFrame = self.scrollView.frame\n            let scaleWidth = scrollViewFrame.size.width / image.size.width\n            let scaleHeight = scrollViewFrame.size.height / image.size.height\n            let minScale = min(scaleWidth, scaleHeight)\n            self.scrollView.minimumZoomScale = minScale\n            DispatchQueue.main.async {\n                self.scrollView.zoomScale = minScale\n            }\n            \n            self.infoLabel.text = \"\\(image.size)\"\n        }\n    }\n}\n\nextension DetailImageViewController: UIScrollViewDelegate {\n    func viewForZooming(in scrollView: UIScrollView) -> UIView? {\n        return imageView\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/GIFHeavyViewController.swift",
    "content": "//\n//  GIFHeavyViewController.swift\n//  Kingfisher\n//\n//  Created by taras on 16/04/2021.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nclass GIFHeavyViewController: UIViewController {\n    let stackView = UIStackView()\n    let imageView_1 = AnimatedImageView()\n    let imageView_2 = AnimatedImageView()\n    let imageView_3 = AnimatedImageView()\n    let imageView_4 = AnimatedImageView()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        stackView.translatesAutoresizingMaskIntoConstraints = false\n        \n        view.addSubview(stackView)\n        \n        if #available(iOS 11.0, *) {\n            NSLayoutConstraint.activate([\n                stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),\n                stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),\n                stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),\n                stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),\n            ])\n        } else {\n            NSLayoutConstraint.activate([\n                stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),\n                stackView.topAnchor.constraint(equalTo: view.topAnchor),\n                stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),\n                stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),\n            ])\n        }\n        \n        stackView.axis = .vertical\n        stackView.distribution = .fillEqually\n        \n        stackView.addArrangedSubview(imageView_1)\n        stackView.addArrangedSubview(imageView_2)\n        stackView.addArrangedSubview(imageView_3)\n        stackView.addArrangedSubview(imageView_4)\n        \n        imageView_1.contentMode = .scaleAspectFit\n        imageView_2.contentMode = .scaleAspectFit\n        imageView_3.contentMode = .scaleAspectFit\n        imageView_4.contentMode = .scaleAspectFit\n        \n        let url = URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/GifHeavy.gif\")\n\n        imageView_1.kf.setImage(with: url)\n        imageView_2.kf.setImage(with: url)\n        imageView_3.kf.setImage(with: url)\n        imageView_4.kf.setImage(with: url)\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/GIFViewController.swift",
    "content": "//\n//  GIFViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/25.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nclass GIFViewController: UIViewController {\n\n    @IBOutlet weak var imageView: UIImageView!\n    @IBOutlet weak var animatedImageView: AnimatedImageView!\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        let url = ImageLoader.gifImageURLs.last!\n        \n        // Should need to use different cache key to prevent data overwritten by each other.\n        KF.url(url, cacheKey: \"\\(url)-imageview\").set(to: imageView)\n        KF.url(url, cacheKey: \"\\(url)-animated_imageview\").set(to: animatedImageView)\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/HighResolutionCollectionViewController.swift",
    "content": "//\n//  HighResolutionCollectionViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/24.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nprivate let reuseIdentifier = \"HighResolution\"\n\nclass HighResolutionCollectionViewController: UICollectionViewController {\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"High Resolution\"\n        setupOperationNavigationBar()\n    }\n\n    override func numberOfSections(in collectionView: UICollectionView) -> Int {\n        return 1\n    }\n\n\n    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {\n        return ImageLoader.highResolutionImageURLs.count * 30\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath)\n    {\n        (cell as! ImageCollectionViewCell).cellImageView.kf.cancelDownloadTask()\n    }\n    \n    override func collectionView(\n        _ collectionView: UICollectionView,\n        willDisplay cell: UICollectionViewCell,\n        forItemAt indexPath: IndexPath)\n    {\n        let imageView = (cell as! ImageCollectionViewCell).cellImageView!\n        let url = ImageLoader.highResolutionImageURLs[indexPath.row % ImageLoader.highResolutionImageURLs.count]\n        // Use different cache key to prevent reuse the same image. It is just for\n        // this demo. Normally you can just use the URL to set image.\n\n        // This should crash most devices due to memory pressure.\n        // let resource = KF.ImageResource(downloadURL: url, cacheKey: \"\\(url.absoluteString)-\\(indexPath.row)\")\n        // imageView.kf.setImage(with: resource)\n\n        // This would survive on even the lowest spec devices!\n        KF.url(url, cacheKey: \"\\(url.absoluteString)-\\(indexPath.row)\")\n            .downsampling(size: CGSize(width: 250, height: 250))\n            .cacheOriginalImage()\n            .set(to: imageView)\n    }\n    \n    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {\n        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)\n        return cell\n    }\n    \n    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {\n        if segue.identifier == \"showImage\" {\n            let vc = segue.destination as! DetailImageViewController\n            let index = collectionView.indexPathsForSelectedItems![0].row\n            vc.imageURL =  ImageLoader.highResolutionImageURLs[index % ImageLoader.highResolutionImageURLs.count]\n        }\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/ImageCollectionViewCell.swift",
    "content": "//\n//  ImageCollectionViewCell.swift\n//  Kingfisher-Demo\n//\n//  Created by Wei Wang on 15/4/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\n\nclass ImageCollectionViewCell: UICollectionViewCell {\n    \n    @IBOutlet weak var cellImageView: UIImageView!\n    \n    #if os(tvOS)\n    override func awakeFromNib() {\n        super.awakeFromNib()\n\n        cellImageView.adjustsImageWhenAncestorFocused = true\n        cellImageView.clipsToBounds = false\n    }\n    #endif\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/ImageDataProviderCollectionViewController.swift",
    "content": "//\n//  ImageDataProviderCollectionViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/12/08.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nprivate let reuseIdentifier = \"ImageDataProviderCell\"\n\nclass ImageDataProviderCollectionViewController: UICollectionViewController {\n\n    let model: [(String, UIColor)] = [\n        (\"A\", .red), (\"B\", .green), (\"C\", .blue), (\"D\", .yellow), (\"赵\", .purple), (\"钱\", .orange),\n        (\"孙\", .black), (\"李\", .brown), (\"ア\", .darkGray), (\"イ\", .cyan), (\"ウ\", .magenta)]\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"Provider\"\n        setupOperationNavigationBar()\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {\n        return model.count\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {\n        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell\n    \n        let pair = model[indexPath.row]\n        let provider = UserNameLetterIconImageProvider(userNameFirstLetter: pair.0, backgroundColor: pair.1)\n        KF.dataProvider(provider)\n            .roundCorner(radius: .point(75))\n            .set(to: cell.cellImageView)\n\n        return cell\n    }\n}\n\nstruct UserNameLetterIconImageProvider: ImageDataProvider {\n    var cacheKey: String { return letter }\n    let letter: String\n    let color: UIColor\n    \n    init(userNameFirstLetter: String, backgroundColor: UIColor) {\n        letter = userNameFirstLetter\n        color = backgroundColor\n    }\n    \n    func data(handler: @escaping (Result<Data, any Error>) -> Void) {\n        let letter = self.letter as NSString\n        let rect = CGRect(x: 0, y: 0, width: 250, height: 250)\n        \n        let format = UIGraphicsImageRendererFormat.default()\n        format.scale = 1\n        \n        let renderer = UIGraphicsImageRenderer(size: rect.size, format: format)\n        let data = renderer.pngData { context in\n            color.setFill()\n            context.fill(rect)\n            \n            let attributes = [\n                NSAttributedString.Key.foregroundColor: UIColor.white,\n                .font: UIFont.systemFont(ofSize: 200)\n            ]\n            \n            let textSize = letter.size(withAttributes: attributes)\n            let textRect = CGRect(\n                x: (rect.width - textSize.width) / 2,\n                y: (rect.height - textSize.height) / 2,\n                width: textSize.width,\n                height: textSize.height)\n            letter.draw(in: textRect, withAttributes: attributes)\n        }\n        handler(.success(data))\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/IndicatorCollectionViewController.swift",
    "content": "//\n//  IndicatorCollectionViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/26.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nprivate let reuseIdentifier = \"IndicatorCell\"\nlet gifData: Data = {\n    let url = Bundle.main.url(forResource: \"loader\", withExtension: \"gif\")!\n    return try! Data(contentsOf: url)\n}()\n\nclass IndicatorCollectionViewController: UICollectionViewController {\n\n    class MyIndicator: Indicator {\n        \n        var timer: Timer?\n        \n        func startAnimatingView() {\n            view.isHidden = false\n            timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { _ in\n                Task { @MainActor in\n                    UIView.animate(withDuration: 0.2, animations: {\n                        if self.view.backgroundColor == .red {\n                            self.view.backgroundColor = .orange\n                        } else {\n                            self.view.backgroundColor = .red\n                        }\n                    })\n                }\n            }\n        }\n        \n        func stopAnimatingView() {\n            view.isHidden = true\n            timer?.invalidate()\n        }\n        \n        var view: IndicatorView = {\n            let view = UIView()\n            view.heightAnchor.constraint(equalToConstant: 30).isActive = true\n            view.widthAnchor.constraint(equalToConstant: 30).isActive = true\n            \n            view.backgroundColor = .red\n            return view\n        }()\n    }\n    \n    let indicators: [String] = [\n        \"None\",\n        \"UIActivityIndicatorView\",\n        \"GIF Image\",\n        \"Custom\"\n    ]\n    var selectedIndicatorIndex: Int = 1 {\n        didSet {\n            collectionView.reloadData()\n        }\n    }\n    var selectedIndicatorType: IndicatorType {\n        switch selectedIndicatorIndex {\n        case 0: return .none\n        case 1: return .activity\n        case 2: return .image(imageData: gifData)\n        case 3: return .custom(indicator: MyIndicator())\n        default: fatalError()\n        }\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        setupOperationNavigationBar()\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {\n        return ImageLoader.sampleImageURLs.count\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {\n        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell\n        cell.cellImageView.kf.indicatorType = selectedIndicatorType\n        KF.url(ImageLoader.sampleImageURLs[indexPath.row])\n            .memoryCacheExpiration(.expired)\n            .diskCacheExpiration(.expired)\n            .set(to: cell.cellImageView)\n        return cell\n    }\n    \n    override func alertPopup(_ sender: Any) -> UIAlertController {\n        let alert = super.alertPopup(sender)\n        for item in indicators.enumerated() {\n            alert.addAction(UIAlertAction.init(title: item.element, style: .default) { _ in\n                self.selectedIndicatorIndex = item.offset\n            })\n        }\n        return alert\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/InfinityCollectionViewController.swift",
    "content": "//\n//  InfinityCollectionViewController.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/11/19.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nprivate let reuseIdentifier = \"InfinityCell\"\n\nclass InfinityCollectionViewController: UICollectionViewController {\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"Infinity\"\n        setupOperationNavigationBar()\n    }\n\n    override func numberOfSections(in collectionView: UICollectionView) -> Int {\n        return 1\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {\n        return 10000000\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {\n        let cell = collectionView\n            .dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell\n        let urls = ImageLoader.sampleImageURLs\n        let url = urls[indexPath.row % urls.count]\n\n        // Mark each row as a new image.\n        let resource = KF.ImageResource(downloadURL: url, cacheKey: \"key-\\(indexPath.row)\")\n        KF.resource(resource).set(to: cell.cellImageView)\n\n        return cell\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/LivePhotoViewController.swift",
    "content": "//\n//  LivePhotoViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2024/10/05.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport PhotosUI\nimport Kingfisher\n\nclass LivePhotoViewController: UIViewController {\n    \n    private var livePhotoView: PHLivePhotoView!\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"Live Photo\"\n        setupOperationNavigationBar()\n        \n        livePhotoView = PHLivePhotoView()\n        livePhotoView.translatesAutoresizingMaskIntoConstraints = false\n        \n        view.addSubview(livePhotoView)\n        NSLayoutConstraint.activate([\n            livePhotoView.heightAnchor.constraint(equalToConstant: 300),\n            livePhotoView.widthAnchor.constraint(equalToConstant: 300),\n            livePhotoView.centerXAnchor.constraint(equalTo: view.centerXAnchor),\n            livePhotoView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -30)\n        ])\n        \n        let urls = [\n            \"https://github.com/onevcat/Kingfisher-TestImages/raw/refs/heads/master/LivePhotos/live_photo_sample.HEIC\",\n            \"https://github.com/onevcat/Kingfisher-TestImages/raw/refs/heads/master/LivePhotos/live_photo_sample.MOV\"\n        ].compactMap(URL.init)\n        livePhotoView.kf.setImage(with: urls, completionHandler: { result in\n            switch result {\n            case .success(let r):\n                print(\"Live Photo done. \\(r.loadingInfo.cacheType)\")\n                print(\"Info: \\(String(describing: r.info))\")\n            case .failure(let error):\n                print(\"Live Photo error: \\(error)\")\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/MainViewController.swift",
    "content": "//\n//  MainViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/18.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\n\nclass MainViewController: UITableViewController {\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"Kingfisher\"\n        setupOperationNavigationBar()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/NetworkMetricsViewController.swift",
    "content": "//\n//  NetworkMetricsViewController.swift\n//  Demo\n//\n//  Created by FunnyValentine on 2025/07/25.\n//\n\nimport UIKit\nimport Kingfisher\n\nclass NetworkMetricsViewController: UIViewController {\n    \n    // MARK: - UI Components\n    \n    private let imageView = UIImageView()\n    private let metricsTextView = UITextView()\n    private let fromNetworkButton = UIButton(type: .system)\n    private let fromMemoryButton = UIButton(type: .system) \n    private let fromDiskButton = UIButton(type: .system)\n    private let stackView = UIStackView()\n    private let buttonStackView = UIStackView()\n    private let metricsContainer = UIView()\n    \n    // MARK: - Properties\n    \n    private var currentImageURL = URL(string: \"https://picsum.photos/200/150?random=\\(Int.random(in: 1...1000))\")!\n    private var showImage = true\n    \n    // MARK: - Lifecycle\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        setupUI()\n        setupConstraints()\n        setupInitialContent()\n    }\n    \n    // MARK: - UI Setup\n    \n    private func setupUI() {\n        title = \"Network Metrics\"\n        view.backgroundColor = .systemBackground\n        \n        setupImageView()\n        setupMetricsTextView()\n        setupButtons()\n        setupStackViews()\n    }\n    \n    private func setupImageView() {\n        imageView.contentMode = .scaleAspectFit\n        imageView.layer.cornerRadius = 8\n        imageView.clipsToBounds = true\n        imageView.backgroundColor = .clear  // Clear background\n        imageView.translatesAutoresizingMaskIntoConstraints = false\n    }\n    \n    private func setupMetricsTextView() {\n        metricsTextView.isEditable = false\n        metricsTextView.backgroundColor = UIColor.systemGray6\n        metricsTextView.layer.cornerRadius = 8\n        metricsTextView.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)\n        metricsTextView.text = \"Tap a button to load image...\"\n        metricsTextView.translatesAutoresizingMaskIntoConstraints = false\n    }\n    \n    private func setupButtons() {\n        setupButton(fromNetworkButton, title: \"From Network\", icon: \"wifi\", color: .systemRed, action: #selector(fromNetworkButtonTapped))\n        setupButton(fromMemoryButton, title: \"From Memory\", icon: \"memorychip\", color: .systemOrange, action: #selector(fromMemoryButtonTapped))\n        setupButton(fromDiskButton, title: \"From Disk\", icon: \"internaldrive\", color: .systemPurple, action: #selector(fromDiskButtonTapped))\n    }\n    \n    private func setupButton(_ button: UIButton, title: String, icon: String, color: UIColor, action: Selector) {\n        button.setTitle(title, for: .normal)\n        button.setImage(UIImage(systemName: icon), for: .normal)\n        button.backgroundColor = color\n        button.setTitleColor(.white, for: .normal)\n        button.tintColor = .white\n        button.layer.cornerRadius = 8\n        button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)\n        button.translatesAutoresizingMaskIntoConstraints = false\n        button.addTarget(self, action: action, for: .touchUpInside)\n        \n        // Configure image and title positioning\n        button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 0)\n        button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0)\n    }\n    \n    private func setupStackViews() {\n        // Button stack view\n        buttonStackView.axis = .vertical\n        buttonStackView.distribution = .fillEqually\n        buttonStackView.spacing = 12\n        buttonStackView.translatesAutoresizingMaskIntoConstraints = false\n        \n        // Network button takes full width\n        buttonStackView.addArrangedSubview(fromNetworkButton)\n        \n        // Memory and Disk buttons in horizontal stack\n        let horizontalButtonStack = UIStackView()\n        horizontalButtonStack.axis = .horizontal\n        horizontalButtonStack.distribution = .fillEqually\n        horizontalButtonStack.spacing = 12\n        horizontalButtonStack.addArrangedSubview(fromMemoryButton)\n        horizontalButtonStack.addArrangedSubview(fromDiskButton)\n        \n        buttonStackView.addArrangedSubview(horizontalButtonStack)\n        \n        // Main stack view\n        stackView.axis = .vertical\n        stackView.spacing = 20\n        stackView.alignment = .center  // Center align all items\n        stackView.translatesAutoresizingMaskIntoConstraints = false\n        setupMetricsSection()\n        \n        stackView.addArrangedSubview(imageView)\n        stackView.addArrangedSubview(metricsContainer)\n        stackView.addArrangedSubview(buttonStackView)\n        \n        view.addSubview(stackView)\n    }\n    \n    private func setupMetricsSection() {\n        metricsContainer.translatesAutoresizingMaskIntoConstraints = false\n        \n        let titleLabel = UILabel()\n        titleLabel.text = \"Metrics Information\"\n        titleLabel.font = UIFont.systemFont(ofSize: 18, weight: .semibold)\n        titleLabel.translatesAutoresizingMaskIntoConstraints = false\n        \n        metricsContainer.addSubview(titleLabel)\n        metricsContainer.addSubview(metricsTextView)\n        \n        NSLayoutConstraint.activate([\n            titleLabel.topAnchor.constraint(equalTo: metricsContainer.topAnchor),\n            titleLabel.leadingAnchor.constraint(equalTo: metricsContainer.leadingAnchor),\n            titleLabel.trailingAnchor.constraint(equalTo: metricsContainer.trailingAnchor),\n            titleLabel.heightAnchor.constraint(equalToConstant: 22),\n            \n            metricsTextView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),\n            metricsTextView.leadingAnchor.constraint(equalTo: metricsContainer.leadingAnchor),\n            metricsTextView.trailingAnchor.constraint(equalTo: metricsContainer.trailingAnchor),\n            metricsTextView.bottomAnchor.constraint(equalTo: metricsContainer.bottomAnchor),\n            metricsTextView.heightAnchor.constraint(equalToConstant: 400)\n        ])\n    }\n    \n    private func setupConstraints() {\n        NSLayoutConstraint.activate([\n            stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),\n            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),\n            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),\n            \n            imageView.heightAnchor.constraint(equalToConstant: 150),\n            imageView.widthAnchor.constraint(equalToConstant: 200),\n\n            fromNetworkButton.heightAnchor.constraint(equalToConstant: 44),\n            fromMemoryButton.heightAnchor.constraint(equalToConstant: 44),\n            fromDiskButton.heightAnchor.constraint(equalToConstant: 44),\n            \n            // Make button stack view and metrics container full width\n            buttonStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),\n            buttonStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),\n            \n            metricsContainer.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),\n            metricsContainer.trailingAnchor.constraint(equalTo: stackView.trailingAnchor)\n        ])\n    }\n    \n    private func setupInitialContent() {\n        // Set initial placeholder\n        imageView.image = createPlaceholderImage(text: \"Tap button to load\")\n    }\n    \n    // MARK: - Actions\n    \n    @objc private func fromNetworkButtonTapped() {\n        // Set placeholder and hide image\n        showImage = false\n        imageView.image = createPlaceholderImage(text: \"Reloading...\")\n        // Clear all cache to force network download\n        KingfisherManager.shared.cache.clearCache()\n\n        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {\n            self.showImage = true\n            self.loadImage()\n        }\n    }\n    \n    @objc private func fromMemoryButtonTapped() {\n        // Set placeholder and hide image\n        showImage = false\n        imageView.image = createPlaceholderImage(text: \"Reloading...\")\n        // Clear disk cache only, keep memory cache\n        KingfisherManager.shared.cache.clearDiskCache()\n        \n        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {\n            self.showImage = true\n            self.loadImage()\n        }\n    }\n    \n    @objc private func fromDiskButtonTapped() {\n        // Set placeholder and hide image  \n        showImage = false\n        imageView.image = createPlaceholderImage(text: \"Reloading...\")\n        // Clear memory cache only, keep disk cache\n        KingfisherManager.shared.cache.clearMemoryCache()\n\n        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {\n            self.showImage = true\n            self.loadImage()\n        }\n    }\n    \n    // MARK: - Image Loading\n    \n    private func loadImage() {\n        guard showImage else { return }\n        \n        let placeholder = createPlaceholderImage(text: \"Loading...\")\n        \n        imageView.kf.setImage(\n            with: currentImageURL,\n            placeholder: placeholder,\n            options: nil,\n            completionHandler: { [weak self] result in\n                DispatchQueue.main.async {\n                    switch result {\n                    case .success(let retrieveImageResult):\n                        self?.displayMetrics(result: retrieveImageResult)\n                    case .failure(let error):\n                        self?.metricsTextView.text = \"Failed to load image: \\(error.localizedDescription)\"\n                        print(\"Error: \\(error)\")\n                    }\n                }\n            }\n        )\n    }\n    \n    // MARK: - Helper Methods\n    \n    private func createPlaceholderImage(text: String) -> UIImage {\n        let size = CGSize(width: 200, height: 150)\n        let renderer = UIGraphicsImageRenderer(size: size)\n        \n        return renderer.image { context in\n            let rect = CGRect(origin: .zero, size: size)\n            \n            // Draw background with rounded corners\n            let path = UIBezierPath(roundedRect: rect, cornerRadius: 8)\n            UIColor.systemGray5.setFill()\n            path.fill()\n            \n            // Draw text\n            let attributes: [NSAttributedString.Key: Any] = [\n                .foregroundColor: UIColor.systemGray,\n                .font: UIFont.systemFont(ofSize: 16)\n            ]\n            \n            let attributedText = NSAttributedString(string: text, attributes: attributes)\n            let textSize = attributedText.size()\n            let textRect = CGRect(\n                x: (size.width - textSize.width) / 2,\n                y: (size.height - textSize.height) / 2,\n                width: textSize.width,\n                height: textSize.height\n            )\n            \n            attributedText.draw(in: textRect)\n        }\n    }\n    \n    private func displayMetrics(result: RetrieveImageResult) {\n        var info = \"=== Image Load Results ===\\n\\n\"\n        \n        // Basic info\n        info += \"Cache Type: \\(cacheTypeDescription(result.cacheType))\\n\\n\"\n        \n        // Network Metrics\n        if let metrics = result.metrics {\n            info += \"=== Network Metrics ===\\n\"\n            info += \"✅ Downloaded from network\\n\\n\"\n            \n            // Timing breakdown\n            info += \"📊 Timing Breakdown:\\n\"\n            info += \"Total Request: \\(String(format: \"%.3f\", metrics.totalRequestDuration))s\\n\"\n            \n            if let dnsTime = metrics.domainLookupDuration {\n                info += \"DNS Lookup: \\(String(format: \"%.3f\", dnsTime))s\\n\"\n            } else {\n                info += \"DNS Lookup: N/A (cached or skipped)\\n\"\n            }\n            \n            if let connectTime = metrics.connectDuration {\n                info += \"TCP Connect: \\(String(format: \"%.3f\", connectTime))s\\n\"\n            } else {\n                info += \"TCP Connect: N/A (reused connection)\\n\"\n            }\n            \n            if let tlsTime = metrics.secureConnectionDuration {\n                info += \"TLS Handshake: \\(String(format: \"%.3f\", tlsTime))s\\n\"\n            } else {\n                info += \"TLS Handshake: N/A (HTTP or reused)\\n\"\n            }\n            \n            // Data transfer\n            info += \"\\n📈 Data Transfer:\\n\"\n            info += \"Request Body: \\(formatBytes(metrics.requestBodyBytesSent))\\n\"\n            info += \"Response Body: \\(formatBytes(metrics.responseBodyBytesReceived))\\n\"\n            \n            if let speed = metrics.downloadSpeed {\n                info += \"Download Speed: \\(formatBytes(Int64(speed)))/s\"\n                info += \"\\n\"\n            }\n            \n            // HTTP details\n            info += \"\\n🌐 HTTP Details:\\n\"\n            if let statusCode = metrics.httpStatusCode {\n                info += \"Status Code: \\(statusCode) \\(httpStatusDescription(statusCode))\\n\"\n            }\n            info += \"Redirects: \\(metrics.redirectCount)\\n\"\n            \n        } else {\n            info += \"=== Network Metrics ===\\n\"\n            info += \"💾 Loaded from cache\\n\"\n            info += \"No network request was made\\n\\n\"\n            \n            info += \"This image was served from:\\n\"\n            switch result.cacheType {\n            case .memory:\n                info += \"• Memory cache (fastest)\\n\"\n            case .disk:\n                info += \"• Disk cache (fast)\\n\"\n            case .none:\n                info += \"• Network (but no metrics available)\\n\"\n            @unknown default:\n                info += \"• Unknown cache type\\n\"\n            }\n        }\n        \n        metricsTextView.text = info\n    }\n    \n    private func cacheTypeDescription(_ cacheType: CacheType) -> String {\n        switch cacheType {\n        case .memory:\n            return \"Memory Cache 🚀\"\n        case .disk:\n            return \"Disk Cache 💽\"\n        case .none:\n            return \"Network Download 🌐\"\n        @unknown default:\n            return \"Unknown\"\n        }\n    }\n    \n    private func httpStatusDescription(_ statusCode: Int) -> String {\n        switch statusCode {\n        case 200: return \"OK\"\n        case 201: return \"Created\"\n        case 204: return \"No Content\"\n        case 301: return \"Moved Permanently\"\n        case 302: return \"Found\"\n        case 304: return \"Not Modified\"\n        case 400: return \"Bad Request\"\n        case 401: return \"Unauthorized\"\n        case 403: return \"Forbidden\"\n        case 404: return \"Not Found\"\n        case 500: return \"Internal Server Error\"\n        default: return \"\"\n        }\n    }\n    \n    private func formatBytes(_ bytes: Int64) -> String {\n        let formatter = ByteCountFormatter()\n        formatter.allowedUnits = [.useBytes, .useKB, .useMB]\n        formatter.countStyle = .file\n        return formatter.string(fromByteCount: bytes)\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/NormalLoadingViewController.swift",
    "content": "//\n//  NormalLoadingViewController.swift\n//  Kingfisher-Demo\n//\n//  Created by Wei Wang on 15/4/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nclass NormalLoadingViewController: UICollectionViewController {\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"Loading\"\n        setupOperationNavigationBar()\n    }\n}\n\nextension NormalLoadingViewController {\n    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {\n        return ImageLoader.sampleImageURLs.count\n    }\n    \n    override func collectionView(\n        _ collectionView: UICollectionView,\n        didEndDisplaying cell: UICollectionViewCell,\n        forItemAt indexPath: IndexPath)\n    {\n        // This will cancel all unfinished downloading task when the cell disappearing.\n        (cell as! ImageCollectionViewCell).cellImageView.kf.cancelDownloadTask()\n    }\n    \n    override func collectionView(\n        _ collectionView: UICollectionView,\n        willDisplay cell: UICollectionViewCell,\n        forItemAt indexPath: IndexPath)\n    {\n        let imageView = (cell as! ImageCollectionViewCell).cellImageView!\n        let url = ImageLoader.sampleImageURLs[indexPath.row]\n        KF.url(url)\n            .fade(duration: 1)\n            .loadDiskFileSynchronously()\n            .onProgress { (received, total) in print(\"\\(indexPath.row + 1): \\(received)/\\(total)\") }\n            .onSuccess { print($0) }\n            .onFailure { err in print(\"Error: \\(err)\") }\n            .set(to: imageView)\n    }\n    \n    override func collectionView(\n        _ collectionView: UICollectionView,\n        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell\n    {\n        let cell = collectionView.dequeueReusableCell(\n            withReuseIdentifier: \"collectionViewCell\",\n            for: indexPath) as! ImageCollectionViewCell\n        cell.cellImageView.kf.indicatorType = .activity\n        return cell\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/OrientationImagesViewController.swift",
    "content": "//\n//  OrientationImagesViewController.swift\n//  Kingfisher-Demo\n//\n//  Created by Wei Wang on 2021/05/09.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nclass OrientationImagesViewController: UICollectionViewController {\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"EXIF\"\n        setupOperationNavigationBar()\n    }\n}\n\nextension OrientationImagesViewController {\n    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {\n        return ImageLoader.orientationImageURLs.count\n    }\n    \n    override func collectionView(\n        _ collectionView: UICollectionView,\n        didEndDisplaying cell: UICollectionViewCell,\n        forItemAt indexPath: IndexPath)\n    {\n        // This will cancel all unfinished downloading task when the cell disappearing.\n        (cell as! ImageCollectionViewCell).cellImageView.kf.cancelDownloadTask()\n    }\n    \n    override func collectionView(\n        _ collectionView: UICollectionView,\n        willDisplay cell: UICollectionViewCell,\n        forItemAt indexPath: IndexPath)\n    {\n        let imageView = (cell as! ImageCollectionViewCell).cellImageView!\n        let url = ImageLoader.orientationImageURLs[indexPath.row]\n        KF.url(url)\n            .fade(duration: 1)\n            .backgroundDecode([0, 1].randomElement() == 0)\n            .loadDiskFileSynchronously()\n            .onProgress { (received, total) in print(\"\\(indexPath.row + 1): \\(received)/\\(total)\") }\n            .onSuccess { print($0) }\n            .onFailure { err in print(\"Error: \\(err)\") }\n            .set(to: imageView)\n    }\n    \n    override func collectionView(\n        _ collectionView: UICollectionView,\n        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell\n    {\n        let cell = collectionView.dequeueReusableCell(\n            withReuseIdentifier: \"collectionViewCell\",\n            for: indexPath) as! ImageCollectionViewCell\n        cell.cellImageView.kf.indicatorType = .activity\n        return cell\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/PHPickerResultViewController.swift",
    "content": "//\n//  PHPickerResultViewController.swift\n//  Kingfisher\n//\n//  Created by nuomi1 on 2024-04-17.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport Kingfisher\nimport PhotosUI\nimport UIKit\n\nclass PHPickerResultViewController: UIViewController {\n    @IBOutlet var imageView: UIImageView!\n\n    @IBAction func onTapButton() {\n        if #available(iOS 14.0, *) {\n            presentPickerViewController()\n        } else {\n            presentAlertController()\n        }\n    }\n\n    private func presentAlertController() {\n        let cancelAction = UIAlertAction(title: \"Cancel\", style: .cancel)\n        let alertController = UIAlertController(title: \"Warning!\", message: \"Only supports iOS 14+\", preferredStyle: .alert)\n        alertController.addAction(cancelAction)\n        present(alertController, animated: true)\n    }\n\n    @available(iOS 14.0, *)\n    private func presentPickerViewController() {\n        var configuration = PHPickerConfiguration(photoLibrary: .shared())\n        configuration.filter = .images\n        configuration.selectionLimit = 1\n        let viewController = PHPickerViewController(configuration: configuration)\n        viewController.delegate = self\n        present(viewController, animated: true)\n    }\n}\n\n@available(iOS 14, *)\nextension PHPickerResultViewController: PHPickerViewControllerDelegate {\n    public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {\n        picker.dismiss(animated: true)\n        guard let result = results.first else { return }\n        let provider = PHPickerResultImageDataProvider(pickerResult: result)\n        imageView.kf.setImage(with: .provider(provider))\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/ProcessorCollectionViewController.swift",
    "content": "//\n//  ProcessorCollectionViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/19.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nprivate let reuseIdentifier = \"ProcessorCell\"\n\nclass ProcessorCollectionViewController: UICollectionViewController {\n\n    var currentProcessor: any ImageProcessor = DefaultImageProcessor.default {\n        didSet {\n            collectionView.reloadData()\n        }\n    }\n    \n    var processors: [(any ImageProcessor, String)] = [\n        (DefaultImageProcessor.default, \"Default\"),\n        (ResizingImageProcessor(referenceSize: CGSize(width: 50, height: 50)), \"Resizing\"),\n        (RoundCornerImageProcessor(radius: .point(20)), \"Round Corner\"),\n        (RoundCornerImageProcessor(radius: .widthFraction(0.5), roundingCorners: [.topLeft, .bottomRight]), \"Round Corner Partial\"),\n        (BorderImageProcessor(border: .init(color: .systemBlue, lineWidth: 8)), \"Border\"),\n        (RoundCornerImageProcessor(radius: .widthFraction(0.2)) |> BorderImageProcessor(border: .init(color: UIColor.systemBlue.withAlphaComponent(0.7), lineWidth: 12, radius: .widthFraction(0.2))), \"Round Border\"),\n        (BlendImageProcessor(blendMode: .lighten, alpha: 1.0, backgroundColor: .red), \"Blend\"),\n        (BlurImageProcessor(blurRadius: 5), \"Blur\"),\n        (OverlayImageProcessor(overlay: .red, fraction: 0.5), \"Overlay\"),\n        (TintImageProcessor(tint: UIColor.red.withAlphaComponent(0.5)), \"Tint\"),\n        (ColorControlsProcessor(brightness: 0.0, contrast: 1.1, saturation: 1.1, inputEV: 1.0), \"Vibrancy\"),\n        (BlackWhiteProcessor(), \"B&W\"),\n        (CroppingImageProcessor(size: CGSize(width: 100, height: 100)), \"Cropping\"),\n        (DownsamplingImageProcessor(size: CGSize(width: 25, height: 25)), \"Downsampling\"),\n        (BlurImageProcessor(blurRadius: 5) |> RoundCornerImageProcessor(cornerRadius: 20), \"Blur + Round Corner\")\n    ]\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"Processor\"\n        setupOperationNavigationBar()\n    }\n\n    override func numberOfSections(in collectionView: UICollectionView) -> Int {\n        return 1\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {\n        return ImageLoader.sampleImageURLs.count\n    }\n\n    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {\n        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell\n        let url = ImageLoader.sampleImageURLs[indexPath.row]\n\n        KF.url(url)\n            .setProcessor(currentProcessor)\n            .serialize(as: .PNG)\n            .onSuccess { print($0) }\n            .onFailure { print($0) }\n            .set(to: cell.cellImageView)\n\n        return cell\n    }\n    \n    override func alertPopup(_ sender: Any) -> UIAlertController {\n        let alert = super.alertPopup(sender)\n        alert.addAction(UIAlertAction(title: \"Processor\", style: .default, handler: { _ in\n            let alert = UIAlertController(title: \"Processor\", message: nil, preferredStyle: .actionSheet)\n            for item in self.processors {\n                alert.addAction(UIAlertAction(title: item.1, style: .default) { _ in self.currentProcessor = item.0 })\n            }\n            alert.addAction(UIAlertAction(title: \"Cancel\", style: .cancel))\n            alert.popoverPresentationController?.barButtonItem = sender as? UIBarButtonItem\n            self.present(alert, animated: true)\n        }))\n        return alert\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/ProgressiveJPEGViewController.swift",
    "content": "//\n//  ProgressiveJPEGViewController.swift\n//  Kingfisher\n//\n//  Created by lixiang on 2019/5/12.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nclass ProgressiveJPEGViewController: UIViewController {\n\n    @IBOutlet weak var imageView: UIImageView!\n    @IBOutlet weak var progressLabel: UILabel!\n    \n    private var isBlur = true\n    private var isFastestScan = true\n    \n    private let processor = RoundCornerImageProcessor(cornerRadius: 30)\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"Progressive JPEG\"\n        setupOperationNavigationBar()\n        loadImage()\n    }\n    \n    private func loadImage() {\n        progressLabel.text = \"- / -\"\n        \n        let progressive = ImageProgressive(\n            isBlur: isBlur,\n            isFastestScan: isFastestScan,\n            scanInterval: 0.1\n        )\n\n        KF.url(ImageLoader.progressiveImageURL)\n            .loadDiskFileSynchronously()\n            .progressiveJPEG(progressive)\n            .roundCorner(radius: .point(30))\n            .onProgress { receivedSize, totalSize in\n                print(\"\\(receivedSize)/\\(totalSize)\")\n                self.progressLabel.text = \"\\(receivedSize) / \\(totalSize)\"\n            }\n            .onSuccess { result in\n                print(result)\n                print(\"Finished\")\n            }\n            .onFailure { error in\n                print(error)\n                self.progressLabel.text = error.localizedDescription\n            }\n            .set(to: imageView)\n    }\n    \n    override func alertPopup(_ sender: Any) -> UIAlertController {\n        let alert = super.alertPopup(sender)\n        \n        func reloadImage() {\n            // Cancel\n            imageView.kf.cancelDownloadTask()\n            // Clean cache\n            KingfisherManager.shared.cache.removeImage(\n                forKey: ImageLoader.progressiveImageURL.cacheKey,\n                processorIdentifier: self.processor.identifier,\n                callbackQueue: .mainAsync,\n                completionHandler: {\n                    Task { @MainActor in self.loadImage() }\n                }\n            )\n        }\n        \n        do {\n            let title = isBlur ? \"Disable Blur\" : \"Enable Blur\"\n            alert.addAction(UIAlertAction(title: title, style: .default) { _ in\n                self.isBlur.toggle()\n                reloadImage()\n            })\n        }\n        \n        do {\n            let title = isFastestScan ? \"Disable Fastest Scan\" : \"Enable Fastest Scan\"\n            alert.addAction(UIAlertAction(title: title, style: .default) { _ in\n                self.isFastestScan.toggle()\n                reloadImage()\n            })\n        }\n        return alert\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/SwiftUIViewController.swift",
    "content": "//\n//  SwiftUIViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/12/16.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport UIKit\n\n@available(iOS 14.0, *)\nclass SwiftUIViewController: UIHostingController<MainView> {\n    required init?(coder: NSCoder) {\n        super.init(coder: coder, rootView: MainView())\n    }\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/TextAttachmentViewController.swift",
    "content": "//\n//  TextAttachmentViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/08/07.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nclass TextAttachmentViewController: UIViewController {\n    @IBOutlet weak var label: UILabel!\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        title = \"Text Attachment\"\n        setupOperationNavigationBar()\n\n        loadAttributedText()\n    }\n\n    private func loadAttributedText() {\n        let attributedText = NSMutableAttributedString(string: \"Hello World\")\n\n        let textAttachment = NSTextAttachment()\n        attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment))\n        label.attributedText = attributedText\n\n        let label = getLabel()\n        KF.url(URL(string: \"https://onevcat.com/assets/images/avatar.jpg\")!)\n            .resizing(referenceSize: CGSize(width: 30, height: 30))\n            .roundCorner(radius: .point(15))\n            .set(to: textAttachment, attributedView: label)\n    }\n    \n    func getLabel() -> UILabel {\n        return label\n    }\n}\n\nextension TextAttachmentViewController: MainDataViewReloadable {\n    func reload() {\n        label.attributedText = NSAttributedString(string: \"-\")\n        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {\n            self.loadAttributedText()\n        }\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-Demo/ViewControllers/TransitionViewController.swift",
    "content": "//\n//  TransitionViewController.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/18.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\nclass TransitionViewController: UIViewController {\n    \n    enum PickerComponent: Int, CaseIterable {\n        case transitionType\n        case duration\n    }\n    \n    @IBOutlet weak var imageView: UIImageView!\n    @IBOutlet weak var transitionPickerView: UIPickerView!\n    \n    let durations: [TimeInterval] = [0.5, 1, 2, 4, 10]\n    let transitions: [String] = [\"none\", \"fade\", \"flip - left\", \"flip - right\", \"flip - top\", \"flip - bottom\"]\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = \"Transition\"\n        setupOperationNavigationBar()\n        imageView.kf.indicatorType = .activity\n    }\n    \n    func makeTransition(type: String, duration: TimeInterval) -> ImageTransition {\n        switch type {\n        case \"none\": return .none\n        case \"fade\": return .fade(duration)\n        case \"flip - left\": return .flipFromLeft(duration)\n        case \"flip - right\": return .flipFromRight(duration)\n        case \"flip - top\": return .flipFromTop(duration)\n        case \"flip - bottom\": return .flipFromBottom(duration)\n        default: return .none\n        }\n    }\n    \n    func reloadImageView() {\n    \n        let typeIndex = transitionPickerView.selectedRow(inComponent: PickerComponent.transitionType.rawValue)\n        let transitionType = transitions[typeIndex]\n        \n        let durationIndex = transitionPickerView.selectedRow(inComponent: PickerComponent.duration.rawValue)\n        let duration = durations[durationIndex]\n        \n        let t = makeTransition(type: transitionType, duration: duration)\n        let url = ImageLoader.sampleImageURLs[0]\n        KF.url(url)\n            .forceTransition()\n            .transition(t)\n            .set(to: imageView)\n    }\n}\n\nextension TransitionViewController: UIPickerViewDelegate {\n    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {\n        switch PickerComponent(rawValue: component)!  {\n        case .transitionType: return transitions[row]\n        case .duration: return String(durations[row])\n        }\n    }\n    \n    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {\n        reloadImageView()\n    }\n}\n\nextension TransitionViewController: UIPickerViewDataSource {\n    func numberOfComponents(in pickerView: UIPickerView) -> Int {\n        return PickerComponent.allCases.count\n    }\n    \n    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {\n        switch PickerComponent(rawValue: component)  {\n        case .transitionType: return transitions.count\n        case .duration: return durations.count\n        default: return 0\n        }\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-macOS-Demo/AppDelegate.swift",
    "content": "//\n//  AppDelegate.swift\n//  Kingfisher-macOS-Demo\n//\n//  Created by Wei Wang on 16/1/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Cocoa\nimport Kingfisher\n\n@NSApplicationMain\nclass AppDelegate: NSObject, NSApplicationDelegate {\n\n    func applicationDidFinishLaunching(aNotification: NSNotification) {\n        // Insert code here to initialize your application\n    }\n\n    func applicationWillTerminate(aNotification: NSNotification) {\n        // Insert code here to tear down your application\n    }\n\n\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-macOS-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"16x16\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"16x16\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"32x32\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"32x32\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"128x128\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"128x128\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"256x256\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"256x256\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"512x512\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"size\" : \"512x512\",\n      \"scale\" : \"2x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"23727\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" initialViewController=\"B8D-0N-5wS\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"23727\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--Application-->\n        <scene sceneID=\"JPo-4y-FX3\">\n            <objects>\n                <application id=\"hnw-xV-0zn\" sceneMemberID=\"viewController\">\n                    <menu key=\"mainMenu\" title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n                        <items>\n                            <menuItem title=\"Kingfisher-macOS-Demo\" id=\"1Xt-HY-uBw\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Kingfisher-macOS-Demo\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                                    <items>\n                                        <menuItem title=\"About Kingfisher-macOS-Demo\" id=\"5kV-Vb-QxS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontStandardAboutPanel:\" target=\"Ady-hI-5gd\" id=\"Exp-CZ-Vem\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                                        <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                                        <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                                        <menuItem title=\"Hide Kingfisher-macOS-Demo\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                            <connections>\n                                                <action selector=\"hide:\" target=\"Ady-hI-5gd\" id=\"PnN-Uc-m68\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"hideOtherApplications:\" target=\"Ady-hI-5gd\" id=\"VT4-aY-XCT\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"unhideAllApplications:\" target=\"Ady-hI-5gd\" id=\"Dhg-Le-xox\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                                        <menuItem title=\"Quit Kingfisher-macOS-Demo\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                            <connections>\n                                                <action selector=\"terminate:\" target=\"Ady-hI-5gd\" id=\"Te7-pn-YzF\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"File\" id=\"dMs-cI-mzQ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"File\" id=\"bib-Uj-vzu\">\n                                    <items>\n                                        <menuItem title=\"New\" keyEquivalent=\"n\" id=\"Was-JA-tGl\">\n                                            <connections>\n                                                <action selector=\"newDocument:\" target=\"Ady-hI-5gd\" id=\"4Si-XN-c54\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Open…\" keyEquivalent=\"o\" id=\"IAo-SY-fd9\">\n                                            <connections>\n                                                <action selector=\"openDocument:\" target=\"Ady-hI-5gd\" id=\"bVn-NM-KNZ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Open Recent\" id=\"tXI-mr-wws\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Open Recent\" systemMenu=\"recentDocuments\" id=\"oas-Oc-fiZ\">\n                                                <items>\n                                                    <menuItem title=\"Clear Menu\" id=\"vNY-rz-j42\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"clearRecentDocuments:\" target=\"Ady-hI-5gd\" id=\"Daa-9d-B3U\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                </items>\n                                            </menu>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"m54-Is-iLE\"/>\n                                        <menuItem title=\"Close\" keyEquivalent=\"w\" id=\"DVo-aG-piG\">\n                                            <connections>\n                                                <action selector=\"performClose:\" target=\"Ady-hI-5gd\" id=\"HmO-Ls-i7Q\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Save…\" keyEquivalent=\"s\" id=\"pxx-59-PXV\">\n                                            <connections>\n                                                <action selector=\"saveDocument:\" target=\"Ady-hI-5gd\" id=\"teZ-XB-qJY\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Save As…\" keyEquivalent=\"S\" id=\"Bw7-FT-i3A\">\n                                            <connections>\n                                                <action selector=\"saveDocumentAs:\" target=\"Ady-hI-5gd\" id=\"mDf-zr-I0C\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Revert to Saved\" id=\"KaW-ft-85H\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"revertDocumentToSaved:\" target=\"Ady-hI-5gd\" id=\"iJ3-Pv-kwq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"aJh-i4-bef\"/>\n                                        <menuItem title=\"Page Setup…\" keyEquivalent=\"P\" id=\"qIS-W8-SiK\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" shift=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"runPageLayout:\" target=\"Ady-hI-5gd\" id=\"Din-rz-gC5\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Print…\" keyEquivalent=\"p\" id=\"aTl-1u-JFS\">\n                                            <connections>\n                                                <action selector=\"print:\" target=\"Ady-hI-5gd\" id=\"qaZ-4w-aoO\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                                    <items>\n                                        <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                            <connections>\n                                                <action selector=\"undo:\" target=\"Ady-hI-5gd\" id=\"M6e-cu-g7V\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                            <connections>\n                                                <action selector=\"redo:\" target=\"Ady-hI-5gd\" id=\"oIA-Rs-6OD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                                        <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                            <connections>\n                                                <action selector=\"cut:\" target=\"Ady-hI-5gd\" id=\"YJe-68-I9s\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                            <connections>\n                                                <action selector=\"copy:\" target=\"Ady-hI-5gd\" id=\"G1f-GL-Joy\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                            <connections>\n                                                <action selector=\"paste:\" target=\"Ady-hI-5gd\" id=\"UvS-8e-Qdg\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"pasteAsPlainText:\" target=\"Ady-hI-5gd\" id=\"cEh-KX-wJQ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"delete:\" target=\"Ady-hI-5gd\" id=\"0Mk-Ml-PaM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                            <connections>\n                                                <action selector=\"selectAll:\" target=\"Ady-hI-5gd\" id=\"VNm-Mi-diN\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                                        <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                                <items>\n                                                    <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                                        <connections>\n                                                            <action selector=\"performFindPanelAction:\" target=\"Ady-hI-5gd\" id=\"cD7-Qs-BN4\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                                        <connections>\n                                                            <action selector=\"performFindPanelAction:\" target=\"Ady-hI-5gd\" id=\"WD3-Gg-5AJ\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                                        <connections>\n                                                            <action selector=\"performFindPanelAction:\" target=\"Ady-hI-5gd\" id=\"NDo-RZ-v9R\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                                        <connections>\n                                                            <action selector=\"performFindPanelAction:\" target=\"Ady-hI-5gd\" id=\"HOh-sY-3ay\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                                        <connections>\n                                                            <action selector=\"performFindPanelAction:\" target=\"Ady-hI-5gd\" id=\"U76-nv-p5D\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                                        <connections>\n                                                            <action selector=\"centerSelectionInVisibleArea:\" target=\"Ady-hI-5gd\" id=\"IOG-6D-g5B\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                </items>\n                                            </menu>\n                                        </menuItem>\n                                        <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                                <items>\n                                                    <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                                        <connections>\n                                                            <action selector=\"showGuessPanel:\" target=\"Ady-hI-5gd\" id=\"vFj-Ks-hy3\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                                        <connections>\n                                                            <action selector=\"checkSpelling:\" target=\"Ady-hI-5gd\" id=\"fz7-VC-reM\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                                    <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleContinuousSpellChecking:\" target=\"Ady-hI-5gd\" id=\"7w6-Qz-0kB\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleGrammarChecking:\" target=\"Ady-hI-5gd\" id=\"muD-Qn-j4w\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"Ady-hI-5gd\" id=\"2lM-Qi-WAP\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                </items>\n                                            </menu>\n                                        </menuItem>\n                                        <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                                <items>\n                                                    <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"orderFrontSubstitutionsPanel:\" target=\"Ady-hI-5gd\" id=\"oku-mr-iSq\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                                    <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleSmartInsertDelete:\" target=\"Ady-hI-5gd\" id=\"3IJ-Se-DZD\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"Ady-hI-5gd\" id=\"ptq-xd-QOA\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleAutomaticDashSubstitution:\" target=\"Ady-hI-5gd\" id=\"oCt-pO-9gS\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleAutomaticLinkDetection:\" target=\"Ady-hI-5gd\" id=\"Gip-E3-Fov\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleAutomaticDataDetection:\" target=\"Ady-hI-5gd\" id=\"R1I-Nq-Kbl\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleAutomaticTextReplacement:\" target=\"Ady-hI-5gd\" id=\"DvP-Fe-Py6\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                </items>\n                                            </menu>\n                                        </menuItem>\n                                        <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                                <items>\n                                                    <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"uppercaseWord:\" target=\"Ady-hI-5gd\" id=\"sPh-Tk-edu\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"lowercaseWord:\" target=\"Ady-hI-5gd\" id=\"iUZ-b5-hil\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"capitalizeWord:\" target=\"Ady-hI-5gd\" id=\"26H-TL-nsh\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                </items>\n                                            </menu>\n                                        </menuItem>\n                                        <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                                <items>\n                                                    <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"startSpeaking:\" target=\"Ady-hI-5gd\" id=\"654-Ng-kyl\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"stopSpeaking:\" target=\"Ady-hI-5gd\" id=\"dX8-6p-jy9\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                </items>\n                                            </menu>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Format\" id=\"jxT-CU-nIS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Format\" id=\"GEO-Iw-cKr\">\n                                    <items>\n                                        <menuItem title=\"Font\" id=\"Gi5-1S-RQB\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Font\" systemMenu=\"font\" id=\"aXa-aM-Jaq\">\n                                                <items>\n                                                    <menuItem title=\"Show Fonts\" keyEquivalent=\"t\" id=\"Q5e-8K-NDq\"/>\n                                                    <menuItem title=\"Bold\" tag=\"2\" keyEquivalent=\"b\" id=\"GB9-OM-e27\"/>\n                                                    <menuItem title=\"Italic\" tag=\"1\" keyEquivalent=\"i\" id=\"Vjx-xi-njq\"/>\n                                                    <menuItem title=\"Underline\" keyEquivalent=\"u\" id=\"WRG-CD-K1S\">\n                                                        <connections>\n                                                            <action selector=\"underline:\" target=\"Ady-hI-5gd\" id=\"FYS-2b-JAY\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem isSeparatorItem=\"YES\" id=\"5gT-KC-WSO\"/>\n                                                    <menuItem title=\"Bigger\" tag=\"3\" keyEquivalent=\"+\" id=\"Ptp-SP-VEL\"/>\n                                                    <menuItem title=\"Smaller\" tag=\"4\" keyEquivalent=\"-\" id=\"i1d-Er-qST\"/>\n                                                    <menuItem isSeparatorItem=\"YES\" id=\"kx3-Dk-x3B\"/>\n                                                    <menuItem title=\"Kern\" id=\"jBQ-r6-VK2\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <menu key=\"submenu\" title=\"Kern\" id=\"tlD-Oa-oAM\">\n                                                            <items>\n                                                                <menuItem title=\"Use Default\" id=\"GUa-eO-cwY\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"useStandardKerning:\" target=\"Ady-hI-5gd\" id=\"6dk-9l-Ckg\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Use None\" id=\"cDB-IK-hbR\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"turnOffKerning:\" target=\"Ady-hI-5gd\" id=\"U8a-gz-Maa\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Tighten\" id=\"46P-cB-AYj\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"tightenKerning:\" target=\"Ady-hI-5gd\" id=\"hr7-Nz-8ro\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Loosen\" id=\"ogc-rX-tC1\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"loosenKerning:\" target=\"Ady-hI-5gd\" id=\"8i4-f9-FKE\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                            </items>\n                                                        </menu>\n                                                    </menuItem>\n                                                    <menuItem title=\"Ligatures\" id=\"o6e-r0-MWq\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <menu key=\"submenu\" title=\"Ligatures\" id=\"w0m-vy-SC9\">\n                                                            <items>\n                                                                <menuItem title=\"Use Default\" id=\"agt-UL-0e3\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"useStandardLigatures:\" target=\"Ady-hI-5gd\" id=\"7uR-wd-Dx6\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Use None\" id=\"J7y-lM-qPV\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"turnOffLigatures:\" target=\"Ady-hI-5gd\" id=\"iX2-gA-Ilz\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Use All\" id=\"xQD-1f-W4t\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"useAllLigatures:\" target=\"Ady-hI-5gd\" id=\"KcB-kA-TuK\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                            </items>\n                                                        </menu>\n                                                    </menuItem>\n                                                    <menuItem title=\"Baseline\" id=\"OaQ-X3-Vso\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <menu key=\"submenu\" title=\"Baseline\" id=\"ijk-EB-dga\">\n                                                            <items>\n                                                                <menuItem title=\"Use Default\" id=\"3Om-Ey-2VK\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"unscript:\" target=\"Ady-hI-5gd\" id=\"0vZ-95-Ywn\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Superscript\" id=\"Rqc-34-cIF\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"superscript:\" target=\"Ady-hI-5gd\" id=\"3qV-fo-wpU\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Subscript\" id=\"I0S-gh-46l\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"subscript:\" target=\"Ady-hI-5gd\" id=\"Q6W-4W-IGz\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Raise\" id=\"2h7-ER-AoG\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"raiseBaseline:\" target=\"Ady-hI-5gd\" id=\"4sk-31-7Q9\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem title=\"Lower\" id=\"1tx-W0-xDw\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"lowerBaseline:\" target=\"Ady-hI-5gd\" id=\"OF1-bc-KW4\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                            </items>\n                                                        </menu>\n                                                    </menuItem>\n                                                    <menuItem isSeparatorItem=\"YES\" id=\"Ndw-q3-faq\"/>\n                                                    <menuItem title=\"Show Colors\" keyEquivalent=\"C\" id=\"bgn-CT-cEk\">\n                                                        <connections>\n                                                            <action selector=\"orderFrontColorPanel:\" target=\"Ady-hI-5gd\" id=\"mSX-Xz-DV3\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem isSeparatorItem=\"YES\" id=\"iMs-zA-UFJ\"/>\n                                                    <menuItem title=\"Copy Style\" keyEquivalent=\"c\" id=\"5Vv-lz-BsD\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                                        <connections>\n                                                            <action selector=\"copyFont:\" target=\"Ady-hI-5gd\" id=\"GJO-xA-L4q\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Paste Style\" keyEquivalent=\"v\" id=\"vKC-jM-MkH\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                                        <connections>\n                                                            <action selector=\"pasteFont:\" target=\"Ady-hI-5gd\" id=\"JfD-CL-leO\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                </items>\n                                            </menu>\n                                        </menuItem>\n                                        <menuItem title=\"Text\" id=\"Fal-I4-PZk\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <menu key=\"submenu\" title=\"Text\" id=\"d9c-me-L2H\">\n                                                <items>\n                                                    <menuItem title=\"Align Left\" keyEquivalent=\"{\" id=\"ZM1-6Q-yy1\">\n                                                        <connections>\n                                                            <action selector=\"alignLeft:\" target=\"Ady-hI-5gd\" id=\"zUv-R1-uAa\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Center\" keyEquivalent=\"|\" id=\"VIY-Ag-zcb\">\n                                                        <connections>\n                                                            <action selector=\"alignCenter:\" target=\"Ady-hI-5gd\" id=\"spX-mk-kcS\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Justify\" id=\"J5U-5w-g23\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"alignJustified:\" target=\"Ady-hI-5gd\" id=\"ljL-7U-jND\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Align Right\" keyEquivalent=\"}\" id=\"wb2-vD-lq4\">\n                                                        <connections>\n                                                            <action selector=\"alignRight:\" target=\"Ady-hI-5gd\" id=\"r48-bG-YeY\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem isSeparatorItem=\"YES\" id=\"4s2-GY-VfK\"/>\n                                                    <menuItem title=\"Writing Direction\" id=\"H1b-Si-o9J\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <menu key=\"submenu\" title=\"Writing Direction\" id=\"8mr-sm-Yjd\">\n                                                            <items>\n                                                                <menuItem title=\"Paragraph\" enabled=\"NO\" id=\"ZvO-Gk-QUH\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                </menuItem>\n                                                                <menuItem id=\"YGs-j5-SAR\">\n                                                                    <string key=\"title\">\tDefault</string>\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"makeBaseWritingDirectionNatural:\" target=\"Ady-hI-5gd\" id=\"qtV-5e-UBP\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem id=\"Lbh-J2-qVU\">\n                                                                    <string key=\"title\">\tLeft to Right</string>\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"makeBaseWritingDirectionLeftToRight:\" target=\"Ady-hI-5gd\" id=\"S0X-9S-QSf\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem id=\"jFq-tB-4Kx\">\n                                                                    <string key=\"title\">\tRight to Left</string>\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"makeBaseWritingDirectionRightToLeft:\" target=\"Ady-hI-5gd\" id=\"5fk-qB-AqJ\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem isSeparatorItem=\"YES\" id=\"swp-gr-a21\"/>\n                                                                <menuItem title=\"Selection\" enabled=\"NO\" id=\"cqv-fj-IhA\">\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                </menuItem>\n                                                                <menuItem id=\"Nop-cj-93Q\">\n                                                                    <string key=\"title\">\tDefault</string>\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"makeTextWritingDirectionNatural:\" target=\"Ady-hI-5gd\" id=\"lPI-Se-ZHp\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem id=\"BgM-ve-c93\">\n                                                                    <string key=\"title\">\tLeft to Right</string>\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"makeTextWritingDirectionLeftToRight:\" target=\"Ady-hI-5gd\" id=\"caW-Bv-w94\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                                <menuItem id=\"RB4-Sm-HuC\">\n                                                                    <string key=\"title\">\tRight to Left</string>\n                                                                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                                    <connections>\n                                                                        <action selector=\"makeTextWritingDirectionRightToLeft:\" target=\"Ady-hI-5gd\" id=\"EXD-6r-ZUu\"/>\n                                                                    </connections>\n                                                                </menuItem>\n                                                            </items>\n                                                        </menu>\n                                                    </menuItem>\n                                                    <menuItem isSeparatorItem=\"YES\" id=\"fKy-g9-1gm\"/>\n                                                    <menuItem title=\"Show Ruler\" id=\"vLm-3I-IUL\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                                        <connections>\n                                                            <action selector=\"toggleRuler:\" target=\"Ady-hI-5gd\" id=\"FOx-HJ-KwY\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Copy Ruler\" keyEquivalent=\"c\" id=\"MkV-Pr-PK5\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                                        <connections>\n                                                            <action selector=\"copyRuler:\" target=\"Ady-hI-5gd\" id=\"71i-fW-3W2\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                    <menuItem title=\"Paste Ruler\" keyEquivalent=\"v\" id=\"LVM-kO-fVI\">\n                                                        <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                                        <connections>\n                                                            <action selector=\"pasteRuler:\" target=\"Ady-hI-5gd\" id=\"cSh-wd-qM2\"/>\n                                                        </connections>\n                                                    </menuItem>\n                                                </items>\n                                            </menu>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                                    <items>\n                                        <menuItem title=\"Show Toolbar\" keyEquivalent=\"t\" id=\"snW-S8-Cw5\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"toggleToolbarShown:\" target=\"Ady-hI-5gd\" id=\"BXY-wc-z0C\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Customize Toolbar…\" id=\"1UK-8n-QPP\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"runToolbarCustomizationPalette:\" target=\"Ady-hI-5gd\" id=\"pQI-g3-MTW\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                                    <items>\n                                        <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                            <connections>\n                                                <action selector=\"performMiniaturize:\" target=\"Ady-hI-5gd\" id=\"VwT-WD-YPe\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"performZoom:\" target=\"Ady-hI-5gd\" id=\"DIl-cC-cCs\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                                        <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"arrangeInFront:\" target=\"Ady-hI-5gd\" id=\"DRN-fu-gQh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Help\" id=\"wpr-3q-Mcd\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"F2S-fz-NVQ\">\n                                    <items>\n                                        <menuItem title=\"Kingfisher-macOS-Demo Help\" keyEquivalent=\"?\" id=\"FKE-Sm-Kum\">\n                                            <connections>\n                                                <action selector=\"showHelp:\" target=\"Ady-hI-5gd\" id=\"y7X-2Q-9no\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                    <connections>\n                        <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"PrD-fu-P6m\"/>\n                    </connections>\n                </application>\n                <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Kingfisher_macOS_Demo\" customModuleProvider=\"target\"/>\n                <customObject id=\"Ady-hI-5gd\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"75\" y=\"0.0\"/>\n        </scene>\n        <!--Window Controller-->\n        <scene sceneID=\"R2V-B0-nI4\">\n            <objects>\n                <windowController id=\"B8D-0N-5wS\" sceneMemberID=\"viewController\">\n                    <window key=\"window\" title=\"Kingfisher\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" visibleAtLaunch=\"NO\" animationBehavior=\"default\" id=\"IQv-IB-iLA\">\n                        <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n                        <windowPositionMask key=\"initialPositionMask\" leftStrut=\"YES\" rightStrut=\"YES\" topStrut=\"YES\" bottomStrut=\"YES\"/>\n                        <rect key=\"contentRect\" x=\"196\" y=\"240\" width=\"620\" height=\"480\"/>\n                        <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"1680\" height=\"1027\"/>\n                        <value key=\"minSize\" type=\"size\" width=\"350\" height=\"300\"/>\n                        <connections>\n                            <outlet property=\"delegate\" destination=\"B8D-0N-5wS\" id=\"aO1-Z5-oNW\"/>\n                        </connections>\n                    </window>\n                    <connections>\n                        <segue destination=\"XfG-lQ-9wD\" kind=\"relationship\" relationship=\"window.shadowedContentViewController\" id=\"cq2-FE-JQM\"/>\n                    </connections>\n                </windowController>\n                <customObject id=\"Oky-zY-oP4\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"75\" y=\"250\"/>\n        </scene>\n        <!--View Controller-->\n        <scene sceneID=\"hIz-AP-VOD\">\n            <objects>\n                <viewController id=\"XfG-lQ-9wD\" customClass=\"ViewController\" customModule=\"Kingfisher_macOS_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" id=\"m2S-Jp-Qdl\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"620\" height=\"480\"/>\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                        <subviews>\n                            <scrollView wantsLayer=\"YES\" autohidesScrollers=\"YES\" horizontalLineScroll=\"10\" horizontalPageScroll=\"10\" verticalLineScroll=\"10\" verticalPageScroll=\"10\" usesPredominantAxisScrolling=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"MlO-xV-qug\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"620\" height=\"450\"/>\n                                <clipView key=\"contentView\" id=\"rM7-g8-q6C\">\n                                    <rect key=\"frame\" x=\"1\" y=\"1\" width=\"618\" height=\"448\"/>\n                                    <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                    <subviews>\n                                        <collectionView id=\"XnK-6H-mn7\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"618\" height=\"448\"/>\n                                            <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\"/>\n                                            <collectionViewFlowLayout key=\"collectionViewLayout\" minimumInteritemSpacing=\"10\" minimumLineSpacing=\"10\" id=\"DFD-7H-C8z\">\n                                                <size key=\"itemSize\" width=\"250\" height=\"250\"/>\n                                                <edgeInsets key=\"sectionInset\" left=\"40\" right=\"40\" top=\"30\" bottom=\"30\"/>\n                                            </collectionViewFlowLayout>\n                                            <color key=\"primaryBackgroundColor\" name=\"controlBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                            <connections>\n                                                <outlet property=\"dataSource\" destination=\"XfG-lQ-9wD\" id=\"IZP-op-kac\"/>\n                                            </connections>\n                                        </collectionView>\n                                    </subviews>\n                                </clipView>\n                                <scroller key=\"horizontalScroller\" hidden=\"YES\" wantsLayer=\"YES\" verticalHuggingPriority=\"750\" horizontal=\"YES\" id=\"9aw-c6-93q\">\n                                    <rect key=\"frame\" x=\"1\" y=\"144\" width=\"233\" height=\"15\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                </scroller>\n                                <scroller key=\"verticalScroller\" hidden=\"YES\" wantsLayer=\"YES\" verticalHuggingPriority=\"750\" doubleValue=\"1\" horizontal=\"NO\" id=\"b9u-Ar-YcK\">\n                                    <rect key=\"frame\" x=\"234\" y=\"1\" width=\"15\" height=\"143\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                </scroller>\n                            </scrollView>\n                            <button verticalHuggingPriority=\"750\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"yIr-uo-Quc\">\n                                <rect key=\"frame\" x=\"13\" y=\"448\" width=\"108\" height=\"32\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"Clear Cache\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"ebf-qp-Vwt\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                                <connections>\n                                    <action selector=\"clearCachePressedWithSender:\" target=\"XfG-lQ-9wD\" id=\"lfl-RU-nX5\"/>\n                                </connections>\n                            </button>\n                            <button verticalHuggingPriority=\"750\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"eLA-Ce-crP\" userLabel=\"Heavy GIFs\">\n                                <rect key=\"frame\" x=\"259\" y=\"448\" width=\"103\" height=\"32\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"Heavy GIFs\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"YYz-yg-1MB\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                                <connections>\n                                    <segue destination=\"19n-lE-hND\" kind=\"show\" id=\"NZO-24-0LI\"/>\n                                </connections>\n                            </button>\n                            <button verticalHuggingPriority=\"750\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Ejh-qu-qmy\">\n                                <rect key=\"frame\" x=\"531\" y=\"448\" width=\"76\" height=\"32\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"Reload\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"DhD-Tg-Bw3\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                                <connections>\n                                    <action selector=\"reloadPressedWithSender:\" target=\"XfG-lQ-9wD\" id=\"k24-Wi-NRd\"/>\n                                </connections>\n                            </button>\n                            <button verticalHuggingPriority=\"750\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"0Du-le-fYN\">\n                                <rect key=\"frame\" x=\"417\" y=\"448\" width=\"78\" height=\"32\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"SwiftUI\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"wIi-ia-bgi\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                                <connections>\n                                    <segue destination=\"ei1-kq-tvV\" kind=\"show\" id=\"Ub0-4z-BqC\"/>\n                                </connections>\n                            </button>\n                        </subviews>\n                        <constraints>\n                            <constraint firstAttribute=\"trailing\" secondItem=\"MlO-xV-qug\" secondAttribute=\"trailing\" id=\"18w-Qc-Jr6\"/>\n                            <constraint firstItem=\"yIr-uo-Quc\" firstAttribute=\"top\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"top\" constant=\"5\" id=\"35L-th-6RE\"/>\n                            <constraint firstItem=\"Ejh-qu-qmy\" firstAttribute=\"leading\" relation=\"greaterThanOrEqual\" secondItem=\"eLA-Ce-crP\" secondAttribute=\"trailing\" constant=\"12\" symbolic=\"YES\" id=\"7TA-fc-yjU\"/>\n                            <constraint firstItem=\"yIr-uo-Quc\" firstAttribute=\"leading\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"leading\" constant=\"20\" id=\"AhA-g2-Cms\"/>\n                            <constraint firstItem=\"eLA-Ce-crP\" firstAttribute=\"leading\" relation=\"greaterThanOrEqual\" secondItem=\"yIr-uo-Quc\" secondAttribute=\"trailing\" constant=\"12\" symbolic=\"YES\" id=\"BRb-HY-Dny\"/>\n                            <constraint firstItem=\"MlO-xV-qug\" firstAttribute=\"top\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"top\" constant=\"30\" id=\"Bzu-9v-r7G\"/>\n                            <constraint firstItem=\"0Du-le-fYN\" firstAttribute=\"top\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"top\" constant=\"5\" id=\"DMs-p7-JWV\"/>\n                            <constraint firstAttribute=\"bottom\" secondItem=\"MlO-xV-qug\" secondAttribute=\"bottom\" id=\"HY0-vM-k0l\"/>\n                            <constraint firstItem=\"MlO-xV-qug\" firstAttribute=\"leading\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"leading\" id=\"Pp3-O7-2Bs\"/>\n                            <constraint firstItem=\"eLA-Ce-crP\" firstAttribute=\"top\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"top\" constant=\"5\" id=\"cJE-Zj-BEy\"/>\n                            <constraint firstAttribute=\"trailing\" secondItem=\"Ejh-qu-qmy\" secondAttribute=\"trailing\" constant=\"20\" id=\"eoW-Xb-6wq\"/>\n                            <constraint firstItem=\"eLA-Ce-crP\" firstAttribute=\"centerX\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"centerX\" id=\"xRm-9b-mgK\"/>\n                            <constraint firstItem=\"Ejh-qu-qmy\" firstAttribute=\"leading\" secondItem=\"0Du-le-fYN\" secondAttribute=\"trailing\" constant=\"50\" id=\"xc2-W0-bu0\"/>\n                            <constraint firstItem=\"Ejh-qu-qmy\" firstAttribute=\"top\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"top\" constant=\"5\" id=\"xnX-II-7iN\"/>\n                        </constraints>\n                    </view>\n                    <connections>\n                        <outlet property=\"collectionView\" destination=\"XnK-6H-mn7\" id=\"EoA-XQ-ad2\"/>\n                    </connections>\n                </viewController>\n                <customObject id=\"rPt-NT-nkU\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"75\" y=\"831\"/>\n        </scene>\n        <!--SwiftUI View Controller-->\n        <scene sceneID=\"Z7V-ea-dRX\">\n            <objects>\n                <viewController id=\"ei1-kq-tvV\" customClass=\"SwiftUIViewController\" customModule=\"Kingfisher_macOS_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" id=\"JMh-S8-QlI\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"620\" height=\"474\"/>\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                    </view>\n                </viewController>\n                <customObject id=\"BB8-QC-got\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"75\" y=\"1406\"/>\n        </scene>\n        <!--Heavy View Controller-->\n        <scene sceneID=\"kQM-gs-M6P\">\n            <objects>\n                <viewController id=\"19n-lE-hND\" customClass=\"GIFHeavyViewController\" customModule=\"Kingfisher_macOS_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" id=\"4Qc-Su-iB9\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"569\" height=\"480\"/>\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                        <subviews>\n                            <stackView distribution=\"fillEqually\" orientation=\"vertical\" alignment=\"leading\" horizontalStackHuggingPriority=\"249.99998474121094\" verticalStackHuggingPriority=\"249.99998474121094\" detachesHiddenViews=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"b04-pQ-0pR\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"569\" height=\"480\"/>\n                            </stackView>\n                        </subviews>\n                        <constraints>\n                            <constraint firstAttribute=\"bottom\" secondItem=\"b04-pQ-0pR\" secondAttribute=\"bottom\" id=\"WP7-Ep-JhF\"/>\n                            <constraint firstItem=\"b04-pQ-0pR\" firstAttribute=\"leading\" secondItem=\"4Qc-Su-iB9\" secondAttribute=\"leading\" id=\"t0Z-wK-nRD\"/>\n                            <constraint firstAttribute=\"trailing\" secondItem=\"b04-pQ-0pR\" secondAttribute=\"trailing\" id=\"yeI-o0-y8O\"/>\n                            <constraint firstItem=\"b04-pQ-0pR\" firstAttribute=\"top\" secondItem=\"4Qc-Su-iB9\" secondAttribute=\"top\" id=\"yhl-hr-4Mw\"/>\n                        </constraints>\n                    </view>\n                    <connections>\n                        <outlet property=\"stackView\" destination=\"b04-pQ-0pR\" id=\"nsI-xM-cZe\"/>\n                    </connections>\n                </viewController>\n                <customObject id=\"Hmf-j6-h1n\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"766.5\" y=\"831\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-macOS-Demo/Cell.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"9531\" systemVersion=\"15C50\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"9531\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\"/>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customView id=\"PdN-uu-KZg\">\n            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n            <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n            <subviews>\n                <imageView horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"hBP-z4-OwJ\">\n                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"250\" height=\"250\"/>\n                    <imageCell key=\"cell\" refusesFirstResponder=\"YES\" alignment=\"left\" imageScaling=\"proportionallyUpOrDown\" id=\"HWt-3p-ZUj\"/>\n                </imageView>\n            </subviews>\n            <constraints>\n                <constraint firstItem=\"hBP-z4-OwJ\" firstAttribute=\"top\" secondItem=\"PdN-uu-KZg\" secondAttribute=\"top\" id=\"2Us-xD-juU\"/>\n                <constraint firstAttribute=\"bottom\" secondItem=\"hBP-z4-OwJ\" secondAttribute=\"bottom\" id=\"Ot4-cl-vX8\"/>\n                <constraint firstItem=\"hBP-z4-OwJ\" firstAttribute=\"leading\" secondItem=\"PdN-uu-KZg\" secondAttribute=\"leading\" id=\"VKh-HW-KNL\"/>\n                <constraint firstAttribute=\"trailing\" secondItem=\"hBP-z4-OwJ\" secondAttribute=\"trailing\" id=\"bn6-9l-8cN\"/>\n            </constraints>\n            <point key=\"canvasLocation\" x=\"56\" y=\"464\"/>\n        </customView>\n        <customObject id=\"vgn-Ae-99c\" customClass=\"NSCollectionViewItem\">\n            <connections>\n                <outlet property=\"imageView\" destination=\"hBP-z4-OwJ\" id=\"BaO-HL-iXz\"/>\n                <outlet property=\"view\" destination=\"PdN-uu-KZg\" id=\"e9G-mO-eGM\"/>\n            </connections>\n        </customObject>\n    </objects>\n</document>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-macOS-Demo/GIFHeavyViewController.swift",
    "content": "//\n//  GIFHeavyViewController.swift\n//  Kingfisher\n//\n//  Created by yeatse on 2024/1/7.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Cocoa\nimport Kingfisher\n\nclass GIFHeavyViewController: NSViewController {\n    @IBOutlet weak var stackView: NSStackView!\n    \n    let imageViews = [\n        AnimatedImageView(),\n        AnimatedImageView(),\n        AnimatedImageView(),\n        AnimatedImageView(),\n    ]\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        let url = URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/GifHeavy.gif\")\n        \n        for imageView in imageViews {\n            stackView.addArrangedSubview(imageView)\n            imageView.translatesAutoresizingMaskIntoConstraints = false\n            imageView.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = true\n            imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)\n            imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)\n            imageView.imageScaling = .scaleProportionallyDown\n        }\n        stackView.layoutSubtreeIfNeeded()\n        for imageView in imageViews {\n            imageView.kf.setImage(with: url)\n        }\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-macOS-Demo/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>CFBundleIconFile</key>\n\t<string></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>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>4.6.2</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>1244</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>Copyright © 2019 Wei Wang. All rights reserved.</string>\n\t<key>NSMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-macOS-Demo/SwiftUIViewController.swift",
    "content": "//\n//  SwiftUIViewController.swift\n//  Kingfisher\n//\n//  Created by yeatse on 2024/1/8.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport SwiftUI\nimport Kingfisher\n\n@available(macOS 11, *)\nclass SwiftUIViewController: NSHostingController<MainView> {\n    required init?(coder: NSCoder) {\n        super.init(coder: coder, rootView: MainView())\n    }\n}\n\n@available(macOS 11, *)\nstruct MainView: View {\n    @State private var index = 1\n    \n    static let gifImageURLs: [URL] = {\n        let prefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF\"\n        return (1...3).map { URL(string: \"\\(prefix)/\\($0).gif\")! }\n    }()\n        \n    var url: URL {\n        MainView.gifImageURLs[index - 1]\n    }\n    \n    var body: some View {\n        VStack {\n            KFAnimatedImage(url)\n                .configure { view in\n                    view.framePreloadCount = 3\n                }\n                .cacheOriginalImage()\n                .onSuccess { r in\n                    print(\"suc: \\(r)\")\n                }\n                .onFailure { e in\n                    print(\"err: \\(e)\")\n                }\n                .placeholder { p in\n                    ProgressView(p)\n                }\n                .fade(duration: 1)\n                .forceTransition()\n                .aspectRatio(contentMode: .fill)\n                .frame(width: 300, height: 300)\n                .cornerRadius(20)\n                .shadow(radius: 5)\n                .frame(width: 320, height: 320)\n\n            Button(action: {\n                self.index = (self.index % 3) + 1\n            }) { Text(\"Next Image\") }\n        }\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-macOS-Demo/ViewController.swift",
    "content": "//\n//  ViewController.swift\n//  Kingfisher-macOS-Demo\n//\n//  Created by Wei Wang on 16/1/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport AppKit\nimport Kingfisher\n\nclass ViewController: NSViewController {\n    \n    @IBOutlet weak var collectionView: NSCollectionView!\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        // Do any additional setup after loading the view.\n        title = \"Kingfisher\"\n    }\n\n    @IBAction func clearCachePressed(sender: AnyObject) {\n        KingfisherManager.shared.cache.clearMemoryCache()\n        KingfisherManager.shared.cache.clearDiskCache()\n    }\n    \n    @IBAction func reloadPressed(sender: AnyObject) {\n        collectionView.reloadData()\n    }\n}\n\nextension ViewController: NSCollectionViewDataSource {\n    func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {\n        return 10\n    }\n    \n    func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {\n        let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: \"Cell\"), for: indexPath)\n        \n        let url = URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\\(indexPath.item + 1).jpg\")!\n        \n        item.imageView?.kf.indicatorType = .activity\n        KF.url(url)\n            .roundCorner(radius: .point(20))\n            .onProgress { receivedSize, totalSize in print(\"\\(indexPath.item + 1): \\(receivedSize)/\\(totalSize)\") }\n            .onSuccess { print($0) }\n            .set(to: item.imageView!)\n        \n        // Set imageView's `animates` to true if you are loading a GIF.\n        // item.imageView?.animates = true\n        return item\n    }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/AppDelegate.swift",
    "content": "//\n//  AppDelegate.swift\n//  Kingfisher-tvOS-Demo\n//\n//  Created by Wei Wang on 15/11/17.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport UIKit\nimport Kingfisher\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n    var window: UIWindow?\n\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {\n        // Override point for customization after application launch.\n        return true\n    }\n\n    func applicationWillResignActive(_ application: UIApplication) {\n        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.\n        // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.\n    }\n\n    func applicationDidEnterBackground(_ application: UIApplication) {\n        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.\n        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.\n    }\n\n    func applicationWillEnterForeground(_ application: UIApplication) {\n        // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.\n    }\n\n    func applicationDidBecomeActive(_ application: UIApplication) {\n        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.\n    }\n\n    func applicationWillTerminate(_ application: UIApplication) {\n        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.\n    }\n\n\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"tv\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json",
    "content": "{\n  \"layers\" : [\n    {\n      \"filename\" : \"Front.imagestacklayer\"\n    },\n    {\n      \"filename\" : \"Middle.imagestacklayer\"\n    },\n    {\n      \"filename\" : \"Back.imagestacklayer\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"tv\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"tv\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"tv\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json",
    "content": "{\n  \"layers\" : [\n    {\n      \"filename\" : \"Front.imagestacklayer\"\n    },\n    {\n      \"filename\" : \"Middle.imagestacklayer\"\n    },\n    {\n      \"filename\" : \"Back.imagestacklayer\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"tv\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"tv\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json",
    "content": "{\n  \"assets\" : [\n    {\n      \"size\" : \"1280x768\",\n      \"idiom\" : \"tv\",\n      \"filename\" : \"App Icon - Large.imagestack\",\n      \"role\" : \"primary-app-icon\"\n    },\n    {\n      \"size\" : \"400x240\",\n      \"idiom\" : \"tv\",\n      \"filename\" : \"App Icon - Small.imagestack\",\n      \"role\" : \"primary-app-icon\"\n    },\n    {\n      \"size\" : \"1920x720\",\n      \"idiom\" : \"tv\",\n      \"filename\" : \"Top Shelf Image.imageset\",\n      \"role\" : \"top-shelf-image\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"tv\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/LaunchImage.launchimage/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"orientation\" : \"landscape\",\n      \"idiom\" : \"tv\",\n      \"extent\" : \"full-screen\",\n      \"minimum-system-version\" : \"9.0\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder.AppleTV.Storyboard\" version=\"3.0\" toolsVersion=\"14460.31\" targetRuntime=\"AppleTV\" propertyAccessControl=\"none\" useAutolayout=\"YES\" colorMatched=\"YES\" initialViewController=\"RxR-mX-Zp2\">\n    <device id=\"appleTV\" orientation=\"landscape\">\n        <adaptation id=\"light\"/>\n    </device>\n    <dependencies>\n        <deployment identifier=\"tvOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"14460.20\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--Navigation Controller-->\n        <scene sceneID=\"Fh9-52-NAy\">\n            <objects>\n                <navigationController id=\"RxR-mX-Zp2\" sceneMemberID=\"viewController\">\n                    <navigationBar key=\"navigationBar\" contentMode=\"scaleToFill\" id=\"RDr-bh-PDt\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"1920\" height=\"145\"/>\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                    </navigationBar>\n                    <connections>\n                        <segue destination=\"Oup-Jp-dod\" kind=\"relationship\" relationship=\"rootViewController\" id=\"mjf-yd-QmD\"/>\n                    </connections>\n                </navigationController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"J8F-kQ-xW5\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"-1492\" y=\"887\"/>\n        </scene>\n        <!--Title-->\n        <scene sceneID=\"QBV-jj-lf4\">\n            <objects>\n                <collectionViewController id=\"Oup-Jp-dod\" customClass=\"NormalLoadingViewController\" customModule=\"Kingfisher_tvOS_Demo\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <collectionView key=\"view\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" dataMode=\"prototypes\" id=\"Rqa-mv-V5P\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"1920\" height=\"1080\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <collectionViewFlowLayout key=\"collectionViewLayout\" scrollDirection=\"horizontal\" minimumLineSpacing=\"60\" minimumInteritemSpacing=\"100\" id=\"w8t-vE-Ny4\">\n                            <size key=\"itemSize\" width=\"500\" height=\"500\"/>\n                            <size key=\"headerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <size key=\"footerReferenceSize\" width=\"0.0\" height=\"0.0\"/>\n                            <inset key=\"sectionInset\" minX=\"40\" minY=\"40\" maxX=\"40\" maxY=\"40\"/>\n                        </collectionViewFlowLayout>\n                        <cells>\n                            <collectionViewCell opaque=\"NO\" multipleTouchEnabled=\"YES\" contentMode=\"center\" reuseIdentifier=\"collectionViewCell\" id=\"DMI-eZ-MxY\" customClass=\"ImageCollectionViewCell\" customModule=\"Kingfisher_tvOS_Demo\" customModuleProvider=\"target\">\n                                <rect key=\"frame\" x=\"40\" y=\"188\" width=\"500\" height=\"500\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMaxY=\"YES\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"500\" height=\"500\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <imageView userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"z1G-94-m4d\">\n                                            <rect key=\"frame\" x=\"98\" y=\"68\" width=\"394\" height=\"424\"/>\n                                        </imageView>\n                                    </subviews>\n                                </view>\n                                <constraints>\n                                    <constraint firstItem=\"z1G-94-m4d\" firstAttribute=\"leading\" secondItem=\"DMI-eZ-MxY\" secondAttribute=\"leadingMargin\" id=\"InY-A2-Rr2\"/>\n                                    <constraint firstAttribute=\"bottomMargin\" secondItem=\"z1G-94-m4d\" secondAttribute=\"bottom\" id=\"ThB-Ig-1SK\"/>\n                                    <constraint firstItem=\"z1G-94-m4d\" firstAttribute=\"top\" secondItem=\"DMI-eZ-MxY\" secondAttribute=\"topMargin\" id=\"Y9C-EY-bV4\"/>\n                                    <constraint firstAttribute=\"trailingMargin\" secondItem=\"z1G-94-m4d\" secondAttribute=\"trailing\" id=\"oXF-tn-0Il\"/>\n                                </constraints>\n                                <connections>\n                                    <outlet property=\"cellImageView\" destination=\"z1G-94-m4d\" id=\"4W9-FP-JSq\"/>\n                                </connections>\n                            </collectionViewCell>\n                        </cells>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"Oup-Jp-dod\" id=\"xr8-eW-p9w\"/>\n                            <outlet property=\"delegate\" destination=\"Oup-Jp-dod\" id=\"jo9-Qc-n6o\"/>\n                        </connections>\n                    </collectionView>\n                    <navigationItem key=\"navigationItem\" title=\"Title\" id=\"73M-Nr-0wa\"/>\n                </collectionViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"9sc-iK-DXi\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"539\" y=\"887\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-tvOS-Demo/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>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>4.6.2</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>1244</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>arm64</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-watchOS-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"24x24\",\n      \"idiom\" : \"watch\",\n      \"scale\" : \"2x\",\n      \"role\" : \"notificationCenter\",\n      \"subtype\" : \"38mm\"\n    },\n    {\n      \"size\" : \"27.5x27.5\",\n      \"idiom\" : \"watch\",\n      \"scale\" : \"2x\",\n      \"role\" : \"notificationCenter\",\n      \"subtype\" : \"42mm\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"companionSettings\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"companionSettings\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"watch\",\n      \"scale\" : \"2x\",\n      \"role\" : \"appLauncher\",\n      \"subtype\" : \"38mm\"\n    },\n    {\n      \"size\" : \"44x44\",\n      \"idiom\" : \"watch\",\n      \"scale\" : \"2x\",\n      \"role\" : \"appLauncher\",\n      \"subtype\" : \"40mm\"\n    },\n    {\n      \"size\" : \"50x50\",\n      \"idiom\" : \"watch\",\n      \"scale\" : \"2x\",\n      \"role\" : \"appLauncher\",\n      \"subtype\" : \"44mm\"\n    },\n    {\n      \"size\" : \"86x86\",\n      \"idiom\" : \"watch\",\n      \"scale\" : \"2x\",\n      \"role\" : \"quickLook\",\n      \"subtype\" : \"38mm\"\n    },\n    {\n      \"size\" : \"98x98\",\n      \"idiom\" : \"watch\",\n      \"scale\" : \"2x\",\n      \"role\" : \"quickLook\",\n      \"subtype\" : \"42mm\"\n    },\n    {\n      \"size\" : \"108x108\",\n      \"idiom\" : \"watch\",\n      \"scale\" : \"2x\",\n      \"role\" : \"quickLook\",\n      \"subtype\" : \"44mm\"\n    },\n    {\n      \"idiom\" : \"watch-marketing\",\n      \"size\" : \"1024x1024\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Demo/Demo/Kingfisher-watchOS-Demo/Base.lproj/Interface.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder.WatchKit.Storyboard\" version=\"3.0\" toolsVersion=\"9531\" systemVersion=\"15C50\" targetRuntime=\"watchKit\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"AgC-eL-Hgc\">\n    <dependencies>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"9529\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBWatchKitPlugin\" version=\"9515\"/>\n    </dependencies>\n    <scenes>\n        <!--1-->\n        <scene sceneID=\"aou-V4-d1y\">\n            <objects>\n                <controller identifier=\"1\" id=\"AgC-eL-Hgc\" customClass=\"InterfaceController\" customModule=\"Kingfisher_watchOS_Demo_Extension\">\n                    <items>\n                        <imageView width=\"1\" height=\"1\" alignment=\"left\" id=\"XYV-y3-lIO\"/>\n                    </items>\n                    <connections>\n                        <outlet property=\"interfaceImage\" destination=\"XYV-y3-lIO\" id=\"kTg-86-n6u\"/>\n                        <segue destination=\"aCz-N9-LCn\" kind=\"relationship\" relationship=\"nextPage\" id=\"xmb-9P-ARd\"/>\n                    </connections>\n                </controller>\n            </objects>\n            <point key=\"canvasLocation\" x=\"421\" y=\"449\"/>\n        </scene>\n        <!--4-->\n        <scene sceneID=\"bpA-Kq-j3C\">\n            <objects>\n                <controller identifier=\"4\" id=\"hfp-j6-MU9\" customClass=\"InterfaceController\" customModule=\"Kingfisher_watchOS_Demo_Extension\">\n                    <items>\n                        <imageView width=\"1\" height=\"1\" alignment=\"left\" id=\"S4O-Qk-Mdh\"/>\n                    </items>\n                    <connections>\n                        <outlet property=\"interfaceImage\" destination=\"S4O-Qk-Mdh\" id=\"DL5-BF-zGh\"/>\n                        <segue destination=\"dHl-VT-6cQ\" kind=\"relationship\" relationship=\"nextPage\" id=\"grO-Uq-G5R\"/>\n                    </connections>\n                </controller>\n            </objects>\n            <point key=\"canvasLocation\" x=\"421\" y=\"798\"/>\n        </scene>\n        <!--5-->\n        <scene sceneID=\"MeK-gw-Fea\">\n            <objects>\n                <controller identifier=\"5\" id=\"dHl-VT-6cQ\" customClass=\"InterfaceController\" customModule=\"Kingfisher_watchOS_Demo_Extension\">\n                    <items>\n                        <imageView width=\"1\" height=\"1\" alignment=\"left\" id=\"PPw-mK-qub\"/>\n                    </items>\n                    <connections>\n                        <outlet property=\"interfaceImage\" destination=\"PPw-mK-qub\" id=\"urb-BX-ln9\"/>\n                    </connections>\n                </controller>\n            </objects>\n            <point key=\"canvasLocation\" x=\"835\" y=\"798\"/>\n        </scene>\n        <!--2-->\n        <scene sceneID=\"za1-tw-dP5\">\n            <objects>\n                <controller identifier=\"2\" id=\"aCz-N9-LCn\" customClass=\"InterfaceController\" customModule=\"Kingfisher_watchOS_Demo_Extension\">\n                    <items>\n                        <imageView width=\"1\" height=\"1\" alignment=\"left\" id=\"PX0-yX-9kz\"/>\n                    </items>\n                    <connections>\n                        <outlet property=\"interfaceImage\" destination=\"PX0-yX-9kz\" id=\"IKY-iD-nJf\"/>\n                        <segue destination=\"CSR-cq-bLK\" kind=\"relationship\" relationship=\"nextPage\" id=\"ogU-Vi-4UX\"/>\n                    </connections>\n                </controller>\n            </objects>\n            <point key=\"canvasLocation\" x=\"634\" y=\"449\"/>\n        </scene>\n        <!--3-->\n        <scene sceneID=\"eM7-ZT-bZR\">\n            <objects>\n                <controller identifier=\"3\" id=\"CSR-cq-bLK\" customClass=\"InterfaceController\" customModule=\"Kingfisher_watchOS_Demo_Extension\">\n                    <items>\n                        <imageView width=\"1\" height=\"1\" alignment=\"left\" id=\"WoJ-or-re5\"/>\n                    </items>\n                    <connections>\n                        <outlet property=\"interfaceImage\" destination=\"WoJ-or-re5\" id=\"Y49-ZL-BUe\"/>\n                        <segue destination=\"hfp-j6-MU9\" kind=\"relationship\" relationship=\"nextPage\" id=\"GHI-ND-mkv\"/>\n                    </connections>\n                </controller>\n            </objects>\n            <point key=\"canvasLocation\" x=\"835\" y=\"449\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-watchOS-Demo/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>CFBundleDisplayName</key>\n\t<string>Kingfisher-Demo</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>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>4.6.2</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>1244</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t</array>\n\t<key>WKCompanionAppBundleIdentifier</key>\n\t<string>com.onevcat.Kingfisher-Demo</string>\n\t<key>WKWatchKitApp</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-watchOS-Demo Extension/Assets.xcassets/README__ignoredByTemplate__",
    "content": "Did you know that git does not support storing empty directories?\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-watchOS-Demo Extension/ExtensionDelegate.swift",
    "content": "//\n//  ExtensionDelegate.swift\n//  Kingfisher-watchOS-Demo Extension\n//\n//  Created by Wei Wang on 16/1/19.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport WatchKit\n\nclass ExtensionDelegate: NSObject, WKExtensionDelegate {\n\n    func applicationDidFinishLaunching() {\n        // Perform any final initialization of your application.\n    }\n\n    func applicationDidBecomeActive() {\n        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.\n    }\n\n    func applicationWillResignActive() {\n        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.\n        // Use this method to pause ongoing tasks, disable timers, etc.\n    }\n\n}\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-watchOS-Demo Extension/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>CFBundleDisplayName</key>\n\t<string>Kingfisher-watchOS-Demo Extension</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>XPC!</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>4.6.2</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>1244</string>\n\t<key>NSExtension</key>\n\t<dict>\n\t\t<key>NSExtensionAttributes</key>\n\t\t<dict>\n\t\t\t<key>WKAppBundleIdentifier</key>\n\t\t\t<string>com.onevcat.Kingfisher-Demo.watchkitapp</string>\n\t\t</dict>\n\t\t<key>NSExtensionPointIdentifier</key>\n\t\t<string>com.apple.watchkit</string>\n\t</dict>\n\t<key>RemoteInterfacePrincipalClass</key>\n\t<string>$(PRODUCT_MODULE_NAME).InterfaceController</string>\n\t<key>WKExtensionDelegateClassName</key>\n\t<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "Demo/Demo/Kingfisher-watchOS-Demo Extension/InterfaceController.swift",
    "content": "//\n//  InterfaceController.swift\n//  Kingfisher-watchOS-Demo Extension\n//\n//  Created by Wei Wang on 16/1/19.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n\nimport WatchKit\nimport Foundation\nimport Kingfisher\n\n@MainActor var count = 0\n\nclass InterfaceController: WKInterfaceController {\n    \n    @IBOutlet var interfaceImage: WKInterfaceImage!\n    \n    var currentIndex: Int?\n    \n    \n    override func awake(withContext context: Any?) {\n        super.awake(withContext: context)\n        \n        currentIndex = count\n        count += 1\n    }\n    \n    func refreshImage() {\n        let url = URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\\(currentIndex! + 1).jpg\")!\n        print(\"Start loading... \\(url)\")\n        interfaceImage.kf.setImage(with: url, completionHandler:  { r in\n            print(r)\n        })\n    }\n\n    override func willActivate() {\n        // This method is called when watch view controller is about to be visible to user\n        super.willActivate()\n        refreshImage()\n    }\n\n    override func didDeactivate() {\n        // This method is called when watch view controller is no longer visible\n        super.didDeactivate()\n    }\n\n}\n"
  },
  {
    "path": "Demo/Kingfisher-Demo.entitlements",
    "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>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Demo/Kingfisher-Demo.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t072922432638639D0089E810 /* AnimatedImageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072922422638639D0089E810 /* AnimatedImageDemo.swift */; };\n\t\t0784D0992F17E41700EB2A07 /* PhotosPickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0784D0982F17E41700EB2A07 /* PhotosPickerDemo.swift */; };\n\t\t078DCB512BCFEFB40008114E /* PHPickerResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */; };\n\t\t277EAE8D2045B39C00547CD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE892045B39C00547CD3 /* Assets.xcassets */; };\n\t\t277EAE8E2045B39C00547CD3 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE8A2045B39C00547CD3 /* Interface.storyboard */; };\n\t\t277EAE9D2045B4D500547CD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE962045B4D500547CD3 /* Assets.xcassets */; };\n\t\t277EAEA12045B52800547CD3 /* InterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277EAE9E2045B52800547CD3 /* InterfaceController.swift */; };\n\t\t277EAEA32045B52800547CD3 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277EAEA02045B52800547CD3 /* ExtensionDelegate.swift */; };\n\t\t3887E1602B4AD7200062C53C /* GIFHeavyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3887E15F2B4AD7200062C53C /* GIFHeavyViewController.swift */; };\n\t\t3887E17B2B4BC04B0062C53C /* SwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3887E17A2B4BC04B0062C53C /* SwiftUIViewController.swift */; };\n\t\t4B120CA726B91BB70060B092 /* TransitionViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B120CA626B91BB70060B092 /* TransitionViewDemo.swift */; };\n\t\t4B1C7A3D21A256E300CE9D31 /* InfinityCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1C7A3C21A256E300CE9D31 /* InfinityCollectionViewController.swift */; };\n\t\t4B4307A51D87E6A700ED2DA9 /* loader.gif in Resources */ = {isa = PBXBuildFile; fileRef = 4B7742461D87E42E0077024E /* loader.gif */; };\n\t\t4B6E1B6D28DB4E8C0023B54B /* Issue1998View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6E1B6C28DB4E8C0023B54B /* Issue1998View.swift */; };\n\t\t4B7742471D87E42E0077024E /* loader.gif in Resources */ = {isa = PBXBuildFile; fileRef = 4B7742461D87E42E0077024E /* loader.gif */; };\n\t\t4B7742481D87E42E0077024E /* loader.gif in Resources */ = {isa = PBXBuildFile; fileRef = 4B7742461D87E42E0077024E /* loader.gif */; };\n\t\t4B779C8526743C2800FF9C1E /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B779C8426743C2800FF9C1E /* GeometryReaderDemo.swift */; };\n\t\t4B92FE5625FF906B00473088 /* AutoSizingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92FE5525FF906B00473088 /* AutoSizingTableViewController.swift */; };\n\t\t4B9AD88F26480D3B0086A261 /* OrientationImagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCC51AA26480CD5007004E8 /* OrientationImagesViewController.swift */; };\n\t\t4B9AD89026480D3C0086A261 /* OrientationImagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCC51AA26480CD5007004E8 /* OrientationImagesViewController.swift */; };\n\t\t4BC0ED4A29A6EE78003E9CD1 /* Issue2035View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC0ED4929A6EE78003E9CD1 /* Issue2035View.swift */; };\n\t\t4BCCF33D1D5B02F8003387C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCCF3361D5B02F8003387C2 /* AppDelegate.swift */; };\n\t\t4BCCF33E1D5B02F8003387C2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4BCCF3371D5B02F8003387C2 /* Assets.xcassets */; };\n\t\t4BCCF33F1D5B02F8003387C2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4BCCF3381D5B02F8003387C2 /* Main.storyboard */; };\n\t\t4BCCF3401D5B02F8003387C2 /* Cell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BCCF33A1D5B02F8003387C2 /* Cell.xib */; };\n\t\t4BCCF3421D5B02F8003387C2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCCF33C1D5B02F8003387C2 /* ViewController.swift */; };\n\t\t4BE855522320F9C800FE4205 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855512320F9C800FE4205 /* Kingfisher.framework */; };\n\t\t4BE855532320F9C800FE4205 /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855512320F9C800FE4205 /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };\n\t\t4BE855552320F9CF00FE4205 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855542320F9CF00FE4205 /* Kingfisher.framework */; };\n\t\t4BE855562320F9CF00FE4205 /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855542320F9CF00FE4205 /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };\n\t\t4BE855582320F9D300FE4205 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855572320F9D300FE4205 /* Kingfisher.framework */; };\n\t\t4BE855592320F9D300FE4205 /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855572320F9D300FE4205 /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };\n\t\t4BE855612320F9DE00FE4205 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855602320F9DE00FE4205 /* Kingfisher.framework */; };\n\t\t4BE855622320F9DE00FE4205 /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855602320F9DE00FE4205 /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };\n\t\t769F07F126298E2E00610767 /* GIFHeavyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F07F026298E2E00610767 /* GIFHeavyViewController.swift */; };\n\t\tC959EEE622874DC600467A10 /* ProgressiveJPEGViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C959EEE522874DC600467A10 /* ProgressiveJPEGViewController.swift */; };\n\t\tD10AC99821A300C9005F057C /* ProcessorCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10AC99721A300C9005F057C /* ProcessorCollectionViewController.swift */; };\n\t\tD12E0C951C47F91800AC98AD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C8C1C47F91800AC98AD /* AppDelegate.swift */; };\n\t\tD12E0C971C47F91800AC98AD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C8F1C47F91800AC98AD /* Main.storyboard */; };\n\t\tD12E0C981C47F91800AC98AD /* ImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C911C47F91800AC98AD /* ImageCollectionViewCell.swift */; };\n\t\tD12E0C991C47F91800AC98AD /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C921C47F91800AC98AD /* Images.xcassets */; };\n\t\tD12E0C9B1C47F91800AC98AD /* NormalLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C941C47F91800AC98AD /* NormalLoadingViewController.swift */; };\n\t\tD12E0CA21C47F92200AC98AD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C9D1C47F92200AC98AD /* AppDelegate.swift */; };\n\t\tD12E0CA31C47F92200AC98AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C9E1C47F92200AC98AD /* Assets.xcassets */; };\n\t\tD12E0CA41C47F92200AC98AD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C9F1C47F92200AC98AD /* Main.storyboard */; };\n\t\tD12E0CB61C47F9C100AC98AD /* NormalLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C941C47F91800AC98AD /* NormalLoadingViewController.swift */; };\n\t\tD12EB83E24DD902300329EE1 /* TextAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12EB83D24DD902300329EE1 /* TextAttachmentViewController.swift */; };\n\t\tD12EB84024DDB9E100329EE1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D12EB83F24DDB9E000329EE1 /* LaunchScreen.storyboard */; };\n\t\tD12F67682CB10AE000AB63AB /* LivePhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67672CB10AD900AB63AB /* LivePhotoViewController.swift */; };\n\t\tD14799D92E1129A900053537 /* LoadingFailureDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14799D82E1129A800053537 /* LoadingFailureDemo.swift */; };\n\t\tD1679A461C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };\n\t\tD16AAF282D5247CF00E7F764 /* Issue2352View.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16AAF272D5247CA00E7F764 /* Issue2352View.swift */; };\n\t\tD16CC3D824E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16CC3D724E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift */; };\n\t\tD198F41E25EDC11500C53E0D /* LazyVStackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D198F41D25EDC11500C53E0D /* LazyVStackDemo.swift */; };\n\t\tD198F42025EDC34000C53E0D /* SizingAnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D198F41F25EDC34000C53E0D /* SizingAnimationDemo.swift */; };\n\t\tD198F42225EDC4B900C53E0D /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D198F42125EDC4B900C53E0D /* GridDemo.swift */; };\n\t\tD1A1CCA321A1879600263AD8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CCA221A1879600263AD8 /* MainViewController.swift */; };\n\t\tD1A1CCA721A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CCA621A18A3200263AD8 /* UIViewController+KingfisherOperation.swift */; };\n\t\tD1A1CCA821A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CCA621A18A3200263AD8 /* UIViewController+KingfisherOperation.swift */; };\n\t\tD1CE1BD021A1AFA300419000 /* TransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CE1BCF21A1AFA300419000 /* TransitionViewController.swift */; };\n\t\tD1CE1BD321A1B45A00419000 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CE1BD221A1B45A00419000 /* ImageLoader.swift */; };\n\t\tD1CE1BD421A1B45A00419000 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CE1BD221A1B45A00419000 /* ImageLoader.swift */; };\n\t\tD1E4CF5421BACBA6004D029D /* ImageDataProviderCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E4CF5321BACBA6004D029D /* ImageDataProviderCollectionViewController.swift */; };\n\t\tD1E612E22D75F9AC00DACD51 /* ProgressiveJPEGDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E612E12D75F99C00DACD51 /* ProgressiveJPEGDemo.swift */; };\n\t\tD1EDF7422C9F01270017FFA5 /* Issue2295View.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EDF7412C9F01200017FFA5 /* Issue2295View.swift */; };\n\t\tD1F06F3321AA4292000B1C38 /* DetailImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F06F3221AA4292000B1C38 /* DetailImageViewController.swift */; };\n\t\tD1F06F3521AA5938000B1C38 /* ImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C911C47F91800AC98AD /* ImageCollectionViewCell.swift */; };\n\t\tD1F06F3721AAEACF000B1C38 /* GIFViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F06F3621AAEACF000B1C38 /* GIFViewController.swift */; };\n\t\tD1F06F3921AAF1EE000B1C38 /* IndicatorCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F06F3821AAF1EE000B1C38 /* IndicatorCollectionViewController.swift */; };\n\t\tD1F78A5F2589F0AA00930759 /* SwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F78A5E2589F0AA00930759 /* SwiftUIViewController.swift */; };\n\t\tD1F78A642589F17200930759 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F78A612589F17200930759 /* ListDemo.swift */; };\n\t\tD1F78A652589F17200930759 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F78A622589F17200930759 /* MainView.swift */; };\n\t\tD1F78A662589F17200930759 /* SingleViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F78A632589F17200930759 /* SingleViewDemo.swift */; };\n\t\tD1FAB06F21A853E600908910 /* HighResolutionCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FAB06E21A853E600908910 /* HighResolutionCollectionViewController.swift */; };\n\t\tF344AEF22E1AD74F00BFE702 /* LoadTransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F344AEF12E1AD74F00BFE702 /* LoadTransitionDemo.swift */; };\n\t\tF39B68D42E33E0D600404B02 /* NetworkMetricsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39B68D32E33E0D600404B02 /* NetworkMetricsViewController.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\tD1679A471C4E78B20020FD12 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = D1ED2D031AD2CFA600CFC3EB /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = D1679A441C4E78B20020FD12;\n\t\t\tremoteInfo = \"Kingfisher-watchOS-Demo Extension\";\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\tD12F2EEC1C4E7CF500B8054D /* 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\t4BE855622320F9DE00FE4205 /* Kingfisher.framework in Embed Frameworks */,\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD13F49ED1BEDA82000CE335D /* 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\t4BE855562320F9CF00FE4205 /* Kingfisher.framework in Embed Frameworks */,\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1679A571C4E78B20020FD12 /* Embed Foundation Extensions */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 13;\n\t\t\tfiles = (\n\t\t\t\tD1679A461C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex in Embed Foundation Extensions */,\n\t\t\t);\n\t\t\tname = \"Embed Foundation Extensions\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD19ADCFC23099ADE00D20B28 /* 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\t4BE855592320F9D300FE4205 /* Kingfisher.framework in Embed Frameworks */,\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD19ADCFF23099B4300D20B28 /* 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);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1ED2D511AD2D09F00CFC3EB /* 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\t4BE855532320F9C800FE4205 /* Kingfisher.framework in Embed Frameworks */,\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t072922422638639D0089E810 /* AnimatedImageDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageDemo.swift; sourceTree = \"<group>\"; };\n\t\t0784D0982F17E41700EB2A07 /* PhotosPickerDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosPickerDemo.swift; sourceTree = \"<group>\"; };\n\t\t078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultViewController.swift; sourceTree = \"<group>\"; };\n\t\t277EAE892045B39C00547CD3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t277EAE8B2045B39C00547CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = \"<group>\"; };\n\t\t277EAE8C2045B39C00547CD3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t277EAE962045B4D500547CD3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t277EAE9E2045B52800547CD3 /* InterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterfaceController.swift; sourceTree = \"<group>\"; };\n\t\t277EAE9F2045B52800547CD3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t277EAEA02045B52800547CD3 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = \"<group>\"; };\n\t\t3887E15F2B4AD7200062C53C /* GIFHeavyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFHeavyViewController.swift; sourceTree = \"<group>\"; };\n\t\t3887E17A2B4BC04B0062C53C /* SwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewController.swift; sourceTree = \"<group>\"; };\n\t\t4B120CA626B91BB70060B092 /* TransitionViewDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionViewDemo.swift; sourceTree = \"<group>\"; };\n\t\t4B1C7A3C21A256E300CE9D31 /* InfinityCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfinityCollectionViewController.swift; sourceTree = \"<group>\"; };\n\t\t4B2944551C3D03880088C3E7 /* Kingfisher-macOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"Kingfisher-macOS-Demo.app\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t4B6E1B6C28DB4E8C0023B54B /* Issue1998View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue1998View.swift; sourceTree = \"<group>\"; };\n\t\t4B7742461D87E42E0077024E /* loader.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = loader.gif; sourceTree = \"<group>\"; };\n\t\t4B779C8426743C2800FF9C1E /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = \"<group>\"; };\n\t\t4B92FE5525FF906B00473088 /* AutoSizingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoSizingTableViewController.swift; sourceTree = \"<group>\"; };\n\t\t4BC0ED4929A6EE78003E9CD1 /* Issue2035View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue2035View.swift; sourceTree = \"<group>\"; };\n\t\t4BCC51AA26480CD5007004E8 /* OrientationImagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationImagesViewController.swift; sourceTree = \"<group>\"; };\n\t\t4BCCF3361D5B02F8003387C2 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t4BCCF3371D5B02F8003387C2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t4BCCF3391D5B02F8003387C2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t4BCCF33A1D5B02F8003387C2 /* Cell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = Cell.xib; sourceTree = \"<group>\"; };\n\t\t4BCCF33B1D5B02F8003387C2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t4BCCF33C1D5B02F8003387C2 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = \"<group>\"; };\n\t\t4BE855512320F9C800FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t4BE855542320F9CF00FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t4BE855572320F9D300FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t4BE8555A2320F9D800FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t4BE8555B2320F9D800FE4205 /* KingfisherSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KingfisherSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t4BE855602320F9DE00FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t769F07F026298E2E00610767 /* GIFHeavyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFHeavyViewController.swift; sourceTree = \"<group>\"; };\n\t\tC959EEE522874DC600467A10 /* ProgressiveJPEGViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressiveJPEGViewController.swift; sourceTree = \"<group>\"; };\n\t\tD10AC99721A300C9005F057C /* ProcessorCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessorCollectionViewController.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C8C1C47F91800AC98AD /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C901C47F91800AC98AD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\tD12E0C911C47F91800AC98AD /* ImageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCollectionViewCell.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C921C47F91800AC98AD /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = \"<group>\"; };\n\t\tD12E0C931C47F91800AC98AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tD12E0C941C47F91800AC98AD /* NormalLoadingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalLoadingViewController.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C9D1C47F92200AC98AD /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C9E1C47F92200AC98AD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\tD12E0CA01C47F92200AC98AD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\tD12E0CA11C47F92200AC98AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tD12EB83D24DD902300329EE1 /* TextAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAttachmentViewController.swift; sourceTree = \"<group>\"; };\n\t\tD12EB83F24DDB9E000329EE1 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\tD12F67672CB10AD900AB63AB /* LivePhotoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoViewController.swift; sourceTree = \"<group>\"; };\n\t\tD13F49C21BEDA53F00CE335D /* Kingfisher-tvOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"Kingfisher-tvOS-Demo.app\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD14799D82E1129A800053537 /* LoadingFailureDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingFailureDemo.swift; sourceTree = \"<group>\"; };\n\t\tD16218A4238EAA67004A1C6C /* Kingfisher-Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = \"Kingfisher-Demo.entitlements\"; sourceTree = \"<group>\"; };\n\t\tD1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"Kingfisher-watchOS-Demo.app\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */ = {isa = PBXFileReference; explicitFileType = \"wrapper.app-extension\"; includeInIndex = 0; path = \"Kingfisher-watchOS-Demo Extension.appex\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD16AAF272D5247CA00E7F764 /* Issue2352View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue2352View.swift; sourceTree = \"<group>\"; };\n\t\tD16CC3D724E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVAssetImageGeneratorViewController.swift; sourceTree = \"<group>\"; };\n\t\tD198F41D25EDC11500C53E0D /* LazyVStackDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyVStackDemo.swift; sourceTree = \"<group>\"; };\n\t\tD198F41F25EDC34000C53E0D /* SizingAnimationDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizingAnimationDemo.swift; sourceTree = \"<group>\"; };\n\t\tD198F42125EDC4B900C53E0D /* GridDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = \"<group>\"; };\n\t\tD1A1CCA221A1879600263AD8 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = \"<group>\"; };\n\t\tD1A1CCA621A18A3200263AD8 /* UIViewController+KingfisherOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UIViewController+KingfisherOperation.swift\"; sourceTree = \"<group>\"; };\n\t\tD1CE1BCF21A1AFA300419000 /* TransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionViewController.swift; sourceTree = \"<group>\"; };\n\t\tD1CE1BD221A1B45A00419000 /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = \"<group>\"; };\n\t\tD1E4CF5321BACBA6004D029D /* ImageDataProviderCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataProviderCollectionViewController.swift; sourceTree = \"<group>\"; };\n\t\tD1E612E12D75F99C00DACD51 /* ProgressiveJPEGDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressiveJPEGDemo.swift; sourceTree = \"<group>\"; };\n\t\tD1ED2D0B1AD2CFA600CFC3EB /* Kingfisher-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"Kingfisher-Demo.app\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD1EDF7412C9F01200017FFA5 /* Issue2295View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue2295View.swift; sourceTree = \"<group>\"; };\n\t\tD1F06F3221AA4292000B1C38 /* DetailImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailImageViewController.swift; sourceTree = \"<group>\"; };\n\t\tD1F06F3621AAEACF000B1C38 /* GIFViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFViewController.swift; sourceTree = \"<group>\"; };\n\t\tD1F06F3821AAF1EE000B1C38 /* IndicatorCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndicatorCollectionViewController.swift; sourceTree = \"<group>\"; };\n\t\tD1F78A5E2589F0AA00930759 /* SwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewController.swift; sourceTree = \"<group>\"; };\n\t\tD1F78A612589F17200930759 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = \"<group>\"; };\n\t\tD1F78A622589F17200930759 /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = \"<group>\"; };\n\t\tD1F78A632589F17200930759 /* SingleViewDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleViewDemo.swift; sourceTree = \"<group>\"; };\n\t\tD1FAB06E21A853E600908910 /* HighResolutionCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighResolutionCollectionViewController.swift; sourceTree = \"<group>\"; };\n\t\tF344AEF12E1AD74F00BFE702 /* LoadTransitionDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTransitionDemo.swift; sourceTree = \"<group>\"; };\n\t\tF39B68D32E33E0D600404B02 /* NetworkMetricsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMetricsViewController.swift; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t4B2944521C3D03880088C3E7 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t4BE855582320F9D300FE4205 /* Kingfisher.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t60796AE9A717329E55ACCCC7 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD13F49BF1BEDA53F00CE335D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t4BE855552320F9CF00FE4205 /* Kingfisher.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1679A421C4E78B20020FD12 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t4BE855612320F9DE00FE4205 /* Kingfisher.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1ED2D081AD2CFA600CFC3EB /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t4BE855522320F9C800FE4205 /* Kingfisher.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\t277EAE762045ADE700547CD3 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t4BE855602320F9DE00FE4205 /* Kingfisher.framework */,\n\t\t\t\t4BE8555A2320F9D800FE4205 /* Kingfisher.framework */,\n\t\t\t\t4BE8555B2320F9D800FE4205 /* KingfisherSwiftUI.framework */,\n\t\t\t\t4BE855572320F9D300FE4205 /* Kingfisher.framework */,\n\t\t\t\t4BE855542320F9CF00FE4205 /* Kingfisher.framework */,\n\t\t\t\t4BE855512320F9C800FE4205 /* Kingfisher.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t277EAE882045B39C00547CD3 /* Kingfisher-watchOS-Demo */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t277EAE892045B39C00547CD3 /* Assets.xcassets */,\n\t\t\t\t277EAE8A2045B39C00547CD3 /* Interface.storyboard */,\n\t\t\t\t277EAE8C2045B39C00547CD3 /* Info.plist */,\n\t\t\t);\n\t\t\tname = \"Kingfisher-watchOS-Demo\";\n\t\t\tpath = \"Demo/Kingfisher-watchOS-Demo\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t277EAE952045B4D500547CD3 /* Kingfisher-watchOS-Demo Extension */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t277EAE962045B4D500547CD3 /* Assets.xcassets */,\n\t\t\t\t277EAEA02045B52800547CD3 /* ExtensionDelegate.swift */,\n\t\t\t\t277EAE9F2045B52800547CD3 /* Info.plist */,\n\t\t\t\t277EAE9E2045B52800547CD3 /* InterfaceController.swift */,\n\t\t\t);\n\t\t\tname = \"Kingfisher-watchOS-Demo Extension\";\n\t\t\tpath = \"Demo/Kingfisher-watchOS-Demo Extension\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t4BC0ED4829A6EE4F003E9CD1 /* Regression */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD1EDF7412C9F01200017FFA5 /* Issue2295View.swift */,\n\t\t\t\t4B6E1B6C28DB4E8C0023B54B /* Issue1998View.swift */,\n\t\t\t\t4BC0ED4929A6EE78003E9CD1 /* Issue2035View.swift */,\n\t\t\t\tD16AAF272D5247CA00E7F764 /* Issue2352View.swift */,\n\t\t\t);\n\t\t\tpath = Regression;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t4BCCF3351D5B02F8003387C2 /* Kingfisher-macOS-Demo */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t4BCCF3361D5B02F8003387C2 /* AppDelegate.swift */,\n\t\t\t\t4BCCF3371D5B02F8003387C2 /* Assets.xcassets */,\n\t\t\t\t4BCCF3381D5B02F8003387C2 /* Main.storyboard */,\n\t\t\t\t4BCCF33A1D5B02F8003387C2 /* Cell.xib */,\n\t\t\t\t4BCCF33B1D5B02F8003387C2 /* Info.plist */,\n\t\t\t\t4BCCF33C1D5B02F8003387C2 /* ViewController.swift */,\n\t\t\t\t3887E15F2B4AD7200062C53C /* GIFHeavyViewController.swift */,\n\t\t\t\t3887E17A2B4BC04B0062C53C /* SwiftUIViewController.swift */,\n\t\t\t);\n\t\t\tname = \"Kingfisher-macOS-Demo\";\n\t\t\tpath = \"Demo/Kingfisher-macOS-Demo\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD10EC22B1C3D62DE00A4211C /* Demo */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12E0C8B1C47F91800AC98AD /* Kingfisher-Demo */,\n\t\t\t\t4BCCF3351D5B02F8003387C2 /* Kingfisher-macOS-Demo */,\n\t\t\t\tD12E0C9C1C47F92200AC98AD /* Kingfisher-tvOS-Demo */,\n\t\t\t\t277EAE952045B4D500547CD3 /* Kingfisher-watchOS-Demo Extension */,\n\t\t\t\t277EAE882045B39C00547CD3 /* Kingfisher-watchOS-Demo */,\n\t\t\t);\n\t\t\tname = Demo;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12E0C8B1C47F91800AC98AD /* Kingfisher-Demo */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12E0C931C47F91800AC98AD /* Info.plist */,\n\t\t\t\tD12E0C8C1C47F91800AC98AD /* AppDelegate.swift */,\n\t\t\t\tD12EB83F24DDB9E000329EE1 /* LaunchScreen.storyboard */,\n\t\t\t\tD12E0C8F1C47F91800AC98AD /* Main.storyboard */,\n\t\t\t\tD1A1CCA921A1936300263AD8 /* ViewControllers */,\n\t\t\t\tD1F78A602589F14C00930759 /* SwiftUIViews */,\n\t\t\t\tD1A1CCA521A1895000263AD8 /* Extensions */,\n\t\t\t\tD1A1CCAB21A1939100263AD8 /* Resources */,\n\t\t\t\tD12E0C921C47F91800AC98AD /* Images.xcassets */,\n\t\t\t);\n\t\t\tname = \"Kingfisher-Demo\";\n\t\t\tpath = \"Demo/Kingfisher-Demo\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12E0C9C1C47F92200AC98AD /* Kingfisher-tvOS-Demo */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12E0C9D1C47F92200AC98AD /* AppDelegate.swift */,\n\t\t\t\tD12E0C9E1C47F92200AC98AD /* Assets.xcassets */,\n\t\t\t\tD12E0C9F1C47F92200AC98AD /* Main.storyboard */,\n\t\t\t\tD12E0CA11C47F92200AC98AD /* Info.plist */,\n\t\t\t);\n\t\t\tname = \"Kingfisher-tvOS-Demo\";\n\t\t\tpath = \"Demo/Kingfisher-tvOS-Demo\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1A1CCA521A1895000263AD8 /* Extensions */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD1A1CCA621A18A3200263AD8 /* UIViewController+KingfisherOperation.swift */,\n\t\t\t);\n\t\t\tpath = Extensions;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1A1CCA921A1936300263AD8 /* ViewControllers */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12F67672CB10AD900AB63AB /* LivePhotoViewController.swift */,\n\t\t\t\tD10AC99721A300C9005F057C /* ProcessorCollectionViewController.swift */,\n\t\t\t\t4B1C7A3C21A256E300CE9D31 /* InfinityCollectionViewController.swift */,\n\t\t\t\tD1CE1BCF21A1AFA300419000 /* TransitionViewController.swift */,\n\t\t\t\tD12E0C941C47F91800AC98AD /* NormalLoadingViewController.swift */,\n\t\t\t\t4BCC51AA26480CD5007004E8 /* OrientationImagesViewController.swift */,\n\t\t\t\t4B92FE5525FF906B00473088 /* AutoSizingTableViewController.swift */,\n\t\t\t\tD12E0C911C47F91800AC98AD /* ImageCollectionViewCell.swift */,\n\t\t\t\tD1A1CCA221A1879600263AD8 /* MainViewController.swift */,\n\t\t\t\tD1FAB06E21A853E600908910 /* HighResolutionCollectionViewController.swift */,\n\t\t\t\tD1F06F3221AA4292000B1C38 /* DetailImageViewController.swift */,\n\t\t\t\tD1F06F3621AAEACF000B1C38 /* GIFViewController.swift */,\n\t\t\t\t769F07F026298E2E00610767 /* GIFHeavyViewController.swift */,\n\t\t\t\tD1F06F3821AAF1EE000B1C38 /* IndicatorCollectionViewController.swift */,\n\t\t\t\tD1E4CF5321BACBA6004D029D /* ImageDataProviderCollectionViewController.swift */,\n\t\t\t\tC959EEE522874DC600467A10 /* ProgressiveJPEGViewController.swift */,\n\t\t\t\tD12EB83D24DD902300329EE1 /* TextAttachmentViewController.swift */,\n\t\t\t\tD16CC3D724E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift */,\n\t\t\t\t078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */,\n\t\t\t\tD1F78A5E2589F0AA00930759 /* SwiftUIViewController.swift */,\n\t\t\t\tF39B68D32E33E0D600404B02 /* NetworkMetricsViewController.swift */,\n\t\t\t);\n\t\t\tpath = ViewControllers;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1A1CCAB21A1939100263AD8 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t4B7742461D87E42E0077024E /* loader.gif */,\n\t\t\t\tD1CE1BD221A1B45A00419000 /* ImageLoader.swift */,\n\t\t\t);\n\t\t\tpath = Resources;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1ED2D021AD2CFA600CFC3EB = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16218A4238EAA67004A1C6C /* Kingfisher-Demo.entitlements */,\n\t\t\t\tD10EC22B1C3D62DE00A4211C /* Demo */,\n\t\t\t\tD1ED2D0C1AD2CFA600CFC3EB /* Products */,\n\t\t\t\t277EAE762045ADE700547CD3 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1ED2D0C1AD2CFA600CFC3EB /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD1ED2D0B1AD2CFA600CFC3EB /* Kingfisher-Demo.app */,\n\t\t\t\tD13F49C21BEDA53F00CE335D /* Kingfisher-tvOS-Demo.app */,\n\t\t\t\t4B2944551C3D03880088C3E7 /* Kingfisher-macOS-Demo.app */,\n\t\t\t\tD1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */,\n\t\t\t\tD1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1F78A602589F14C00930759 /* SwiftUIViews */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t4BC0ED4829A6EE4F003E9CD1 /* Regression */,\n\t\t\t\tD1F78A622589F17200930759 /* MainView.swift */,\n\t\t\t\tD14799D82E1129A800053537 /* LoadingFailureDemo.swift */,\n\t\t\t\tD1F78A612589F17200930759 /* ListDemo.swift */,\n\t\t\t\tD198F42125EDC4B900C53E0D /* GridDemo.swift */,\n\t\t\t\t4B779C8426743C2800FF9C1E /* GeometryReaderDemo.swift */,\n\t\t\t\tD1F78A632589F17200930759 /* SingleViewDemo.swift */,\n\t\t\t\tD1E612E12D75F99C00DACD51 /* ProgressiveJPEGDemo.swift */,\n\t\t\t\t4B120CA626B91BB70060B092 /* TransitionViewDemo.swift */,\n\t\t\t\tD198F41D25EDC11500C53E0D /* LazyVStackDemo.swift */,\n\t\t\t\tD198F41F25EDC34000C53E0D /* SizingAnimationDemo.swift */,\n\t\t\t\t072922422638639D0089E810 /* AnimatedImageDemo.swift */,\n\t\t\t\tF344AEF12E1AD74F00BFE702 /* LoadTransitionDemo.swift */,\n\t\t\t\t0784D0982F17E41700EB2A07 /* PhotosPickerDemo.swift */,\n\t\t\t);\n\t\t\tpath = SwiftUIViews;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t4B2944541C3D03880088C3E7 /* Kingfisher-macOS-Demo */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 4B2944611C3D03880088C3E7 /* Build configuration list for PBXNativeTarget \"Kingfisher-macOS-Demo\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t4B2944511C3D03880088C3E7 /* Sources */,\n\t\t\t\t4B2944521C3D03880088C3E7 /* Frameworks */,\n\t\t\t\t4B2944531C3D03880088C3E7 /* Resources */,\n\t\t\t\tD19ADCFC23099ADE00D20B28 /* Embed Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Kingfisher-macOS-Demo\";\n\t\t\tproductName = \"Kingfisher-OSX-Demo\";\n\t\t\tproductReference = 4B2944551C3D03880088C3E7 /* Kingfisher-macOS-Demo.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\tD13F49C11BEDA53F00CE335D /* Kingfisher-tvOS-Demo */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D13F49D01BEDA53F00CE335D /* Build configuration list for PBXNativeTarget \"Kingfisher-tvOS-Demo\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD13F49BE1BEDA53F00CE335D /* Sources */,\n\t\t\t\tD13F49BF1BEDA53F00CE335D /* Frameworks */,\n\t\t\t\tD13F49C01BEDA53F00CE335D /* Resources */,\n\t\t\t\tD13F49ED1BEDA82000CE335D /* Embed Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Kingfisher-tvOS-Demo\";\n\t\t\tproductName = \"Kingfisher-tvOS-Demo\";\n\t\t\tproductReference = D13F49C21BEDA53F00CE335D /* Kingfisher-tvOS-Demo.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\tD1679A381C4E78B20020FD12 /* Kingfisher-watchOS-Demo */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D1679A581C4E78B20020FD12 /* Build configuration list for PBXNativeTarget \"Kingfisher-watchOS-Demo\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD1679A371C4E78B20020FD12 /* Resources */,\n\t\t\t\tD1679A571C4E78B20020FD12 /* Embed Foundation Extensions */,\n\t\t\t\t60796AE9A717329E55ACCCC7 /* Frameworks */,\n\t\t\t\tD19ADCFF23099B4300D20B28 /* Embed Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\tD1679A481C4E78B20020FD12 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = \"Kingfisher-watchOS-Demo\";\n\t\t\tproductName = \"Kingfisher-watchOS-Demo\";\n\t\t\tproductReference = D1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */;\n\t\t\tproductType = \"com.apple.product-type.application.watchapp2\";\n\t\t};\n\t\tD1679A441C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D1679A541C4E78B20020FD12 /* Build configuration list for PBXNativeTarget \"Kingfisher-watchOS-Demo Extension\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD1679A411C4E78B20020FD12 /* Sources */,\n\t\t\t\tD1679A421C4E78B20020FD12 /* Frameworks */,\n\t\t\t\tD1679A431C4E78B20020FD12 /* Resources */,\n\t\t\t\tD12F2EEC1C4E7CF500B8054D /* Embed Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Kingfisher-watchOS-Demo Extension\";\n\t\t\tproductName = \"Kingfisher-watchOS-Demo Extension\";\n\t\t\tproductReference = D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */;\n\t\t\tproductType = \"com.apple.product-type.watchkit2-extension\";\n\t\t};\n\t\tD1ED2D0A1AD2CFA600CFC3EB /* Kingfisher-Demo */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D1ED2D2A1AD2CFA600CFC3EB /* Build configuration list for PBXNativeTarget \"Kingfisher-Demo\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD1ED2D071AD2CFA600CFC3EB /* Sources */,\n\t\t\t\tD1ED2D081AD2CFA600CFC3EB /* Frameworks */,\n\t\t\t\tD1ED2D091AD2CFA600CFC3EB /* Resources */,\n\t\t\t\tD1ED2D511AD2D09F00CFC3EB /* Embed Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Kingfisher-Demo\";\n\t\t\tproductName = \"Kingfisher-Demo\";\n\t\t\tproductReference = D1ED2D0B1AD2CFA600CFC3EB /* Kingfisher-Demo.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\tD1ED2D031AD2CFA600CFC3EB /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 1100;\n\t\t\t\tLastUpgradeCheck = 1640;\n\t\t\t\tORGANIZATIONNAME = \"Wei Wang\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t4B2944541C3D03880088C3E7 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.2;\n\t\t\t\t\t\tDevelopmentTeam = A4YJ9MRZ66;\n\t\t\t\t\t\tLastSwiftMigration = 0900;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t};\n\t\t\t\t\tD13F49C11BEDA53F00CE335D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.1;\n\t\t\t\t\t\tLastSwiftMigration = 0900;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t};\n\t\t\t\t\tD1679A381C4E78B20020FD12 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.2;\n\t\t\t\t\t\tLastSwiftMigration = 0900;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t};\n\t\t\t\t\tD1679A441C4E78B20020FD12 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.2;\n\t\t\t\t\t\tLastSwiftMigration = 0920;\n\t\t\t\t\t};\n\t\t\t\t\tD1ED2D0A1AD2CFA600CFC3EB = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 6.2;\n\t\t\t\t\t\tLastSwiftMigration = 1200;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = D1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject \"Kingfisher-Demo\" */;\n\t\t\tcompatibilityVersion = \"Xcode 3.2\";\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 = D1ED2D021AD2CFA600CFC3EB;\n\t\t\tproductRefGroup = D1ED2D0C1AD2CFA600CFC3EB /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\tD1ED2D0A1AD2CFA600CFC3EB /* Kingfisher-Demo */,\n\t\t\t\tD13F49C11BEDA53F00CE335D /* Kingfisher-tvOS-Demo */,\n\t\t\t\t4B2944541C3D03880088C3E7 /* Kingfisher-macOS-Demo */,\n\t\t\t\tD1679A381C4E78B20020FD12 /* Kingfisher-watchOS-Demo */,\n\t\t\t\tD1679A441C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t4B2944531C3D03880088C3E7 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t4BCCF33E1D5B02F8003387C2 /* Assets.xcassets in Resources */,\n\t\t\t\t4BCCF3401D5B02F8003387C2 /* Cell.xib in Resources */,\n\t\t\t\t4B4307A51D87E6A700ED2DA9 /* loader.gif in Resources */,\n\t\t\t\t4BCCF33F1D5B02F8003387C2 /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD13F49C01BEDA53F00CE335D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t4B7742481D87E42E0077024E /* loader.gif in Resources */,\n\t\t\t\tD12E0CA31C47F92200AC98AD /* Assets.xcassets in Resources */,\n\t\t\t\tD12E0CA41C47F92200AC98AD /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1679A371C4E78B20020FD12 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t277EAE8E2045B39C00547CD3 /* Interface.storyboard in Resources */,\n\t\t\t\t277EAE8D2045B39C00547CD3 /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1679A431C4E78B20020FD12 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t277EAE9D2045B4D500547CD3 /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1ED2D091AD2CFA600CFC3EB /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD12E0C991C47F91800AC98AD /* Images.xcassets in Resources */,\n\t\t\t\t4B7742471D87E42E0077024E /* loader.gif in Resources */,\n\t\t\t\tD12EB84024DDB9E100329EE1 /* LaunchScreen.storyboard in Resources */,\n\t\t\t\tD12E0C971C47F91800AC98AD /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t4B2944511C3D03880088C3E7 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t3887E17B2B4BC04B0062C53C /* SwiftUIViewController.swift in Sources */,\n\t\t\t\t4BCCF3421D5B02F8003387C2 /* ViewController.swift in Sources */,\n\t\t\t\t3887E1602B4AD7200062C53C /* GIFHeavyViewController.swift in Sources */,\n\t\t\t\t4BCCF33D1D5B02F8003387C2 /* AppDelegate.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD13F49BE1BEDA53F00CE335D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD1CE1BD421A1B45A00419000 /* ImageLoader.swift in Sources */,\n\t\t\t\tD12E0CB61C47F9C100AC98AD /* NormalLoadingViewController.swift in Sources */,\n\t\t\t\tD1F06F3521AA5938000B1C38 /* ImageCollectionViewCell.swift in Sources */,\n\t\t\t\t4B9AD89026480D3C0086A261 /* OrientationImagesViewController.swift in Sources */,\n\t\t\t\tD1A1CCA821A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */,\n\t\t\t\tD12E0CA21C47F92200AC98AD /* AppDelegate.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1679A411C4E78B20020FD12 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t277EAEA32045B52800547CD3 /* ExtensionDelegate.swift in Sources */,\n\t\t\t\t277EAEA12045B52800547CD3 /* InterfaceController.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1ED2D071AD2CFA600CFC3EB /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD16CC3D824E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift in Sources */,\n\t\t\t\tC959EEE622874DC600467A10 /* ProgressiveJPEGViewController.swift in Sources */,\n\t\t\t\tD1CE1BD321A1B45A00419000 /* ImageLoader.swift in Sources */,\n\t\t\t\tD12EB83E24DD902300329EE1 /* TextAttachmentViewController.swift in Sources */,\n\t\t\t\tD12E0C9B1C47F91800AC98AD /* NormalLoadingViewController.swift in Sources */,\n\t\t\t\tD1CE1BD021A1AFA300419000 /* TransitionViewController.swift in Sources */,\n\t\t\t\tF344AEF22E1AD74F00BFE702 /* LoadTransitionDemo.swift in Sources */,\n\t\t\t\tD10AC99821A300C9005F057C /* ProcessorCollectionViewController.swift in Sources */,\n\t\t\t\tD1F06F3921AAF1EE000B1C38 /* IndicatorCollectionViewController.swift in Sources */,\n\t\t\t\tD1F78A662589F17200930759 /* SingleViewDemo.swift in Sources */,\n\t\t\t\tF39B68D42E33E0D600404B02 /* NetworkMetricsViewController.swift in Sources */,\n\t\t\t\tD12E0C981C47F91800AC98AD /* ImageCollectionViewCell.swift in Sources */,\n\t\t\t\tD198F42025EDC34000C53E0D /* SizingAnimationDemo.swift in Sources */,\n\t\t\t\t072922432638639D0089E810 /* AnimatedImageDemo.swift in Sources */,\n\t\t\t\t4B6E1B6D28DB4E8C0023B54B /* Issue1998View.swift in Sources */,\n\t\t\t\tD1A1CCA721A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */,\n\t\t\t\tD14799D92E1129A900053537 /* LoadingFailureDemo.swift in Sources */,\n\t\t\t\tD1E612E22D75F9AC00DACD51 /* ProgressiveJPEGDemo.swift in Sources */,\n\t\t\t\t4B92FE5625FF906B00473088 /* AutoSizingTableViewController.swift in Sources */,\n\t\t\t\tD1F78A642589F17200930759 /* ListDemo.swift in Sources */,\n\t\t\t\tD198F41E25EDC11500C53E0D /* LazyVStackDemo.swift in Sources */,\n\t\t\t\tD1E4CF5421BACBA6004D029D /* ImageDataProviderCollectionViewController.swift in Sources */,\n\t\t\t\t4B9AD88F26480D3B0086A261 /* OrientationImagesViewController.swift in Sources */,\n\t\t\t\tD1FAB06F21A853E600908910 /* HighResolutionCollectionViewController.swift in Sources */,\n\t\t\t\tD1F78A652589F17200930759 /* MainView.swift in Sources */,\n\t\t\t\t4B779C8526743C2800FF9C1E /* GeometryReaderDemo.swift in Sources */,\n\t\t\t\tD1F78A5F2589F0AA00930759 /* SwiftUIViewController.swift in Sources */,\n\t\t\t\t0784D0992F17E41700EB2A07 /* PhotosPickerDemo.swift in Sources */,\n\t\t\t\tD12E0C951C47F91800AC98AD /* AppDelegate.swift in Sources */,\n\t\t\t\tD1F06F3321AA4292000B1C38 /* DetailImageViewController.swift in Sources */,\n\t\t\t\tD198F42225EDC4B900C53E0D /* GridDemo.swift in Sources */,\n\t\t\t\t4B1C7A3D21A256E300CE9D31 /* InfinityCollectionViewController.swift in Sources */,\n\t\t\t\t078DCB512BCFEFB40008114E /* PHPickerResultViewController.swift in Sources */,\n\t\t\t\tD1EDF7422C9F01270017FFA5 /* Issue2295View.swift in Sources */,\n\t\t\t\tD1A1CCA321A1879600263AD8 /* MainViewController.swift in Sources */,\n\t\t\t\tD16AAF282D5247CF00E7F764 /* Issue2352View.swift in Sources */,\n\t\t\t\tD12F67682CB10AE000AB63AB /* LivePhotoViewController.swift in Sources */,\n\t\t\t\t4BC0ED4A29A6EE78003E9CD1 /* Issue2035View.swift in Sources */,\n\t\t\t\tD1F06F3721AAEACF000B1C38 /* GIFViewController.swift in Sources */,\n\t\t\t\t4B120CA726B91BB70060B092 /* TransitionViewDemo.swift in Sources */,\n\t\t\t\t769F07F126298E2E00610767 /* GIFHeavyViewController.swift 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\tD1679A481C4E78B20020FD12 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = D1679A441C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension */;\n\t\t\ttargetProxy = D1679A471C4E78B20020FD12 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t277EAE8A2045B39C00547CD3 /* Interface.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t277EAE8B2045B39C00547CD3 /* Base */,\n\t\t\t);\n\t\t\tname = Interface.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t4BCCF3381D5B02F8003387C2 /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t4BCCF3391D5B02F8003387C2 /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12E0C8F1C47F91800AC98AD /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\tD12E0C901C47F91800AC98AD /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12E0C9F1C47F92200AC98AD /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\tD12E0CA01C47F92200AC98AD /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t4B2944621C3D03880088C3E7 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Mac Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tDEVELOPMENT_TEAM = A4YJ9MRZ66;\n\t\t\t\tENABLE_HARDENED_RUNTIME = YES;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-macOS-Demo/Info.plist\";\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\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.Kingfisher-OSX-Demo\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSDKROOT = macosx;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t4B2944631C3D03880088C3E7 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Mac Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tDEVELOPMENT_TEAM = A4YJ9MRZ66;\n\t\t\t\tENABLE_HARDENED_RUNTIME = YES;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-macOS-Demo/Info.plist\";\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\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.Kingfisher-OSX-Demo\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD13F49CE1BEDA53F00CE335D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = \"App Icon & Top Shelf Image\";\n\t\t\t\tASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=appletvos*]\" = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-tvOS-Demo/Info.plist\";\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\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.Kingfisher-tvOS-Demo\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSDKROOT = appletvos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 3;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD13F49CF1BEDA53F00CE335D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = \"App Icon & Top Shelf Image\";\n\t\t\t\tASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=appletvos*]\" = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-tvOS-Demo/Info.plist\";\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\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.Kingfisher-tvOS-Demo\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSDKROOT = appletvos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = 3;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD1679A551C4E78B20020FD12 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-watchOS-Demo Extension/Info.plist\";\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\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.Kingfisher-Demo.watchkitapp.watchkitextension\";\n\t\t\t\tPRODUCT_NAME = \"${TARGET_NAME}\";\n\t\t\t\tSDKROOT = watchos;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = 4;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD1679A561C4E78B20020FD12 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-watchOS-Demo Extension/Info.plist\";\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\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.Kingfisher-Demo.watchkitapp.watchkitextension\";\n\t\t\t\tPRODUCT_NAME = \"${TARGET_NAME}\";\n\t\t\t\tSDKROOT = watchos;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = 4;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD1679A591C4E78B20020FD12 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=watchos*]\" = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tIBSC_MODULE = Kingfisher_watchOS_Demo_Extension;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-watchOS-Demo/Info.plist\";\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\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.Kingfisher-Demo.watchkitapp\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSDKROOT = watchos;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 4;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD1679A5A1C4E78B20020FD12 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=watchos*]\" = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tIBSC_MODULE = Kingfisher_watchOS_Demo_Extension;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-watchOS-Demo/Info.plist\";\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\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.Kingfisher-Demo.watchkitapp\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSDKROOT = watchos;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 4;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD1ED2D281AD2CFA600CFC3EB /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = 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_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\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = 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_SYMBOLS_PRIVATE_EXTERN = NO;\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\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_STRICT_CONCURRENCY = complete;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 6.0;\n\t\t\t\tXROS_DEPLOYMENT_TARGET = 1.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD1ED2D291AD2CFA600CFC3EB /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = 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_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\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = 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\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tSWIFT_STRICT_CONCURRENCY = complete;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 6.0;\n\t\t\t\tXROS_DEPLOYMENT_TARGET = 1.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD1ED2D2B1AD2CFA600CFC3EB /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = \"Kingfisher-Demo.entitlements\";\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tDERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;\n\t\t\t\tDEVELOPMENT_TEAM = A4YJ9MRZ66;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-Demo/Info.plist\";\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\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator xros xrsimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = YES;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2,7\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD1ED2D2C1AD2CFA600CFC3EB /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = \"Kingfisher-Demo.entitlements\";\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tDERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;\n\t\t\t\tDEVELOPMENT_TEAM = A4YJ9MRZ66;\n\t\t\t\tINFOPLIST_FILE = \"Demo/Kingfisher-Demo/Info.plist\";\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\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator xros xrsimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = YES;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2,7\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t4B2944611C3D03880088C3E7 /* Build configuration list for PBXNativeTarget \"Kingfisher-macOS-Demo\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t4B2944621C3D03880088C3E7 /* Debug */,\n\t\t\t\t4B2944631C3D03880088C3E7 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD13F49D01BEDA53F00CE335D /* Build configuration list for PBXNativeTarget \"Kingfisher-tvOS-Demo\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD13F49CE1BEDA53F00CE335D /* Debug */,\n\t\t\t\tD13F49CF1BEDA53F00CE335D /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD1679A541C4E78B20020FD12 /* Build configuration list for PBXNativeTarget \"Kingfisher-watchOS-Demo Extension\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD1679A551C4E78B20020FD12 /* Debug */,\n\t\t\t\tD1679A561C4E78B20020FD12 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD1679A581C4E78B20020FD12 /* Build configuration list for PBXNativeTarget \"Kingfisher-watchOS-Demo\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD1679A591C4E78B20020FD12 /* Debug */,\n\t\t\t\tD1679A5A1C4E78B20020FD12 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject \"Kingfisher-Demo\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD1ED2D281AD2CFA600CFC3EB /* Debug */,\n\t\t\t\tD1ED2D291AD2CFA600CFC3EB /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD1ED2D2A1AD2CFA600CFC3EB /* Build configuration list for PBXNativeTarget \"Kingfisher-Demo\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD1ED2D2B1AD2CFA600CFC3EB /* Debug */,\n\t\t\t\tD1ED2D2C1AD2CFA600CFC3EB /* 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 = D1ED2D031AD2CFA600CFC3EB /* Project object */;\n}\n"
  },
  {
    "path": "Demo/Kingfisher-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:Kingfisher-Demo.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "Demo/Kingfisher-Demo.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": "Demo/Kingfisher-Demo.xcodeproj/xcshareddata/xcschemes/Kingfisher-Demo.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1640\"\n   version = \"1.7\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\"\n      buildArchitectures = \"Automatic\">\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 = \"D1ED2D0A1AD2CFA600CFC3EB\"\n               BuildableName = \"Kingfisher-Demo.app\"\n               BlueprintName = \"Kingfisher-Demo\"\n               ReferencedContainer = \"container:Kingfisher-Demo.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      shouldAutocreateTestPlan = \"YES\">\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 = \"D1ED2D0A1AD2CFA600CFC3EB\"\n            BuildableName = \"Kingfisher-Demo.app\"\n            BlueprintName = \"Kingfisher-Demo\"\n            ReferencedContainer = \"container:Kingfisher-Demo.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 = \"D1ED2D0A1AD2CFA600CFC3EB\"\n            BuildableName = \"Kingfisher-Demo.app\"\n            BlueprintName = \"Kingfisher-Demo\"\n            ReferencedContainer = \"container:Kingfisher-Demo.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": "Gemfile",
    "content": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngem \"base64\", \"~> 0.2.0\"\ngem \"fastlane\"\ngem \"cocoapods\"\n"
  },
  {
    "path": "Kingfisher.json",
    "content": "{\n  \"8.3.2\": \"https://github.com/onevcat/Kingfisher/releases/download/8.3.2/Kingfisher-8.3.2.xcframework.zip\",\n  \"8.3.1\": \"https://github.com/onevcat/Kingfisher/releases/download/8.3.1/Kingfisher-8.3.1.xcframework.zip\",\n  \"8.3.0\": \"https://github.com/onevcat/Kingfisher/releases/download/8.3.0/Kingfisher-8.3.0.xcframework.zip\"\n}\n"
  },
  {
    "path": "Kingfisher.podspec",
    "content": "Pod::Spec.new do |s|\n\n  s.name         = \"Kingfisher\"\n  s.version      = \"8.8.0\"\n  s.summary      = \"A lightweight and pure Swift implemented library for downloading and cacheing image from the web.\"\n\n  s.description  = <<-DESC\n                   Kingfisher is a powerful and pure Swift implemented library for downloading and cacheing image from the web. It provides you a chance to use pure Swift alternation in your next app.\n\n                   * Everything in Kingfisher goes asynchronously, not only downloading, but also caching. That means you can never worry about blocking your UI thread.\n                   * Multiple-layer cache. Downloaded image will be cached in both memory and disk. So there is no need to download it again and this could boost your app dramatically.\n                   * Cache management. You can set the max duration or size the cache could take. And the cache will also be cleaned automatically to prevent taking too much resource.\n                   * Modern framework. Kingfisher uses `NSURLSession` and the latest technology of GCD, which makes it a strong and swift framework. It also provides you easy APIs to use.\n                   * Cancellable processing task. You can cancel the downloading or retriving image process if it is not needed anymore.\n                   * Independent components. You can use the downloader or caching system separately. Or even create your own cache based on Kingfisher's code.\n                   * Options to decompress the image in background before render it, which could improve the UI performance.\n                   * A category over `UIImageView` for setting image from an url directly.\n                   DESC\n\n  s.homepage     = \"https://github.com/onevcat/Kingfisher\"\n  s.screenshots  = \"https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png\"\n\n  s.license      = { :type => \"MIT\", :file => \"LICENSE\" }\n\n  s.authors            = { \"onevcat\" => \"onevcat@gmail.com\" }\n  s.social_media_url   = \"https://github.com/onevcat\"\n\n  s.swift_versions = ['5.0']\n\n  s.ios.deployment_target = \"13.0\"\n  s.tvos.deployment_target = \"13.0\"\n  s.osx.deployment_target = \"10.15\"\n  s.watchos.deployment_target = \"6.0\"\n  s.visionos.deployment_target = \"1.0\"\n\n  s.source       = { :git => \"https://github.com/onevcat/Kingfisher.git\", :tag => s.version }\n  s.source_files  = [\"Sources/**/*.swift\"]\n  s.resource_bundles = {\"Kingfisher\" => [\"Sources/PrivacyInfo.xcprivacy\"]}\n  s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' }\n\n  s.requires_arc = true\n  s.frameworks = \"CFNetwork\", \"Accelerate\"\n  s.weak_frameworks = \"SwiftUI\", \"Combine\"\nend\n"
  },
  {
    "path": "Kingfisher.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t00A8E26E2E81B89600ABB84F /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A8E26D2E81B89600ABB84F /* NetworkMonitor.swift */; };\n\t\t07292245263B02F00089E810 /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07292244263B02F00089E810 /* KFAnimatedImage.swift */; };\n\t\t0784D09B2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0784D09A2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift */; };\n\t\t078DCB4F2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */; };\n\t\t22FDCE0E2700078B0044D11E /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */; };\n\t\t388F37382B4D9CDB0089705C /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388F37372B4D9CDB0089705C /* DisplayLink.swift */; };\n\t\t3ADE9AF92A73CD69009A86CA /* String+SHA256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */; };\n\t\t4B10480D216F157000300C61 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B10480C216F157000300C61 /* ImageDataProcessor.swift */; };\n\t\t4B46CC5F217449C600D90C4A /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC5E217449C600D90C4A /* MemoryStorage.swift */; };\n\t\t4B46CC64217449E000D90C4A /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC63217449E000D90C4A /* Storage.swift */; };\n\t\t4B46CC6921744AC500D90C4A /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC6821744AC500D90C4A /* DiskStorage.swift */; };\n\t\t4B8351C8217066580081EED8 /* StubHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8351C7217066580081EED8 /* StubHelpers.swift */; };\n\t\t4B8351CC217084660081EED8 /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8351CB217084660081EED8 /* Runtime.swift */; };\n\t\t4B88CEB02646C056009EBB41 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */; };\n\t\t4B88CEB22646C653009EBB41 /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */; };\n\t\t4B88CEB42646D0BF009EBB41 /* ImageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88CEB32646D0BF009EBB41 /* ImageContext.swift */; };\n\t\t4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */; };\n\t\t4B8E291C216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */; };\n\t\t4BA3BF1E228BCDD100909201 /* DataReceivingSideEffectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */; };\n\t\t4BCFF7A621990DB70055AAC4 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */; };\n\t\t4BCFF7AA219932390055AAC4 /* DiskStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCFF7A9219932390055AAC4 /* DiskStorageTests.swift */; };\n\t\t4BD821622189FC0C0084CC21 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */; };\n\t\t4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821662189FD330084CC21 /* SessionDataTask.swift */; };\n\t\t4BE688F722FD513100B11168 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */; };\n\t\t76FB4FD2262D773E006D15F8 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FB4FD1262D773E006D15F8 /* GraphicsContext.swift */; };\n\t\tC9286407228584EB00257182 /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9286406228584EB00257182 /* ImageProgressive.swift */; };\n\t\tD1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1132C9625919F69003E528D /* KFOptionsSetter.swift */; };\n\t\tD11D9B72245FA6F700C5A0AE /* RetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11D9B71245FA6F700C5A0AE /* RetryStrategy.swift */; };\n\t\tD12AB6C0215D2BB50013BA68 /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB69D215D2BB50013BA68 /* RequestModifier.swift */; };\n\t\tD12AB6C4215D2BB50013BA68 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB69E215D2BB50013BA68 /* Resource.swift */; };\n\t\tD12AB6C8215D2BB50013BA68 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB69F215D2BB50013BA68 /* ImageDownloader.swift */; };\n\t\tD12AB6CC215D2BB50013BA68 /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A0215D2BB50013BA68 /* ImageModifier.swift */; };\n\t\tD12AB6D0215D2BB50013BA68 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A1215D2BB50013BA68 /* ImagePrefetcher.swift */; };\n\t\tD12AB6D4215D2BB50013BA68 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A3215D2BB50013BA68 /* Image.swift */; };\n\t\tD12AB6D8215D2BB50013BA68 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A4215D2BB50013BA68 /* ImageTransition.swift */; };\n\t\tD12AB6DC215D2BB50013BA68 /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A5215D2BB50013BA68 /* ImageProcessor.swift */; };\n\t\tD12AB6E0215D2BB50013BA68 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A6215D2BB50013BA68 /* Filter.swift */; };\n\t\tD12AB6E4215D2BB50013BA68 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A7215D2BB50013BA68 /* Placeholder.swift */; };\n\t\tD12AB6E8215D2BB50013BA68 /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A8215D2BB50013BA68 /* GIFAnimatedImage.swift */; };\n\t\tD12AB6F4215D2BB50013BA68 /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */; };\n\t\tD12AB6FC215D2BB50013BA68 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */; };\n\t\tD12AB704215D2BB50013BA68 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B1215D2BB50013BA68 /* Kingfisher.swift */; };\n\t\tD12AB708215D2BB50013BA68 /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B2215D2BB50013BA68 /* KingfisherError.swift */; };\n\t\tD12AB70C215D2BB50013BA68 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B3215D2BB50013BA68 /* KingfisherManager.swift */; };\n\t\tD12AB710215D2BB50013BA68 /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B4215D2BB50013BA68 /* KingfisherOptionsInfo.swift */; };\n\t\tD12AB714215D2BB50013BA68 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B6215D2BB50013BA68 /* ImageCache.swift */; };\n\t\tD12AB718215D2BB50013BA68 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B7215D2BB50013BA68 /* CacheSerializer.swift */; };\n\t\tD12AB71C215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B8215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift */; };\n\t\tD12AB724215D2BB50013BA68 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BB215D2BB50013BA68 /* Box.swift */; };\n\t\tD12AB72C215D2BB50013BA68 /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BE215D2BB50013BA68 /* Indicator.swift */; };\n\t\tD12AB730215D2BB50013BA68 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BF215D2BB50013BA68 /* AnimatedImageView.swift */; };\n\t\tD12E0C4F1C47F23500AC98AD /* dancing-banana.gif in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C441C47F23500AC98AD /* dancing-banana.gif */; };\n\t\tD12E0C501C47F23500AC98AD /* ImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C451C47F23500AC98AD /* ImageCacheTests.swift */; };\n\t\tD12E0C511C47F23500AC98AD /* ImageDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C461C47F23500AC98AD /* ImageDownloaderTests.swift */; };\n\t\tD12E0C521C47F23500AC98AD /* ImageExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C471C47F23500AC98AD /* ImageExtensionTests.swift */; };\n\t\tD12E0C531C47F23500AC98AD /* ImageViewExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C481C47F23500AC98AD /* ImageViewExtensionTests.swift */; };\n\t\tD12E0C551C47F23500AC98AD /* KingfisherManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4A1C47F23500AC98AD /* KingfisherManagerTests.swift */; };\n\t\tD12E0C561C47F23500AC98AD /* KingfisherOptionsInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4B1C47F23500AC98AD /* KingfisherOptionsInfoTests.swift */; };\n\t\tD12E0C571C47F23500AC98AD /* KingfisherTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4C1C47F23500AC98AD /* KingfisherTestHelper.swift */; };\n\t\tD12E0C581C47F23500AC98AD /* UIButtonExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */; };\n\t\tD12EB83C24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */; };\n\t\tD12F67602CAC2DBF00AB63AB /* ImageDownloader+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F675F2CAC2DB700AB63AB /* ImageDownloader+LivePhoto.swift */; };\n\t\tD12F67622CAC32BF00AB63AB /* KingfisherManager+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */; };\n\t\tD12F67642CAC330A00AB63AB /* LivePhotoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67632CAC330600AB63AB /* LivePhotoSource.swift */; };\n\t\tD12F67662CB022FC00AB63AB /* PHLivePhotoView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67652CB022EB00AB63AB /* PHLivePhotoView+Kingfisher.swift */; };\n\t\tD13646742165A1A100A33652 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = D13646732165A1A100A33652 /* Result.swift */; };\n\t\tD16CC3D624E02E9500F1A515 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */; };\n\t\tD16FEA3A23078C63006E67D5 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = D16FE9F623078C63006E67D5 /* LICENSE */; };\n\t\tD16FEA3B23078C63006E67D5 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = D16FE9F723078C63006E67D5 /* README.md */; };\n\t\tD16FEA3C23078C63006E67D5 /* LSStubRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FE9FA23078C63006E67D5 /* LSStubRequest.m */; };\n\t\tD16FEA3D23078C63006E67D5 /* LSStubResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FE9FB23078C63006E67D5 /* LSStubResponse.m */; };\n\t\tD16FEA3E23078C63006E67D5 /* LSNocilla.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FE9FE23078C63006E67D5 /* LSNocilla.m */; };\n\t\tD16FEA3F23078C63006E67D5 /* LSHTTPRequestDiff.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0023078C63006E67D5 /* LSHTTPRequestDiff.m */; };\n\t\tD16FEA4023078C63006E67D5 /* LSHTTPClientHook.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0423078C63006E67D5 /* LSHTTPClientHook.m */; };\n\t\tD16FEA4123078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0723078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m */; };\n\t\tD16FEA4223078C63006E67D5 /* LSNSURLHook.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0C23078C63006E67D5 /* LSNSURLHook.m */; };\n\t\tD16FEA4323078C63006E67D5 /* NSURLRequest+DSL.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0D23078C63006E67D5 /* NSURLRequest+DSL.m */; };\n\t\tD16FEA4423078C63006E67D5 /* LSHTTPStubURLProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0E23078C63006E67D5 /* LSHTTPStubURLProtocol.m */; };\n\t\tD16FEA4523078C63006E67D5 /* ASIHTTPRequestStub.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA1123078C63006E67D5 /* ASIHTTPRequestStub.m */; };\n\t\tD16FEA4623078C63006E67D5 /* LSASIHTTPRequestHook.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA1323078C63006E67D5 /* LSASIHTTPRequestHook.m */; };\n\t\tD16FEA4723078C63006E67D5 /* LSASIHTTPRequestAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA1423078C63006E67D5 /* LSASIHTTPRequestAdapter.m */; };\n\t\tD16FEA4823078C63006E67D5 /* LSNSURLSessionHook.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA1823078C63006E67D5 /* LSNSURLSessionHook.m */; };\n\t\tD16FEA4923078C63006E67D5 /* NSRegularExpression+Matcheable.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2223078C63006E67D5 /* NSRegularExpression+Matcheable.m */; };\n\t\tD16FEA4A23078C63006E67D5 /* NSString+Matcheable.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2423078C63006E67D5 /* NSString+Matcheable.m */; };\n\t\tD16FEA4B23078C63006E67D5 /* NSData+Matcheable.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2523078C63006E67D5 /* NSData+Matcheable.m */; };\n\t\tD16FEA4C23078C63006E67D5 /* LSDataMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2623078C63006E67D5 /* LSDataMatcher.m */; };\n\t\tD16FEA4D23078C63006E67D5 /* LSMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2723078C63006E67D5 /* LSMatcher.m */; };\n\t\tD16FEA4E23078C63006E67D5 /* LSRegexMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2823078C63006E67D5 /* LSRegexMatcher.m */; };\n\t\tD16FEA4F23078C63006E67D5 /* LSStringMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2A23078C63006E67D5 /* LSStringMatcher.m */; };\n\t\tD16FEA5023078C63006E67D5 /* NSString+Nocilla.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3023078C63006E67D5 /* NSString+Nocilla.m */; };\n\t\tD16FEA5123078C63006E67D5 /* NSData+Nocilla.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3123078C63006E67D5 /* NSData+Nocilla.m */; };\n\t\tD16FEA5223078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3423078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m */; };\n\t\tD16FEA5323078C63006E67D5 /* LSStubResponseDSL.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3623078C63006E67D5 /* LSStubResponseDSL.m */; };\n\t\tD16FEA5423078C63006E67D5 /* LSStubRequestDSL.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3723078C63006E67D5 /* LSStubRequestDSL.m */; };\n\t\tD16FEA5523079707006E67D5 /* NSButtonExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */; };\n\t\tD1839845216E333E003927D3 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1839844216E333E003927D3 /* Delegate.swift */; };\n\t\tD186696D21834261002B502E /* ImageDrawingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D186696C21834261002B502E /* ImageDrawingTests.swift */; };\n\t\tD1889534258F7649003B73BE /* KFImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1889533258F7648003B73BE /* KFImageOptions.swift */; };\n\t\tD18B3222251852E100662F63 /* KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18B3221251852E100662F63 /* KF.swift */; };\n\t\tD1A1CC9A219FAB4B00263AD8 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CC99219FAB4B00263AD8 /* Source.swift */; };\n\t\tD1A1CC9F21A0F98600263AD8 /* ImageDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CC9E21A0F98600263AD8 /* ImageDataProviderTests.swift */; };\n\t\tD1A37BDE215D34E8009B39B7 /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A37BDD215D34E8009B39B7 /* ImageDrawing.swift */; };\n\t\tD1A37BE3215D359F009B39B7 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A37BE2215D359F009B39B7 /* ImageFormat.swift */; };\n\t\tD1A37BE8215D365A009B39B7 /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A37BE7215D365A009B39B7 /* ExtensionHelpers.swift */; };\n\t\tD1A37BF2215D3850009B39B7 /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A37BF1215D3850009B39B7 /* SizeExtensions.swift */; };\n\t\tD1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F7607523097532000C5269 /* ImageBinder.swift */; };\n\t\tD1AEB09725890DEA008556DF /* KFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F7607623097532000C5269 /* KFImage.swift */; };\n\t\tD1BA781D2174D07800C69D7B /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BA781C2174D07800C69D7B /* CallbackQueue.swift */; };\n\t\tD1BFED95222ACC6B009330C8 /* ImageProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BFED94222ACC6B009330C8 /* ImageProcessorTests.swift */; };\n\t\tD1D2C32A1C70A3230018F2F9 /* single-frame.gif in Resources */ = {isa = PBXBuildFile; fileRef = D1D2C3291C70A3230018F2F9 /* single-frame.gif */; };\n\t\tD1D550D52AEB9E8700AAD79D /* PrivacyInfo.xcprivacy in CopyFiles */ = {isa = PBXBuildFile; fileRef = D1C04A3E2A45D20500B3775F /* PrivacyInfo.xcprivacy */; };\n\t\tD1DC4B411D60996D00DFDFAA /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DC4B401D60996D00DFDFAA /* StringExtensionTests.swift */; };\n\t\tD1E564412199C21E0057AAE3 /* StorageExpirationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E564402199C21E0057AAE3 /* StorageExpirationTests.swift */; };\n\t\tD1E56445219B16330057AAE3 /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E56444219B16330057AAE3 /* ImageDataProvider.swift */; };\n\t\tD1ED2D401AD2D09F00CFC3EB /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */; };\n\t\tD1F1F6FF24625EC600910725 /* RetryStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F1F6FE24625EC600910725 /* RetryStrategyTests.swift */; };\n\t\tD1F66CC12CB2CF2E004959F3 /* LivePhotoSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F66CC02CB2CF2D004959F3 /* LivePhotoSourceTests.swift */; };\n\t\tD8FCF6A821C5A0E500F9ABC0 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */; };\n\t\tD9638BA61C7DC71F0046523D /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */; };\n\t\tE9E3ED8B2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */; };\n\t\tF39B68C82E33AC2A00404B02 /* NetworkMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39B68C72E33AC2A00404B02 /* NetworkMetrics.swift */; };\n\t\tF72CE9CE1FCF17ED00CC522A /* ImageModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */; };\n\t\t38D5D3A32C5C757E00BF1D01 /* PixelFormatDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5D3A42C5C757E00BF1D01 /* PixelFormatDecodingTests.swift */; };\n\t\t38D5D3C12C5C7A1800BF1D01 /* gradient-8b-srgb-opaque.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3AC2C5C784700BF1D01 /* gradient-8b-srgb-opaque.png */; };\n\t\t38D5D3C22C5C7A1800BF1D01 /* gradient-8b-srgb-alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3AD2C5C784700BF1D01 /* gradient-8b-srgb-alpha.png */; };\n\t\t38D5D3C32C5C7A1800BF1D01 /* gradient-8b-displayp3-alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3AE2C5C784700BF1D01 /* gradient-8b-displayp3-alpha.png */; };\n\t\t38D5D3C42C5C7A1800BF1D01 /* gradient-8b-gray.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3AF2C5C784700BF1D01 /* gradient-8b-gray.png */; };\n\t\t38D5D3C52C5C7A1800BF1D01 /* gradient-10b-srgb-opaque.heic in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B02C5C784700BF1D01 /* gradient-10b-srgb-opaque.heic */; };\n\t\t38D5D3C62C5C7A1800BF1D01 /* gradient-10b-srgb-alpha.heic in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B12C5C784700BF1D01 /* gradient-10b-srgb-alpha.heic */; };\n\t\t38D5D3C72C5C7A1800BF1D01 /* gradient-10b-displayp3-alpha.heic in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B22C5C784700BF1D01 /* gradient-10b-displayp3-alpha.heic */; };\n\t\t38D5D3C82C5C7A1800BF1D01 /* gradient-16b-srgb-alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B32C5C784700BF1D01 /* gradient-16b-srgb-alpha.png */; };\n\t\t38D5D3C92C5C7A1800BF1D01 /* gradient-16b-gray.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B42C5C784700BF1D01 /* gradient-16b-gray.png */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\tD1ED2D411AD2D09F00CFC3EB /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = D1ED2D031AD2CFA600CFC3EB /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = D1ED2D341AD2D09F00CFC3EB;\n\t\t\tremoteInfo = Kingfisher;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\tD1D550D42AEB9E7300AAD79D /* CopyFiles */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 7;\n\t\t\tfiles = (\n\t\t\t\tD1D550D52AEB9E8700AAD79D /* PrivacyInfo.xcprivacy 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\t00A8E26D2E81B89600ABB84F /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = \"<group>\"; };\n\t\t07292244263B02F00089E810 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFAnimatedImage.swift; sourceTree = \"<group>\"; };\n\t\t0784D09A2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosPickerItemImageDataProvider.swift; sourceTree = \"<group>\"; };\n\t\t078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultImageDataProvider.swift; sourceTree = \"<group>\"; };\n\t\t185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSButtonExtensionTests.swift; sourceTree = \"<group>\"; };\n\t\t22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CPListItem+Kingfisher.swift\"; sourceTree = \"<group>\"; };\n\t\t388F37372B4D9CDB0089705C /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = \"<group>\"; };\n\t\t3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"String+SHA256.swift\"; sourceTree = \"<group>\"; };\n\t\t4B10480C216F157000300C61 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataProcessor.swift; sourceTree = \"<group>\"; };\n\t\t4B164ACE1B8D554200768EC6 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; };\n\t\t4B3E714D1B01FEB200F5AAED /* WatchKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WatchKit.framework; path = System/Library/Frameworks/WatchKit.framework; sourceTree = SDKROOT; };\n\t\t4B46CC5E217449C600D90C4A /* MemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorage.swift; sourceTree = \"<group>\"; };\n\t\t4B46CC63217449E000D90C4A /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = \"<group>\"; };\n\t\t4B46CC6821744AC500D90C4A /* DiskStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskStorage.swift; sourceTree = \"<group>\"; };\n\t\t4B8351C7217066580081EED8 /* StubHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubHelpers.swift; sourceTree = \"<group>\"; };\n\t\t4B8351CB217084660081EED8 /* Runtime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Runtime.swift; sourceTree = \"<group>\"; };\n\t\t4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageProtocol.swift; sourceTree = \"<group>\"; };\n\t\t4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageRenderer.swift; sourceTree = \"<group>\"; };\n\t\t4B88CEB32646D0BF009EBB41 /* ImageContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContext.swift; sourceTree = \"<group>\"; };\n\t\t4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloaderDelegate.swift; sourceTree = \"<group>\"; };\n\t\t4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationChallengeResponsable.swift; sourceTree = \"<group>\"; };\n\t\t4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataReceivingSideEffectTests.swift; sourceTree = \"<group>\"; };\n\t\t38D5D3A42C5C757E00BF1D01 /* PixelFormatDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFormatDecodingTests.swift; sourceTree = \"<group>\"; };\n\t\t4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorageTests.swift; sourceTree = \"<group>\"; };\n\t\t4BCFF7A9219932390055AAC4 /* DiskStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskStorageTests.swift; sourceTree = \"<group>\"; };\n\t\t4BD821612189FC0C0084CC21 /* SessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDelegate.swift; sourceTree = \"<group>\"; };\n\t\t4BD821662189FD330084CC21 /* SessionDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDataTask.swift; sourceTree = \"<group>\"; };\n\t\t38D5D3AC2C5C784700BF1D01 /* gradient-8b-srgb-opaque.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-8b-srgb-opaque.png; sourceTree = \"<group>\"; };\n\t\t38D5D3AD2C5C784700BF1D01 /* gradient-8b-srgb-alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-8b-srgb-alpha.png; sourceTree = \"<group>\"; };\n\t\t38D5D3AE2C5C784700BF1D01 /* gradient-8b-displayp3-alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-8b-displayp3-alpha.png; sourceTree = \"<group>\"; };\n\t\t38D5D3AF2C5C784700BF1D01 /* gradient-8b-gray.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-8b-gray.png; sourceTree = \"<group>\"; };\n\t\t38D5D3B02C5C784700BF1D01 /* gradient-10b-srgb-opaque.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = gradient-10b-srgb-opaque.heic; sourceTree = \"<group>\"; };\n\t\t38D5D3B12C5C784700BF1D01 /* gradient-10b-srgb-alpha.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = gradient-10b-srgb-alpha.heic; sourceTree = \"<group>\"; };\n\t\t38D5D3B22C5C784700BF1D01 /* gradient-10b-displayp3-alpha.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = gradient-10b-displayp3-alpha.heic; sourceTree = \"<group>\"; };\n\t\t38D5D3B32C5C784700BF1D01 /* gradient-16b-srgb-alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-16b-srgb-alpha.png; sourceTree = \"<group>\"; };\n\t\t38D5D3B42C5C784700BF1D01 /* gradient-16b-gray.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-16b-gray.png; sourceTree = \"<group>\"; };\n\t\t76FB4FD1262D773E006D15F8 /* GraphicsContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicsContext.swift; sourceTree = \"<group>\"; };\n\t\tC9286406228584EB00257182 /* ImageProgressive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProgressive.swift; sourceTree = \"<group>\"; };\n\t\tC959EEE7228940FE00467A10 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };\n\t\tD1132C9625919F69003E528D /* KFOptionsSetter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFOptionsSetter.swift; sourceTree = \"<group>\"; };\n\t\tD11D9B71245FA6F700C5A0AE /* RetryStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryStrategy.swift; sourceTree = \"<group>\"; };\n\t\tD12AB69D215D2BB50013BA68 /* RequestModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestModifier.swift; sourceTree = \"<group>\"; };\n\t\tD12AB69E215D2BB50013BA68 /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = \"<group>\"; };\n\t\tD12AB69F215D2BB50013BA68 /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A0215D2BB50013BA68 /* ImageModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageModifier.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A1215D2BB50013BA68 /* ImagePrefetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcher.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A3215D2BB50013BA68 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A4215D2BB50013BA68 /* ImageTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageTransition.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A5215D2BB50013BA68 /* ImageProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A6215D2BB50013BA68 /* Filter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A7215D2BB50013BA68 /* Placeholder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Placeholder.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A8215D2BB50013BA68 /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GIFAnimatedImage.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6A9215D2BB50013BA68 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tD12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"ImageView+Kingfisher.swift\"; sourceTree = \"<group>\"; };\n\t\tD12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"NSButton+Kingfisher.swift\"; sourceTree = \"<group>\"; };\n\t\tD12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"UIButton+Kingfisher.swift\"; sourceTree = \"<group>\"; };\n\t\tD12AB6B1215D2BB50013BA68 /* Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Kingfisher.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6B2215D2BB50013BA68 /* KingfisherError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherError.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6B3215D2BB50013BA68 /* KingfisherManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherManager.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6B4215D2BB50013BA68 /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherOptionsInfo.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6B6215D2BB50013BA68 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6B7215D2BB50013BA68 /* CacheSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheSerializer.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6B8215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormatIndicatedCacheSerializer.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6BB215D2BB50013BA68 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6BE215D2BB50013BA68 /* Indicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Indicator.swift; sourceTree = \"<group>\"; };\n\t\tD12AB6BF215D2BB50013BA68 /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C441C47F23500AC98AD /* dancing-banana.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = \"dancing-banana.gif\"; sourceTree = \"<group>\"; };\n\t\tD12E0C451C47F23500AC98AD /* ImageCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheTests.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C461C47F23500AC98AD /* ImageDownloaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloaderTests.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C471C47F23500AC98AD /* ImageExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageExtensionTests.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C481C47F23500AC98AD /* ImageViewExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewExtensionTests.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C491C47F23500AC98AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tD12E0C4A1C47F23500AC98AD /* KingfisherManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherManagerTests.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C4B1C47F23500AC98AD /* KingfisherOptionsInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherOptionsInfoTests.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C4C1C47F23500AC98AD /* KingfisherTestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherTestHelper.swift; sourceTree = \"<group>\"; };\n\t\tD12E0C4D1C47F23500AC98AD /* KingfisherTests-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = \"KingfisherTests-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\tD12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensionTests.swift; sourceTree = \"<group>\"; };\n\t\tD12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"NSTextAttachment+Kingfisher.swift\"; sourceTree = \"<group>\"; };\n\t\tD12F675F2CAC2DB700AB63AB /* ImageDownloader+LivePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ImageDownloader+LivePhoto.swift\"; sourceTree = \"<group>\"; };\n\t\tD12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"KingfisherManager+LivePhoto.swift\"; sourceTree = \"<group>\"; };\n\t\tD12F67632CAC330600AB63AB /* LivePhotoSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoSource.swift; sourceTree = \"<group>\"; };\n\t\tD12F67652CB022EB00AB63AB /* PHLivePhotoView+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PHLivePhotoView+Kingfisher.swift\"; sourceTree = \"<group>\"; };\n\t\tD1356CEA2B273AEC009554C8 /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = \"<group>\"; };\n\t\tD13646732165A1A100A33652 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = \"<group>\"; };\n\t\tD16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVAssetImageDataProvider.swift; sourceTree = \"<group>\"; };\n\t\tD16FE9F623078C63006E67D5 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = \"<group>\"; };\n\t\tD16FE9F723078C63006E67D5 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = \"<group>\"; };\n\t\tD16FE9FA23078C63006E67D5 /* LSStubRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStubRequest.m; sourceTree = \"<group>\"; };\n\t\tD16FE9FB23078C63006E67D5 /* LSStubResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStubResponse.m; sourceTree = \"<group>\"; };\n\t\tD16FE9FC23078C63006E67D5 /* LSStubResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStubResponse.h; sourceTree = \"<group>\"; };\n\t\tD16FE9FD23078C63006E67D5 /* LSStubRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStubRequest.h; sourceTree = \"<group>\"; };\n\t\tD16FE9FE23078C63006E67D5 /* LSNocilla.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSNocilla.m; sourceTree = \"<group>\"; };\n\t\tD16FEA0023078C63006E67D5 /* LSHTTPRequestDiff.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSHTTPRequestDiff.m; sourceTree = \"<group>\"; };\n\t\tD16FEA0123078C63006E67D5 /* LSHTTPRequestDiff.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPRequestDiff.h; sourceTree = \"<group>\"; };\n\t\tD16FEA0223078C63006E67D5 /* Nocilla.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Nocilla.h; sourceTree = \"<group>\"; };\n\t\tD16FEA0423078C63006E67D5 /* LSHTTPClientHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSHTTPClientHook.m; sourceTree = \"<group>\"; };\n\t\tD16FEA0523078C63006E67D5 /* LSHTTPClientHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPClientHook.h; sourceTree = \"<group>\"; };\n\t\tD16FEA0723078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = \"NSURLRequest+LSHTTPRequest.m\"; sourceTree = \"<group>\"; };\n\t\tD16FEA0823078C63006E67D5 /* NSURLRequest+DSL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = \"NSURLRequest+DSL.h\"; sourceTree = \"<group>\"; };\n\t\tD16FEA0923078C63006E67D5 /* LSNSURLHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSNSURLHook.h; sourceTree = \"<group>\"; };\n\t\tD16FEA0A23078C63006E67D5 /* LSHTTPStubURLProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPStubURLProtocol.h; sourceTree = \"<group>\"; };\n\t\tD16FEA0B23078C63006E67D5 /* NSURLRequest+LSHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = \"NSURLRequest+LSHTTPRequest.h\"; sourceTree = \"<group>\"; };\n\t\tD16FEA0C23078C63006E67D5 /* LSNSURLHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSNSURLHook.m; sourceTree = \"<group>\"; };\n\t\tD16FEA0D23078C63006E67D5 /* NSURLRequest+DSL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = \"NSURLRequest+DSL.m\"; sourceTree = \"<group>\"; };\n\t\tD16FEA0E23078C63006E67D5 /* LSHTTPStubURLProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSHTTPStubURLProtocol.m; sourceTree = \"<group>\"; };\n\t\tD16FEA1023078C63006E67D5 /* LSASIHTTPRequestHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSASIHTTPRequestHook.h; sourceTree = \"<group>\"; };\n\t\tD16FEA1123078C63006E67D5 /* ASIHTTPRequestStub.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIHTTPRequestStub.m; sourceTree = \"<group>\"; };\n\t\tD16FEA1223078C63006E67D5 /* LSASIHTTPRequestAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSASIHTTPRequestAdapter.h; sourceTree = \"<group>\"; };\n\t\tD16FEA1323078C63006E67D5 /* LSASIHTTPRequestHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSASIHTTPRequestHook.m; sourceTree = \"<group>\"; };\n\t\tD16FEA1423078C63006E67D5 /* LSASIHTTPRequestAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSASIHTTPRequestAdapter.m; sourceTree = \"<group>\"; };\n\t\tD16FEA1523078C63006E67D5 /* ASIHTTPRequestStub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIHTTPRequestStub.h; sourceTree = \"<group>\"; };\n\t\tD16FEA1723078C63006E67D5 /* LSNSURLSessionHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSNSURLSessionHook.h; sourceTree = \"<group>\"; };\n\t\tD16FEA1823078C63006E67D5 /* LSNSURLSessionHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSNSURLSessionHook.m; sourceTree = \"<group>\"; };\n\t\tD16FEA1A23078C63006E67D5 /* LSHTTPBody.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPBody.h; sourceTree = \"<group>\"; };\n\t\tD16FEA1B23078C63006E67D5 /* LSHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPRequest.h; sourceTree = \"<group>\"; };\n\t\tD16FEA1C23078C63006E67D5 /* LSHTTPResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPResponse.h; sourceTree = \"<group>\"; };\n\t\tD16FEA1D23078C63006E67D5 /* LSNocilla.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSNocilla.h; sourceTree = \"<group>\"; };\n\t\tD16FEA1F23078C63006E67D5 /* LSMatcheable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSMatcheable.h; sourceTree = \"<group>\"; };\n\t\tD16FEA2023078C63006E67D5 /* LSMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSMatcher.h; sourceTree = \"<group>\"; };\n\t\tD16FEA2123078C63006E67D5 /* LSStringMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStringMatcher.h; sourceTree = \"<group>\"; };\n\t\tD16FEA2223078C63006E67D5 /* NSRegularExpression+Matcheable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = \"NSRegularExpression+Matcheable.m\"; sourceTree = \"<group>\"; };\n\t\tD16FEA2323078C63006E67D5 /* LSRegexMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSRegexMatcher.h; sourceTree = \"<group>\"; };\n\t\tD16FEA2423078C63006E67D5 /* NSString+Matcheable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = \"NSString+Matcheable.m\"; sourceTree = \"<group>\"; };\n\t\tD16FEA2523078C63006E67D5 /* NSData+Matcheable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = \"NSData+Matcheable.m\"; sourceTree = \"<group>\"; };\n\t\tD16FEA2623078C63006E67D5 /* LSDataMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSDataMatcher.m; sourceTree = \"<group>\"; };\n\t\tD16FEA2723078C63006E67D5 /* LSMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSMatcher.m; sourceTree = \"<group>\"; };\n\t\tD16FEA2823078C63006E67D5 /* LSRegexMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSRegexMatcher.m; sourceTree = \"<group>\"; };\n\t\tD16FEA2923078C63006E67D5 /* NSRegularExpression+Matcheable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = \"NSRegularExpression+Matcheable.h\"; sourceTree = \"<group>\"; };\n\t\tD16FEA2A23078C63006E67D5 /* LSStringMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStringMatcher.m; sourceTree = \"<group>\"; };\n\t\tD16FEA2B23078C63006E67D5 /* NSString+Matcheable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = \"NSString+Matcheable.h\"; sourceTree = \"<group>\"; };\n\t\tD16FEA2C23078C63006E67D5 /* LSDataMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSDataMatcher.h; sourceTree = \"<group>\"; };\n\t\tD16FEA2D23078C63006E67D5 /* NSData+Matcheable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = \"NSData+Matcheable.h\"; sourceTree = \"<group>\"; };\n\t\tD16FEA2F23078C63006E67D5 /* NSData+Nocilla.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = \"NSData+Nocilla.h\"; sourceTree = \"<group>\"; };\n\t\tD16FEA3023078C63006E67D5 /* NSString+Nocilla.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = \"NSString+Nocilla.m\"; sourceTree = \"<group>\"; };\n\t\tD16FEA3123078C63006E67D5 /* NSData+Nocilla.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = \"NSData+Nocilla.m\"; sourceTree = \"<group>\"; };\n\t\tD16FEA3223078C63006E67D5 /* NSString+Nocilla.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = \"NSString+Nocilla.h\"; sourceTree = \"<group>\"; };\n\t\tD16FEA3423078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSHTTPRequestDSLRepresentation.m; sourceTree = \"<group>\"; };\n\t\tD16FEA3523078C63006E67D5 /* LSStubRequestDSL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStubRequestDSL.h; sourceTree = \"<group>\"; };\n\t\tD16FEA3623078C63006E67D5 /* LSStubResponseDSL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStubResponseDSL.m; sourceTree = \"<group>\"; };\n\t\tD16FEA3723078C63006E67D5 /* LSStubRequestDSL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStubRequestDSL.m; sourceTree = \"<group>\"; };\n\t\tD16FEA3823078C63006E67D5 /* LSHTTPRequestDSLRepresentation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPRequestDSLRepresentation.h; sourceTree = \"<group>\"; };\n\t\tD16FEA3923078C63006E67D5 /* LSStubResponseDSL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStubResponseDSL.h; sourceTree = \"<group>\"; };\n\t\tD1839844216E333E003927D3 /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = \"<group>\"; };\n\t\tD186696C21834261002B502E /* ImageDrawingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDrawingTests.swift; sourceTree = \"<group>\"; };\n\t\tD1889533258F7648003B73BE /* KFImageOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageOptions.swift; sourceTree = \"<group>\"; };\n\t\tD18B3221251852E100662F63 /* KF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KF.swift; sourceTree = \"<group>\"; };\n\t\tD1A1CC99219FAB4B00263AD8 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = \"<group>\"; };\n\t\tD1A1CC9E21A0F98600263AD8 /* ImageDataProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataProviderTests.swift; sourceTree = \"<group>\"; };\n\t\tD1A37BDD215D34E8009B39B7 /* ImageDrawing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDrawing.swift; sourceTree = \"<group>\"; };\n\t\tD1A37BE2215D359F009B39B7 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = \"<group>\"; };\n\t\tD1A37BE7215D365A009B39B7 /* ExtensionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionHelpers.swift; sourceTree = \"<group>\"; };\n\t\tD1A37BF1215D3850009B39B7 /* SizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeExtensions.swift; sourceTree = \"<group>\"; };\n\t\tD1BA781C2174D07800C69D7B /* CallbackQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallbackQueue.swift; sourceTree = \"<group>\"; };\n\t\tD1BFED94222ACC6B009330C8 /* ImageProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessorTests.swift; sourceTree = \"<group>\"; };\n\t\tD1C04A3E2A45D20500B3775F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = \"<group>\"; };\n\t\tD1D2C3291C70A3230018F2F9 /* single-frame.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = \"single-frame.gif\"; sourceTree = \"<group>\"; };\n\t\tD1DC4B401D60996D00DFDFAA /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = \"<group>\"; };\n\t\tD1E564402199C21E0057AAE3 /* StorageExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageExpirationTests.swift; sourceTree = \"<group>\"; };\n\t\tD1E56444219B16330057AAE3 /* ImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = SOURCE_ROOT; };\n\t\tD1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD1ED2D3F1AD2D09F00CFC3EB /* KingfisherTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KingfisherTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD1F1F6FE24625EC600910725 /* RetryStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryStrategyTests.swift; sourceTree = \"<group>\"; };\n\t\tD1F66CC02CB2CF2D004959F3 /* LivePhotoSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoSourceTests.swift; sourceTree = \"<group>\"; };\n\t\tD1F7607523097532000C5269 /* ImageBinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageBinder.swift; sourceTree = \"<group>\"; };\n\t\tD1F7607623097532000C5269 /* KFImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KFImage.swift; sourceTree = \"<group>\"; };\n\t\tD8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedirectHandler.swift; sourceTree = \"<group>\"; };\n\t\tD9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcherTests.swift; sourceTree = \"<group>\"; };\n\t\tE9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"HasImageComponent+Kingfisher.swift\"; sourceTree = \"<group>\"; };\n\t\tF39B68C72E33AC2A00404B02 /* NetworkMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMetrics.swift; sourceTree = \"<group>\"; };\n\t\tF72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageModifierTests.swift; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\tD1ED2D311AD2D09F00CFC3EB /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1ED2D3C1AD2D09F00CFC3EB /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD1ED2D401AD2D09F00CFC3EB /* Kingfisher.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\t4B8351C6217066400081EED8 /* Utils */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t4B8351C7217066580081EED8 /* StubHelpers.swift */,\n\t\t\t);\n\t\t\tpath = Utils;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD10EC22C1C3D62E800A4211C /* Tests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FE9F423078C63006E67D5 /* Dependency */,\n\t\t\t\tD12E0C431C47F23500AC98AD /* KingfisherTests */,\n\t\t\t);\n\t\t\tname = Tests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12AB688215D2A280013BA68 /* Sources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD1356CEA2B273AEC009554C8 /* Documentation.docc */,\n\t\t\t\tD12AB6A9215D2BB50013BA68 /* Info.plist */,\n\t\t\t\tD1C04A3E2A45D20500B3775F /* PrivacyInfo.xcprivacy */,\n\t\t\t\tD12AB6B0215D2BB50013BA68 /* General */,\n\t\t\t\tD12AB6A2215D2BB50013BA68 /* Image */,\n\t\t\t\tD12AB69C215D2BB50013BA68 /* Networking */,\n\t\t\t\tD12AB6B5215D2BB50013BA68 /* Cache */,\n\t\t\t\tD12AB6BD215D2BB50013BA68 /* Views */,\n\t\t\t\tD12AB6AB215D2BB50013BA68 /* Extensions */,\n\t\t\t\tD12AB6B9215D2BB50013BA68 /* Utility */,\n\t\t\t\tD1F7607423097532000C5269 /* SwiftUI */,\n\t\t\t);\n\t\t\tpath = Sources;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12AB69C215D2BB50013BA68 /* Networking */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t00A8E26D2E81B89600ABB84F /* NetworkMonitor.swift */,\n\t\t\t\tD12AB69D215D2BB50013BA68 /* RequestModifier.swift */,\n\t\t\t\tD8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */,\n\t\t\t\tD12AB69F215D2BB50013BA68 /* ImageDownloader.swift */,\n\t\t\t\tD12F675F2CAC2DB700AB63AB /* ImageDownloader+LivePhoto.swift */,\n\t\t\t\t4BD821612189FC0C0084CC21 /* SessionDelegate.swift */,\n\t\t\t\t4BD821662189FD330084CC21 /* SessionDataTask.swift */,\n\t\t\t\t4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */,\n\t\t\t\tF39B68C72E33AC2A00404B02 /* NetworkMetrics.swift */,\n\t\t\t\t4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */,\n\t\t\t\t4B10480C216F157000300C61 /* ImageDataProcessor.swift */,\n\t\t\t\tD12AB6A0215D2BB50013BA68 /* ImageModifier.swift */,\n\t\t\t\tD12AB6A1215D2BB50013BA68 /* ImagePrefetcher.swift */,\n\t\t\t\tD11D9B71245FA6F700C5A0AE /* RetryStrategy.swift */,\n\t\t\t);\n\t\t\tpath = Networking;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12AB6A2215D2BB50013BA68 /* Image */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12AB6A3215D2BB50013BA68 /* Image.swift */,\n\t\t\t\tD1A37BDD215D34E8009B39B7 /* ImageDrawing.swift */,\n\t\t\t\tD1A37BE2215D359F009B39B7 /* ImageFormat.swift */,\n\t\t\t\tD12AB6A4215D2BB50013BA68 /* ImageTransition.swift */,\n\t\t\t\tD12AB6A5215D2BB50013BA68 /* ImageProcessor.swift */,\n\t\t\t\tC9286406228584EB00257182 /* ImageProgressive.swift */,\n\t\t\t\tD12AB6A6215D2BB50013BA68 /* Filter.swift */,\n\t\t\t\tD12AB6A7215D2BB50013BA68 /* Placeholder.swift */,\n\t\t\t\tD12AB6A8215D2BB50013BA68 /* GIFAnimatedImage.swift */,\n\t\t\t\t76FB4FD1262D773E006D15F8 /* GraphicsContext.swift */,\n\t\t\t);\n\t\t\tpath = Image;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12AB6AB215D2BB50013BA68 /* Extensions */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12F67652CB022EB00AB63AB /* PHLivePhotoView+Kingfisher.swift */,\n\t\t\t\tD12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */,\n\t\t\t\tD12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */,\n\t\t\t\tD12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */,\n\t\t\t\tE9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */,\n\t\t\t\tD12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */,\n\t\t\t\t22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */,\n\t\t\t);\n\t\t\tpath = Extensions;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12AB6B0215D2BB50013BA68 /* General */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD1A1CC98219FAB3500263AD8 /* ImageSource */,\n\t\t\t\tD12AB6B1215D2BB50013BA68 /* Kingfisher.swift */,\n\t\t\t\tD18B3221251852E100662F63 /* KF.swift */,\n\t\t\t\tD1132C9625919F69003E528D /* KFOptionsSetter.swift */,\n\t\t\t\tD12AB6B2215D2BB50013BA68 /* KingfisherError.swift */,\n\t\t\t\tD12AB6B3215D2BB50013BA68 /* KingfisherManager.swift */,\n\t\t\t\tD12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */,\n\t\t\t\tD12AB6B4215D2BB50013BA68 /* KingfisherOptionsInfo.swift */,\n\t\t\t);\n\t\t\tpath = General;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12AB6B5215D2BB50013BA68 /* Cache */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12AB6B6215D2BB50013BA68 /* ImageCache.swift */,\n\t\t\t\tD12AB6B7215D2BB50013BA68 /* CacheSerializer.swift */,\n\t\t\t\tD12AB6B8215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift */,\n\t\t\t\t4B46CC63217449E000D90C4A /* Storage.swift */,\n\t\t\t\t4B46CC5E217449C600D90C4A /* MemoryStorage.swift */,\n\t\t\t\t4B46CC6821744AC500D90C4A /* DiskStorage.swift */,\n\t\t\t);\n\t\t\tpath = Cache;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12AB6B9215D2BB50013BA68 /* Utility */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD13646732165A1A100A33652 /* Result.swift */,\n\t\t\t\tD12AB6BB215D2BB50013BA68 /* Box.swift */,\n\t\t\t\tD1A37BE7215D365A009B39B7 /* ExtensionHelpers.swift */,\n\t\t\t\tD1A37BF1215D3850009B39B7 /* SizeExtensions.swift */,\n\t\t\t\tD1839844216E333E003927D3 /* Delegate.swift */,\n\t\t\t\t4B8351CB217084660081EED8 /* Runtime.swift */,\n\t\t\t\tD1BA781C2174D07800C69D7B /* CallbackQueue.swift */,\n\t\t\t\t388F37372B4D9CDB0089705C /* DisplayLink.swift */,\n\t\t\t\t3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */,\n\t\t\t);\n\t\t\tpath = Utility;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12AB6BD215D2BB50013BA68 /* Views */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12AB6BE215D2BB50013BA68 /* Indicator.swift */,\n\t\t\t\tD12AB6BF215D2BB50013BA68 /* AnimatedImageView.swift */,\n\t\t\t);\n\t\t\tpath = Views;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD12E0C431C47F23500AC98AD /* KingfisherTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t38D5D3AB2C5C77F200BF1D01 /* PixelFormats */,\n\t\t\t\t4B8351C6217066400081EED8 /* Utils */,\n\t\t\t\tD12E0C491C47F23500AC98AD /* Info.plist */,\n\t\t\t\tD12E0C441C47F23500AC98AD /* dancing-banana.gif */,\n\t\t\t\tD1D2C3291C70A3230018F2F9 /* single-frame.gif */,\n\t\t\t\tD12E0C451C47F23500AC98AD /* ImageCacheTests.swift */,\n\t\t\t\tD1DC4B401D60996D00DFDFAA /* StringExtensionTests.swift */,\n\t\t\t\tD12E0C461C47F23500AC98AD /* ImageDownloaderTests.swift */,\n\t\t\t\tD12E0C471C47F23500AC98AD /* ImageExtensionTests.swift */,\n\t\t\t\t38D5D3A42C5C757E00BF1D01 /* PixelFormatDecodingTests.swift */,\n\t\t\t\tD186696C21834261002B502E /* ImageDrawingTests.swift */,\n\t\t\t\tD9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */,\n\t\t\t\tD12E0C481C47F23500AC98AD /* ImageViewExtensionTests.swift */,\n\t\t\t\tD12E0C4A1C47F23500AC98AD /* KingfisherManagerTests.swift */,\n\t\t\t\tD12E0C4B1C47F23500AC98AD /* KingfisherOptionsInfoTests.swift */,\n\t\t\t\tD12E0C4C1C47F23500AC98AD /* KingfisherTestHelper.swift */,\n\t\t\t\tF72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */,\n\t\t\t\tD12E0C4D1C47F23500AC98AD /* KingfisherTests-Bridging-Header.h */,\n\t\t\t\tD12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */,\n\t\t\t\t185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */,\n\t\t\t\t4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */,\n\t\t\t\t4BCFF7A9219932390055AAC4 /* DiskStorageTests.swift */,\n\t\t\t\tD1E564402199C21E0057AAE3 /* StorageExpirationTests.swift */,\n\t\t\t\tD1A1CC9E21A0F98600263AD8 /* ImageDataProviderTests.swift */,\n\t\t\t\tD1F66CC02CB2CF2D004959F3 /* LivePhotoSourceTests.swift */,\n\t\t\t\tD1BFED94222ACC6B009330C8 /* ImageProcessorTests.swift */,\n\t\t\t\t4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */,\n\t\t\t\tD1F1F6FE24625EC600910725 /* RetryStrategyTests.swift */,\n\t\t);\n\t\tname = KingfisherTests;\n\t\tpath = Tests/KingfisherTests;\n\t\tsourceTree = \"<group>\";\n\t};\n\t\t38D5D3AB2C5C77F200BF1D01 /* PixelFormats */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t38D5D3AC2C5C784700BF1D01 /* gradient-8b-srgb-opaque.png */,\n\t\t\t\t38D5D3AD2C5C784700BF1D01 /* gradient-8b-srgb-alpha.png */,\n\t\t\t\t38D5D3AE2C5C784700BF1D01 /* gradient-8b-displayp3-alpha.png */,\n\t\t\t\t38D5D3AF2C5C784700BF1D01 /* gradient-8b-gray.png */,\n\t\t\t\t38D5D3B02C5C784700BF1D01 /* gradient-10b-srgb-opaque.heic */,\n\t\t\t\t38D5D3B12C5C784700BF1D01 /* gradient-10b-srgb-alpha.heic */,\n\t\t\t\t38D5D3B22C5C784700BF1D01 /* gradient-10b-displayp3-alpha.heic */,\n\t\t\t\t38D5D3B32C5C784700BF1D01 /* gradient-16b-srgb-alpha.png */,\n\t\t\t\t38D5D3B42C5C784700BF1D01 /* gradient-16b-gray.png */,\n\t\t\t);\n\t\t\tpath = PixelFormats;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FE9F423078C63006E67D5 /* Dependency */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FE9F523078C63006E67D5 /* Nocilla */,\n\t\t\t);\n\t\t\tname = Dependency;\n\t\t\tpath = Tests/Dependency;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FE9F523078C63006E67D5 /* Nocilla */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FE9F623078C63006E67D5 /* LICENSE */,\n\t\t\t\tD16FE9F723078C63006E67D5 /* README.md */,\n\t\t\t\tD16FE9F823078C63006E67D5 /* Nocilla */,\n\t\t\t);\n\t\t\tpath = Nocilla;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FE9F823078C63006E67D5 /* Nocilla */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FE9F923078C63006E67D5 /* Stubs */,\n\t\t\t\tD16FE9FE23078C63006E67D5 /* LSNocilla.m */,\n\t\t\t\tD16FE9FF23078C63006E67D5 /* Diff */,\n\t\t\t\tD16FEA0223078C63006E67D5 /* Nocilla.h */,\n\t\t\t\tD16FEA0323078C63006E67D5 /* Hooks */,\n\t\t\t\tD16FEA1923078C63006E67D5 /* Model */,\n\t\t\t\tD16FEA1D23078C63006E67D5 /* LSNocilla.h */,\n\t\t\t\tD16FEA1E23078C63006E67D5 /* Matchers */,\n\t\t\t\tD16FEA2E23078C63006E67D5 /* Categories */,\n\t\t\t\tD16FEA3323078C63006E67D5 /* DSL */,\n\t\t\t);\n\t\t\tpath = Nocilla;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FE9F923078C63006E67D5 /* Stubs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FE9FA23078C63006E67D5 /* LSStubRequest.m */,\n\t\t\t\tD16FE9FB23078C63006E67D5 /* LSStubResponse.m */,\n\t\t\t\tD16FE9FC23078C63006E67D5 /* LSStubResponse.h */,\n\t\t\t\tD16FE9FD23078C63006E67D5 /* LSStubRequest.h */,\n\t\t\t);\n\t\t\tpath = Stubs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FE9FF23078C63006E67D5 /* Diff */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA0023078C63006E67D5 /* LSHTTPRequestDiff.m */,\n\t\t\t\tD16FEA0123078C63006E67D5 /* LSHTTPRequestDiff.h */,\n\t\t\t);\n\t\t\tpath = Diff;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FEA0323078C63006E67D5 /* Hooks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA0423078C63006E67D5 /* LSHTTPClientHook.m */,\n\t\t\t\tD16FEA0523078C63006E67D5 /* LSHTTPClientHook.h */,\n\t\t\t\tD16FEA0623078C63006E67D5 /* NSURLRequest */,\n\t\t\t\tD16FEA0F23078C63006E67D5 /* ASIHTTPRequest */,\n\t\t\t\tD16FEA1623078C63006E67D5 /* NSURLSession */,\n\t\t\t);\n\t\t\tpath = Hooks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FEA0623078C63006E67D5 /* NSURLRequest */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA0723078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m */,\n\t\t\t\tD16FEA0823078C63006E67D5 /* NSURLRequest+DSL.h */,\n\t\t\t\tD16FEA0923078C63006E67D5 /* LSNSURLHook.h */,\n\t\t\t\tD16FEA0A23078C63006E67D5 /* LSHTTPStubURLProtocol.h */,\n\t\t\t\tD16FEA0B23078C63006E67D5 /* NSURLRequest+LSHTTPRequest.h */,\n\t\t\t\tD16FEA0C23078C63006E67D5 /* LSNSURLHook.m */,\n\t\t\t\tD16FEA0D23078C63006E67D5 /* NSURLRequest+DSL.m */,\n\t\t\t\tD16FEA0E23078C63006E67D5 /* LSHTTPStubURLProtocol.m */,\n\t\t\t);\n\t\t\tpath = NSURLRequest;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FEA0F23078C63006E67D5 /* ASIHTTPRequest */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA1023078C63006E67D5 /* LSASIHTTPRequestHook.h */,\n\t\t\t\tD16FEA1123078C63006E67D5 /* ASIHTTPRequestStub.m */,\n\t\t\t\tD16FEA1223078C63006E67D5 /* LSASIHTTPRequestAdapter.h */,\n\t\t\t\tD16FEA1323078C63006E67D5 /* LSASIHTTPRequestHook.m */,\n\t\t\t\tD16FEA1423078C63006E67D5 /* LSASIHTTPRequestAdapter.m */,\n\t\t\t\tD16FEA1523078C63006E67D5 /* ASIHTTPRequestStub.h */,\n\t\t\t);\n\t\t\tpath = ASIHTTPRequest;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FEA1623078C63006E67D5 /* NSURLSession */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA1723078C63006E67D5 /* LSNSURLSessionHook.h */,\n\t\t\t\tD16FEA1823078C63006E67D5 /* LSNSURLSessionHook.m */,\n\t\t\t);\n\t\t\tpath = NSURLSession;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FEA1923078C63006E67D5 /* Model */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA1A23078C63006E67D5 /* LSHTTPBody.h */,\n\t\t\t\tD16FEA1B23078C63006E67D5 /* LSHTTPRequest.h */,\n\t\t\t\tD16FEA1C23078C63006E67D5 /* LSHTTPResponse.h */,\n\t\t\t);\n\t\t\tpath = Model;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FEA1E23078C63006E67D5 /* Matchers */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA1F23078C63006E67D5 /* LSMatcheable.h */,\n\t\t\t\tD16FEA2023078C63006E67D5 /* LSMatcher.h */,\n\t\t\t\tD16FEA2123078C63006E67D5 /* LSStringMatcher.h */,\n\t\t\t\tD16FEA2223078C63006E67D5 /* NSRegularExpression+Matcheable.m */,\n\t\t\t\tD16FEA2323078C63006E67D5 /* LSRegexMatcher.h */,\n\t\t\t\tD16FEA2423078C63006E67D5 /* NSString+Matcheable.m */,\n\t\t\t\tD16FEA2523078C63006E67D5 /* NSData+Matcheable.m */,\n\t\t\t\tD16FEA2623078C63006E67D5 /* LSDataMatcher.m */,\n\t\t\t\tD16FEA2723078C63006E67D5 /* LSMatcher.m */,\n\t\t\t\tD16FEA2823078C63006E67D5 /* LSRegexMatcher.m */,\n\t\t\t\tD16FEA2923078C63006E67D5 /* NSRegularExpression+Matcheable.h */,\n\t\t\t\tD16FEA2A23078C63006E67D5 /* LSStringMatcher.m */,\n\t\t\t\tD16FEA2B23078C63006E67D5 /* NSString+Matcheable.h */,\n\t\t\t\tD16FEA2C23078C63006E67D5 /* LSDataMatcher.h */,\n\t\t\t\tD16FEA2D23078C63006E67D5 /* NSData+Matcheable.h */,\n\t\t\t);\n\t\t\tpath = Matchers;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FEA2E23078C63006E67D5 /* Categories */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA2F23078C63006E67D5 /* NSData+Nocilla.h */,\n\t\t\t\tD16FEA3023078C63006E67D5 /* NSString+Nocilla.m */,\n\t\t\t\tD16FEA3123078C63006E67D5 /* NSData+Nocilla.m */,\n\t\t\t\tD16FEA3223078C63006E67D5 /* NSString+Nocilla.h */,\n\t\t\t);\n\t\t\tpath = Categories;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD16FEA3323078C63006E67D5 /* DSL */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD16FEA3423078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m */,\n\t\t\t\tD16FEA3523078C63006E67D5 /* LSStubRequestDSL.h */,\n\t\t\t\tD16FEA3623078C63006E67D5 /* LSStubResponseDSL.m */,\n\t\t\t\tD16FEA3723078C63006E67D5 /* LSStubRequestDSL.m */,\n\t\t\t\tD16FEA3823078C63006E67D5 /* LSHTTPRequestDSLRepresentation.h */,\n\t\t\t\tD16FEA3923078C63006E67D5 /* LSStubResponseDSL.h */,\n\t\t\t);\n\t\t\tpath = DSL;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1A1CC98219FAB3500263AD8 /* ImageSource */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD1A1CC99219FAB4B00263AD8 /* Source.swift */,\n\t\t\t\tD12F67632CAC330600AB63AB /* LivePhotoSource.swift */,\n\t\t\t\tD12AB69E215D2BB50013BA68 /* Resource.swift */,\n\t\t\t\tD1E56444219B16330057AAE3 /* ImageDataProvider.swift */,\n\t\t\t\tD16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */,\n\t\t\t\t078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */,\n\t\t\t\t0784D09A2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift */,\n\t\t\t);\n\t\t\tpath = ImageSource;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1ED2D021AD2CFA600CFC3EB = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD12AB688215D2A280013BA68 /* Sources */,\n\t\t\t\tD10EC22C1C3D62E800A4211C /* Tests */,\n\t\t\t\tD1ED2D0C1AD2CFA600CFC3EB /* Products */,\n\t\t\t\tEA99D30544BD22799F7A5367 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1ED2D0C1AD2CFA600CFC3EB /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */,\n\t\t\t\tD1ED2D3F1AD2D09F00CFC3EB /* KingfisherTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD1F7607423097532000C5269 /* SwiftUI */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD1F7607523097532000C5269 /* ImageBinder.swift */,\n\t\t\t\t4B88CEB32646D0BF009EBB41 /* ImageContext.swift */,\n\t\t\t\tD1F7607623097532000C5269 /* KFImage.swift */,\n\t\t\t\t07292244263B02F00089E810 /* KFAnimatedImage.swift */,\n\t\t\t\t4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */,\n\t\t\t\tD1889533258F7648003B73BE /* KFImageOptions.swift */,\n\t\t\t\t4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */,\n\t\t\t);\n\t\t\tpath = SwiftUI;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tEA99D30544BD22799F7A5367 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tC959EEE7228940FE00467A10 /* QuartzCore.framework */,\n\t\t\t\t4B164ACE1B8D554200768EC6 /* CFNetwork.framework */,\n\t\t\t\t4B3E714D1B01FEB200F5AAED /* WatchKit.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXHeadersBuildPhase section */\n\t\tD1ED2D321AD2D09F00CFC3EB /* Headers */ = {\n\t\t\tisa = PBXHeadersBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXHeadersBuildPhase section */\n\n/* Begin PBXNativeTarget section */\n\t\tD1ED2D341AD2D09F00CFC3EB /* Kingfisher */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D1ED2D4E1AD2D09F00CFC3EB /* Build configuration list for PBXNativeTarget \"Kingfisher\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD1ED2D301AD2D09F00CFC3EB /* Sources */,\n\t\t\t\tD1D550D42AEB9E7300AAD79D /* CopyFiles */,\n\t\t\t\tD1ED2D311AD2D09F00CFC3EB /* Frameworks */,\n\t\t\t\tD1ED2D321AD2D09F00CFC3EB /* Headers */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Kingfisher;\n\t\t\tproductName = Kingfisher;\n\t\t\tproductReference = D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */;\n\t\t\tproductType = \"com.apple.product-type.framework\";\n\t\t};\n\t\tD1ED2D3E1AD2D09F00CFC3EB /* KingfisherTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D1ED2D521AD2D09F00CFC3EB /* Build configuration list for PBXNativeTarget \"KingfisherTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD1ED2D3B1AD2D09F00CFC3EB /* Sources */,\n\t\t\t\tD1ED2D3C1AD2D09F00CFC3EB /* Frameworks */,\n\t\t\t\tD1ED2D3D1AD2D09F00CFC3EB /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\tD1ED2D421AD2D09F00CFC3EB /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = KingfisherTests;\n\t\t\tproductName = KingfisherTests;\n\t\t\tproductReference = D1ED2D3F1AD2D09F00CFC3EB /* KingfisherTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\tD1ED2D031AD2CFA600CFC3EB /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 0720;\n\t\t\t\tLastUpgradeCheck = 1600;\n\t\t\t\tORGANIZATIONNAME = \"Wei Wang\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\tD1ED2D341AD2D09F00CFC3EB = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 6.2;\n\t\t\t\t\t\tLastSwiftMigration = 1020;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t\tD1ED2D3E1AD2D09F00CFC3EB = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 6.2;\n\t\t\t\t\t\tLastSwiftMigration = 1020;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = D1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject \"Kingfisher\" */;\n\t\t\tcompatibilityVersion = \"Xcode 3.2\";\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 = D1ED2D021AD2CFA600CFC3EB;\n\t\t\tproductRefGroup = D1ED2D0C1AD2CFA600CFC3EB /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\tD1ED2D341AD2D09F00CFC3EB /* Kingfisher */,\n\t\t\t\tD1ED2D3E1AD2D09F00CFC3EB /* KingfisherTests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\tD1ED2D3D1AD2D09F00CFC3EB /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD16FEA3A23078C63006E67D5 /* LICENSE in Resources */,\n\t\t\t\tD1D2C32A1C70A3230018F2F9 /* single-frame.gif in Resources */,\n\t\t\t\tD16FEA3B23078C63006E67D5 /* README.md in Resources */,\n\t\t\t\tD12E0C4F1C47F23500AC98AD /* dancing-banana.gif in Resources */,\n\t\t\t\t38D5D3C12C5C7A1800BF1D01 /* gradient-8b-srgb-opaque.png in Resources */,\n\t\t\t\t38D5D3C22C5C7A1800BF1D01 /* gradient-8b-srgb-alpha.png in Resources */,\n\t\t\t\t38D5D3C32C5C7A1800BF1D01 /* gradient-8b-displayp3-alpha.png in Resources */,\n\t\t\t\t38D5D3C42C5C7A1800BF1D01 /* gradient-8b-gray.png in Resources */,\n\t\t\t\t38D5D3C52C5C7A1800BF1D01 /* gradient-10b-srgb-opaque.heic in Resources */,\n\t\t\t\t38D5D3C62C5C7A1800BF1D01 /* gradient-10b-srgb-alpha.heic in Resources */,\n\t\t\t\t38D5D3C72C5C7A1800BF1D01 /* gradient-10b-displayp3-alpha.heic in Resources */,\n\t\t\t\t38D5D3C82C5C7A1800BF1D01 /* gradient-16b-srgb-alpha.png in Resources */,\n\t\t\t\t38D5D3C92C5C7A1800BF1D01 /* gradient-16b-gray.png in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\tD1ED2D301AD2D09F00CFC3EB /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD12AB6CC215D2BB50013BA68 /* ImageModifier.swift in Sources */,\n\t\t\t\tD12AB718215D2BB50013BA68 /* CacheSerializer.swift in Sources */,\n\t\t\t\tD1E56445219B16330057AAE3 /* ImageDataProvider.swift in Sources */,\n\t\t\t\t4B88CEB22646C653009EBB41 /* KFImageRenderer.swift in Sources */,\n\t\t\t\tD12AB730215D2BB50013BA68 /* AnimatedImageView.swift in Sources */,\n\t\t\t\t4B46CC64217449E000D90C4A /* Storage.swift in Sources */,\n\t\t\t\tD12AB6E4215D2BB50013BA68 /* Placeholder.swift in Sources */,\n\t\t\t\t4B46CC6921744AC500D90C4A /* DiskStorage.swift in Sources */,\n\t\t\t\t4B46CC5F217449C600D90C4A /* MemoryStorage.swift in Sources */,\n\t\t\t\t4B88CEB42646D0BF009EBB41 /* ImageContext.swift in Sources */,\n\t\t\t\tD16CC3D624E02E9500F1A515 /* AVAssetImageDataProvider.swift in Sources */,\n\t\t\t\tD1839845216E333E003927D3 /* Delegate.swift in Sources */,\n\t\t\t\tD12AB6D8215D2BB50013BA68 /* ImageTransition.swift in Sources */,\n\t\t\t\tD12F67642CAC330A00AB63AB /* LivePhotoSource.swift in Sources */,\n\t\t\t\tD1A37BE8215D365A009B39B7 /* ExtensionHelpers.swift in Sources */,\n\t\t\t\tC9286407228584EB00257182 /* ImageProgressive.swift in Sources */,\n\t\t\t\t0784D09B2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift in Sources */,\n\t\t\t\tD12AB6DC215D2BB50013BA68 /* ImageProcessor.swift in Sources */,\n\t\t\t\tD12AB6D4215D2BB50013BA68 /* Image.swift in Sources */,\n\t\t\t\tD1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */,\n\t\t\t\tD12F67622CAC32BF00AB63AB /* KingfisherManager+LivePhoto.swift in Sources */,\n\t\t\t\t4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */,\n\t\t\t\tE9E3ED8B2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift in Sources */,\n\t\t\t\tD1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */,\n\t\t\t\tD18B3222251852E100662F63 /* KF.swift in Sources */,\n\t\t\t\tD12AB704215D2BB50013BA68 /* Kingfisher.swift in Sources */,\n\t\t\t\t00A8E26E2E81B89600ABB84F /* NetworkMonitor.swift in Sources */,\n\t\t\t\tD1AEB09725890DEA008556DF /* KFImage.swift in Sources */,\n\t\t\t\tF39B68C82E33AC2A00404B02 /* NetworkMetrics.swift in Sources */,\n\t\t\t\tD1BA781D2174D07800C69D7B /* CallbackQueue.swift in Sources */,\n\t\t\t\tD12AB71C215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift in Sources */,\n\t\t\t\tD1A37BF2215D3850009B39B7 /* SizeExtensions.swift in Sources */,\n\t\t\t\tD12AB70C215D2BB50013BA68 /* KingfisherManager.swift in Sources */,\n\t\t\t\t4B8351CC217084660081EED8 /* Runtime.swift in Sources */,\n\t\t\t\tD12AB6C0215D2BB50013BA68 /* RequestModifier.swift in Sources */,\n\t\t\t\t4B10480D216F157000300C61 /* ImageDataProcessor.swift in Sources */,\n\t\t\t\tD12AB72C215D2BB50013BA68 /* Indicator.swift in Sources */,\n\t\t\t\tD12AB6C8215D2BB50013BA68 /* ImageDownloader.swift in Sources */,\n\t\t\t\tD11D9B72245FA6F700C5A0AE /* RetryStrategy.swift in Sources */,\n\t\t\t\tD1A37BE3215D359F009B39B7 /* ImageFormat.swift in Sources */,\n\t\t\t\tD12EB83C24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift in Sources */,\n\t\t\t\tD1889534258F7649003B73BE /* KFImageOptions.swift in Sources */,\n\t\t\t\tD12AB714215D2BB50013BA68 /* ImageCache.swift in Sources */,\n\t\t\t\t4B88CEB02646C056009EBB41 /* KFImageProtocol.swift in Sources */,\n\t\t\t\tD12AB6D0215D2BB50013BA68 /* ImagePrefetcher.swift in Sources */,\n\t\t\t\t078DCB4F2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift in Sources */,\n\t\t\t\t388F37382B4D9CDB0089705C /* DisplayLink.swift in Sources */,\n\t\t\t\tD12AB6F4215D2BB50013BA68 /* ImageView+Kingfisher.swift in Sources */,\n\t\t\t\tD12AB6FC215D2BB50013BA68 /* UIButton+Kingfisher.swift in Sources */,\n\t\t\t\tD12F67602CAC2DBF00AB63AB /* ImageDownloader+LivePhoto.swift in Sources */,\n\t\t\t\tD12AB6E8215D2BB50013BA68 /* GIFAnimatedImage.swift in Sources */,\n\t\t\t\t22FDCE0E2700078B0044D11E /* CPListItem+Kingfisher.swift in Sources */,\n\t\t\t\tD13646742165A1A100A33652 /* Result.swift in Sources */,\n\t\t\t\tD1A1CC9A219FAB4B00263AD8 /* Source.swift in Sources */,\n\t\t\t\t4BD821622189FC0C0084CC21 /* SessionDelegate.swift in Sources */,\n\t\t\t\tD12AB6E0215D2BB50013BA68 /* Filter.swift in Sources */,\n\t\t\t\t4BE688F722FD513100B11168 /* NSButton+Kingfisher.swift in Sources */,\n\t\t\t\tD12AB6C4215D2BB50013BA68 /* Resource.swift in Sources */,\n\t\t\t\t76FB4FD2262D773E006D15F8 /* GraphicsContext.swift in Sources */,\n\t\t\t\tD8FCF6A821C5A0E500F9ABC0 /* RedirectHandler.swift in Sources */,\n\t\t\t\t07292245263B02F00089E810 /* KFAnimatedImage.swift in Sources */,\n\t\t\t\tD1A37BDE215D34E8009B39B7 /* ImageDrawing.swift in Sources */,\n\t\t\t\t4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */,\n\t\t\t\tD12AB708215D2BB50013BA68 /* KingfisherError.swift in Sources */,\n\t\t\t\tD12AB724215D2BB50013BA68 /* Box.swift in Sources */,\n\t\t\t\t4B8E291C216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */,\n\t\t\t\t3ADE9AF92A73CD69009A86CA /* String+SHA256.swift in Sources */,\n\t\t\t\tD12F67662CB022FC00AB63AB /* PHLivePhotoView+Kingfisher.swift in Sources */,\n\t\t\t\tD12AB710215D2BB50013BA68 /* KingfisherOptionsInfo.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD1ED2D3B1AD2D09F00CFC3EB /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD12E0C571C47F23500AC98AD /* KingfisherTestHelper.swift in Sources */,\n\t\t\t\tD16FEA4923078C63006E67D5 /* NSRegularExpression+Matcheable.m in Sources */,\n\t\t\t\tD12E0C581C47F23500AC98AD /* UIButtonExtensionTests.swift in Sources */,\n\t\t\t\tD1E564412199C21E0057AAE3 /* StorageExpirationTests.swift in Sources */,\n\t\t\t\tD16FEA4123078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m in Sources */,\n\t\t\t\t4BCFF7A621990DB70055AAC4 /* MemoryStorageTests.swift in Sources */,\n\t\t\t\tD16FEA4623078C63006E67D5 /* LSASIHTTPRequestHook.m in Sources */,\n\t\t\t\tD12E0C561C47F23500AC98AD /* KingfisherOptionsInfoTests.swift in Sources */,\n\t\t\t\tD9638BA61C7DC71F0046523D /* ImagePrefetcherTests.swift in Sources */,\n\t\t\t\tD12E0C551C47F23500AC98AD /* KingfisherManagerTests.swift in Sources */,\n\t\t\t\tD16FEA4523078C63006E67D5 /* ASIHTTPRequestStub.m in Sources */,\n\t\t\t\tD16FEA4423078C63006E67D5 /* LSHTTPStubURLProtocol.m in Sources */,\n\t\t\t\tD12E0C511C47F23500AC98AD /* ImageDownloaderTests.swift in Sources */,\n\t\t\t\tD16FEA4A23078C63006E67D5 /* NSString+Matcheable.m in Sources */,\n\t\t\t\tD16FEA4723078C63006E67D5 /* LSASIHTTPRequestAdapter.m in Sources */,\n\t\t\t\tD16FEA4F23078C63006E67D5 /* LSStringMatcher.m in Sources */,\n\t\t\t\tD16FEA3C23078C63006E67D5 /* LSStubRequest.m in Sources */,\n\t\t\t\tD12E0C521C47F23500AC98AD /* ImageExtensionTests.swift in Sources */,\n\t\t\t\tD16FEA5123078C63006E67D5 /* NSData+Nocilla.m in Sources */,\n\t\t\t\tD16FEA5523079707006E67D5 /* NSButtonExtensionTests.swift in Sources */,\n\t\t\t\tD16FEA5023078C63006E67D5 /* NSString+Nocilla.m in Sources */,\n\t\t\t\tD16FEA4E23078C63006E67D5 /* LSRegexMatcher.m in Sources */,\n\t\t\t\tF72CE9CE1FCF17ED00CC522A /* ImageModifierTests.swift in Sources */,\n\t\t\t\tD1F66CC12CB2CF2E004959F3 /* LivePhotoSourceTests.swift in Sources */,\n\t\t\t\tD12E0C531C47F23500AC98AD /* ImageViewExtensionTests.swift in Sources */,\n\t\t\t\tD16FEA4023078C63006E67D5 /* LSHTTPClientHook.m in Sources */,\n\t\t\t\tD16FEA3F23078C63006E67D5 /* LSHTTPRequestDiff.m in Sources */,\n\t\t\t\tD186696D21834261002B502E /* ImageDrawingTests.swift in Sources */,\n\t\t\t\t38D5D3A32C5C757E00BF1D01 /* PixelFormatDecodingTests.swift in Sources */,\n\t\t\t\tD16FEA4323078C63006E67D5 /* NSURLRequest+DSL.m in Sources */,\n\t\t\t\tD16FEA5323078C63006E67D5 /* LSStubResponseDSL.m in Sources */,\n\t\t\t\tD16FEA4C23078C63006E67D5 /* LSDataMatcher.m in Sources */,\n\t\t\t\t4BCFF7AA219932390055AAC4 /* DiskStorageTests.swift in Sources */,\n\t\t\t\tD16FEA5423078C63006E67D5 /* LSStubRequestDSL.m in Sources */,\n\t\t\t\tD1A1CC9F21A0F98600263AD8 /* ImageDataProviderTests.swift in Sources */,\n\t\t\t\tD16FEA4823078C63006E67D5 /* LSNSURLSessionHook.m in Sources */,\n\t\t\t\tD16FEA3E23078C63006E67D5 /* LSNocilla.m in Sources */,\n\t\t\t\tD16FEA4D23078C63006E67D5 /* LSMatcher.m in Sources */,\n\t\t\t\t4B8351C8217066580081EED8 /* StubHelpers.swift in Sources */,\n\t\t\t\tD1F1F6FF24625EC600910725 /* RetryStrategyTests.swift in Sources */,\n\t\t\t\tD1DC4B411D60996D00DFDFAA /* StringExtensionTests.swift in Sources */,\n\t\t\t\tD1BFED95222ACC6B009330C8 /* ImageProcessorTests.swift in Sources */,\n\t\t\t\tD16FEA5223078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m in Sources */,\n\t\t\t\tD16FEA4223078C63006E67D5 /* LSNSURLHook.m in Sources */,\n\t\t\t\tD16FEA4B23078C63006E67D5 /* NSData+Matcheable.m in Sources */,\n\t\t\t\tD16FEA3D23078C63006E67D5 /* LSStubResponse.m in Sources */,\n\t\t\t\tD12E0C501C47F23500AC98AD /* ImageCacheTests.swift in Sources */,\n\t\t\t\t4BA3BF1E228BCDD100909201 /* DataReceivingSideEffectTests.swift 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\tD1ED2D421AD2D09F00CFC3EB /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = D1ED2D341AD2D09F00CFC3EB /* Kingfisher */;\n\t\t\ttargetProxy = D1ED2D411AD2D09F00CFC3EB /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin XCBuildConfiguration section */\n\t\tD1ED2D281AD2CFA600CFC3EB /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = 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_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\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = 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_SYMBOLS_PRIVATE_EXTERN = NO;\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\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_DYLIB_INSTALL_NAME = \"@rpath\";\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tPRODUCT_BUNDLE_PACKAGE_TYPE = BNDL;\n\t\t\t\tSDKROOT = \"\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"watchsimulator iphonesimulator appletvsimulator watchos appletvos iphoneos macosx\";\n\t\t\t\tSWIFT_INSTALL_OBJC_HEADER = NO;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2,3,4\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 6.0;\n\t\t\t\tXROS_DEPLOYMENT_TARGET = 1.0;\n\t\t\t\t_EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD1ED2D291AD2CFA600CFC3EB /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = 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_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\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = 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\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_DYLIB_INSTALL_NAME = \"@rpath\";\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tPRODUCT_BUNDLE_PACKAGE_TYPE = BNDL;\n\t\t\t\tSDKROOT = \"\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"watchsimulator iphonesimulator appletvsimulator watchos appletvos iphoneos macosx\";\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_INSTALL_OBJC_HEADER = NO;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2,3,4\";\n\t\t\t\tTVOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tWATCHOS_DEPLOYMENT_TARGET = 6.0;\n\t\t\t\tXROS_DEPLOYMENT_TARGET = 1.0;\n\t\t\t\t_EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD1ED2D4F1AD2D09F00CFC3EB /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_WARN_ASSIGN_ENUM = YES;\n\t\t\t\tCLANG_WARN_CXX0X_EXTENSIONS = YES;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES;\n\t\t\t\tCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 3260;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEFINES_MODULE = YES;\n\t\t\t\tDYLIB_COMPATIBILITY_VERSION = 1;\n\t\t\t\tDYLIB_CURRENT_VERSION = 3260;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tENABLE_MODULE_VERIFIER = YES;\n\t\t\t\tGCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;\n\t\t\t\tGCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;\n\t\t\t\tGCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;\n\t\t\t\tGCC_WARN_ABOUT_MISSING_NEWLINE = YES;\n\t\t\t\tGCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;\n\t\t\t\tGCC_WARN_FOUR_CHARACTER_CONSTANTS = YES;\n\t\t\t\tGCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;\n\t\t\t\tGCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;\n\t\t\t\tGCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;\n\t\t\t\tGCC_WARN_SIGN_COMPARE = YES;\n\t\t\t\tGCC_WARN_STRICT_SELECTOR_MATCH = YES;\n\t\t\t\tGCC_WARN_UNKNOWN_PRAGMAS = YES;\n\t\t\t\tGCC_WARN_UNUSED_LABEL = YES;\n\t\t\t\tGCC_WARN_UNUSED_PARAMETER = YES;\n\t\t\t\tINFOPLIST_FILE = Sources/Info.plist;\n\t\t\t\tLD_DYLIB_INSTALL_NAME = \"$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)\";\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\tMODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = \"gnu99 gnu++11\";\n\t\t\t\tOTHER_SWIFT_FLAGS = \"-Xfrontend -warn-long-expression-type-checking=150\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_BUNDLE_PACKAGE_TYPE = FMWK;\n\t\t\t\tPRODUCT_NAME = Kingfisher;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_INSTALL_OBJC_HEADER = YES;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_STRICT_CONCURRENCY = complete;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2,3,4,7\";\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD1ED2D501AD2D09F00CFC3EB /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_WARN_ASSIGN_ENUM = YES;\n\t\t\t\tCLANG_WARN_CXX0X_EXTENSIONS = YES;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES;\n\t\t\t\tCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 3260;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEFINES_MODULE = YES;\n\t\t\t\tDYLIB_COMPATIBILITY_VERSION = 1;\n\t\t\t\tDYLIB_CURRENT_VERSION = 3260;\n\t\t\t\tDYLIB_INSTALL_NAME_BASE = \"@rpath\";\n\t\t\t\tENABLE_MODULE_VERIFIER = YES;\n\t\t\t\tGCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;\n\t\t\t\tGCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;\n\t\t\t\tGCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;\n\t\t\t\tGCC_WARN_ABOUT_MISSING_NEWLINE = YES;\n\t\t\t\tGCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;\n\t\t\t\tGCC_WARN_FOUR_CHARACTER_CONSTANTS = YES;\n\t\t\t\tGCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;\n\t\t\t\tGCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;\n\t\t\t\tGCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;\n\t\t\t\tGCC_WARN_SIGN_COMPARE = YES;\n\t\t\t\tGCC_WARN_STRICT_SELECTOR_MATCH = YES;\n\t\t\t\tGCC_WARN_UNKNOWN_PRAGMAS = YES;\n\t\t\t\tGCC_WARN_UNUSED_LABEL = YES;\n\t\t\t\tGCC_WARN_UNUSED_PARAMETER = YES;\n\t\t\t\tINFOPLIST_FILE = Sources/Info.plist;\n\t\t\t\tLD_DYLIB_INSTALL_NAME = \"$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)\";\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\tMODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = \"gnu99 gnu++11\";\n\t\t\t\tOTHER_SWIFT_FLAGS = \"\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.onevcat.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_BUNDLE_PACKAGE_TYPE = FMWK;\n\t\t\t\tPRODUCT_NAME = Kingfisher;\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_INSTALL_OBJC_HEADER = YES;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Owholemodule\";\n\t\t\t\tSWIFT_STRICT_CONCURRENCY = complete;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2,3,4,7\";\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD1ED2D531AD2D09F00CFC3EB /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"\";\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\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\tINFOPLIST_FILE = Tests/KingfisherTests/Info.plist;\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\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.onevcat.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Tests/KingfisherTests/KingfisherTests-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2,3,4,7\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD1ED2D541AD2D09F00CFC3EB /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"\";\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tINFOPLIST_FILE = Tests/KingfisherTests/Info.plist;\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\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.onevcat.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Tests/KingfisherTests/KingfisherTests-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2,3,4,7\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\tD1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject \"Kingfisher\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD1ED2D281AD2CFA600CFC3EB /* Debug */,\n\t\t\t\tD1ED2D291AD2CFA600CFC3EB /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD1ED2D4E1AD2D09F00CFC3EB /* Build configuration list for PBXNativeTarget \"Kingfisher\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD1ED2D4F1AD2D09F00CFC3EB /* Debug */,\n\t\t\t\tD1ED2D501AD2D09F00CFC3EB /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD1ED2D521AD2D09F00CFC3EB /* Build configuration list for PBXNativeTarget \"KingfisherTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD1ED2D531AD2D09F00CFC3EB /* Debug */,\n\t\t\t\tD1ED2D541AD2D09F00CFC3EB /* 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 = D1ED2D031AD2CFA600CFC3EB /* Project object */;\n}\n"
  },
  {
    "path": "Kingfisher.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:Kingfisher-Demo.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "Kingfisher.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": "Kingfisher.xcodeproj/xcshareddata/xcbaselines/D1ED2D3E1AD2D09F00CFC3EB.xcbaseline/74237B0B-7981-4A24-B6C4-95F4A5E7727F.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>classNames</key>\n\t<dict>\n\t\t<key>ImageCacheTests</key>\n\t\t<dict>\n\t\t\t<key>testRetrivingImagePerformance()</key>\n\t\t\t<dict>\n\t\t\t\t<key>com.apple.XCTPerformanceMetric_WallClockTime</key>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>baselineAverage</key>\n\t\t\t\t\t<real>0.27</real>\n\t\t\t\t\t<key>baselineIntegrationDisplayName</key>\n\t\t\t\t\t<string>Local Baseline</string>\n\t\t\t\t</dict>\n\t\t\t</dict>\n\t\t</dict>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "Kingfisher.xcodeproj/xcshareddata/xcbaselines/D1ED2D3E1AD2D09F00CFC3EB.xcbaseline/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>runDestinationsByUUID</key>\n\t<dict>\n\t\t<key>74237B0B-7981-4A24-B6C4-95F4A5E7727F</key>\n\t\t<dict>\n\t\t\t<key>localComputer</key>\n\t\t\t<dict>\n\t\t\t\t<key>busSpeedInMHz</key>\n\t\t\t\t<integer>100</integer>\n\t\t\t\t<key>cpuCount</key>\n\t\t\t\t<integer>1</integer>\n\t\t\t\t<key>cpuKind</key>\n\t\t\t\t<string>Intel Core i7</string>\n\t\t\t\t<key>cpuSpeedInMHz</key>\n\t\t\t\t<integer>3500</integer>\n\t\t\t\t<key>logicalCPUCoresPerPackage</key>\n\t\t\t\t<integer>8</integer>\n\t\t\t\t<key>modelCode</key>\n\t\t\t\t<string>iMac14,2</string>\n\t\t\t\t<key>physicalCPUCoresPerPackage</key>\n\t\t\t\t<integer>4</integer>\n\t\t\t\t<key>platformIdentifier</key>\n\t\t\t\t<string>com.apple.platform.macosx</string>\n\t\t\t</dict>\n\t\t\t<key>targetArchitecture</key>\n\t\t\t<string>x86_64</string>\n\t\t\t<key>targetDevice</key>\n\t\t\t<dict>\n\t\t\t\t<key>modelCode</key>\n\t\t\t\t<string>iPhone7,2</string>\n\t\t\t\t<key>platformIdentifier</key>\n\t\t\t\t<string>com.apple.platform.iphonesimulator</string>\n\t\t\t</dict>\n\t\t</dict>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "Kingfisher.xcodeproj/xcshareddata/xcschemes/Kingfisher.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1640\"\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 = \"D1ED2D341AD2D09F00CFC3EB\"\n               BuildableName = \"Kingfisher.framework\"\n               BlueprintName = \"Kingfisher\"\n               ReferencedContainer = \"container:Kingfisher.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      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            testExecutionOrdering = \"random\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"D1ED2D3E1AD2D09F00CFC3EB\"\n               BuildableName = \"KingfisherTests.xctest\"\n               BlueprintName = \"KingfisherTests\"\n               ReferencedContainer = \"container:Kingfisher.xcodeproj\">\n            </BuildableReference>\n            <SkippedTests>\n               <Test\n                  Identifier = \"ImagePrefetcherTests\">\n               </Test>\n            </SkippedTests>\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 = \"D1ED2D341AD2D09F00CFC3EB\"\n            BuildableName = \"Kingfisher.framework\"\n            BlueprintName = \"Kingfisher\"\n            ReferencedContainer = \"container:Kingfisher.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\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 = \"D1ED2D341AD2D09F00CFC3EB\"\n            BuildableName = \"Kingfisher.framework\"\n            BlueprintName = \"Kingfisher\"\n            ReferencedContainer = \"container:Kingfisher.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": "Kingfisher.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Kingfisher.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Demo/Kingfisher-Demo.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "Kingfisher.xcworkspace/xcshareddata/IDETemplateMacros.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>FILEHEADER</key>\n\t<string>\n//  ___FILENAME___\n//  Kingfisher\n//\n//  Created by ___USERNAME___ on ___DATE___.\n//\n//  Copyright (c) ___YEAR___ Wei Wang &lt;onevcat@gmail.com&gt;\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the &quot;Software&quot;), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "Kingfisher.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": "Kingfisher.xcworkspace/xcshareddata/Kingfisher.xcscmblueprint",
    "content": "{\n  \"DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey\" : \"17FB51EC7B87DC25DD21E28FDFD877B479CFFAC1\",\n  \"DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey\" : {\n\n  },\n  \"DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey\" : {\n    \"EDD4D5CA2765BF1400F1A6D6680AADA3F565AD7A\" : 9223372036854775807,\n    \"17FB51EC7B87DC25DD21E28FDFD877B479CFFAC1\" : 9223372036854775807\n  },\n  \"DVTSourceControlWorkspaceBlueprintIdentifierKey\" : \"A97B351B-F1BD-4510-8138-460C0D267EF0\",\n  \"DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey\" : {\n    \"EDD4D5CA2765BF1400F1A6D6680AADA3F565AD7A\" : \"Kingfisher\\/Kingfisher-TestImages\\/\",\n    \"17FB51EC7B87DC25DD21E28FDFD877B479CFFAC1\" : \"Kingfisher\\/\"\n  },\n  \"DVTSourceControlWorkspaceBlueprintNameKey\" : \"Kingfisher\",\n  \"DVTSourceControlWorkspaceBlueprintVersion\" : 204,\n  \"DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey\" : \"Kingfisher.xcworkspace\",\n  \"DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey\" : [\n    {\n      \"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey\" : \"github.com:onevcat\\/Kingfisher.git\",\n      \"DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey\" : \"com.apple.dt.Xcode.sourcecontrol.Git\",\n      \"DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey\" : \"17FB51EC7B87DC25DD21E28FDFD877B479CFFAC1\"\n    },\n    {\n      \"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey\" : \"github.com:onevcat\\/Kingfisher-TestImages.git\",\n      \"DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey\" : \"com.apple.dt.Xcode.sourcecontrol.Git\",\n      \"DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey\" : \"EDD4D5CA2765BF1400F1A6D6680AADA3F565AD7A\"\n    }\n  ]\n}"
  },
  {
    "path": "Kingfisher.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "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</plist>\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2019 Wei Wang\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\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version:5.1\nimport PackageDescription\n\nlet package = Package(\n    name: \"Kingfisher\",\n    platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6)],\n    products: [\n        .library(name: \"Kingfisher\", targets: [\"Kingfisher\"])\n    ],\n    targets: [\n        .target(\n            name: \"Kingfisher\",\n            path: \"Sources\"\n        )\n    ]\n)\n"
  },
  {
    "path": "Package@swift-5.9.swift",
    "content": "// swift-tools-version:5.9\nimport PackageDescription\n\nlet package = Package(\n    name: \"Kingfisher\",\n    platforms: [\n        .iOS(.v13),\n        .macOS(.v10_15),\n        .tvOS(.v13),\n        .watchOS(.v6),\n        .visionOS(.v1)\n    ],\n    products: [\n        .library(name: \"Kingfisher\", targets: [\"Kingfisher\"])\n    ],\n    targets: [\n        .target(\n            name: \"Kingfisher\",\n            path: \"Sources\",\n            resources: [.process(\"PrivacyInfo.xcprivacy\")]\n        )\n    ]\n)\n"
  },
  {
    "path": "README-LLM.md",
    "content": "<!-- Generated: 2025-06-15 12:45:00 UTC -->\n\n# Kingfisher\n\nKingfisher is a powerful, pure-Swift library for downloading and caching images from the web, providing elegant async APIs for iOS, macOS, tvOS, watchOS, and visionOS applications. The library handles the complete image lifecycle with multi-layer caching, built-in processing, and extensive UI component integrations.\n\n## Quick Start\n\n**Core API Entry Points:**\n- `Sources/General/KingfisherManager.swift` - Central coordinator\n- `Sources/General/KF.swift` - Builder pattern API (`KF.url()...`)\n- `Sources/Extensions/ImageView+Kingfisher.swift` - UIKit/AppKit extensions\n- `Sources/SwiftUI/KFImage.swift` - SwiftUI components\n\n**Essential Build Commands:**\n```bash\n# Install dependencies and run all tests\nbundle install && bundle exec fastlane tests\n\n# Build for specific platform\nswift build\n\n# Full release workflow\nbundle exec fastlane release version:X.X.X\n```\n\n## Documentation\n\n**For LLMs and Developers:**\n\n- **[Project Overview](docs/project-overview.md)** - What Kingfisher does, core purpose, technology stack, and platform support\n- **[Architecture](docs/architecture.md)** - System organization, component map, key files, and data flow with specific file references  \n- **[Build System](docs/build-system.md)** - Swift Package Manager and Fastlane workflows, platform setup, and troubleshooting\n- **[Testing](docs/testing.md)** - Test categories, running tests, and test infrastructure with file locations\n- **[Development](docs/development.md)** - Code style, implementation patterns, workflows, and common solutions\n- **[Deployment](docs/deployment.md)** - Package types, platform deployment, release management, and CI/CD\n- **[File Catalog](docs/files.md)** - Comprehensive file organization with specific file purposes and relationships\n\n**Configuration Files:**\n- `Package.swift` - Swift Package Manager manifest\n- `Kingfisher.podspec` - CocoaPods specification  \n- `fastlane/Fastfile` - Build automation\n- `Sources/Documentation.docc/` - DocC documentation\n\n**Key Patterns:**\n- Namespace wrapper (`.kf` property) in `Sources/General/Kingfisher.swift`\n- Builder pattern API in `Sources/General/KF.swift` \n- Options system in `Sources/General/KingfisherOptionsInfo.swift`\n- Protocol-oriented design throughout `Sources/Image/ImageProcessor.swift`\n\n## Requirements\n\n- **Swift 5.9+** (Swift 6 strict concurrency ready)\n- **iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+**\n- **SwiftUI support**: iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n<img src=\"https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png\" alt=\"Kingfisher\" title=\"Kingfisher\" width=\"557\"/>\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/onevcat/Kingfisher/actions?query=workflow%3Abuild\"><img src=\"https://github.com/onevcat/kingfisher/workflows/build/badge.svg?branch=master\"></a>\n<a href=\"https://swiftpackageindex.com/onevcat/Kingfisher/master/documentation/kingfisher\"><img src=\"https://img.shields.io/badge/Swift-Doc-DE5C43.svg?style=flat\"></a>\n<a href=\"https://cocoapods.org/pods/Kingfisher\"><img src=\"https://img.shields.io/github/v/tag/onevcat/Kingfisher.svg?color=blue&include_prereleases=&sort=semver\"></a>\n<a href=\"https://swift.org/package-manager/\"><img src=\"https://img.shields.io/badge/SPM-supported-DE5C43.svg?style=flat\"></a>\n<a href=\"https://raw.githubusercontent.com/onevcat/Kingfisher/master/LICENSE\"><img src=\"https://img.shields.io/badge/license-MIT-black\"></a>\n</p>\n\nKingfisher is a powerful, pure-Swift library for downloading and caching images from the web. It provides you a chance to use a pure-Swift way to work with remote images in your next app.\n\n## Features\n\n- [x] Asynchronous image downloading and caching.\n- [x] Loading image from either `URLSession`-based networking or local provided data.\n- [x] Useful image processors and filters provided.\n- [x] Multiple-layer hybrid cache for both memory and disk.\n- [x] Fine control on cache behavior. Customizable expiration date and size limit.\n- [x] Cancelable downloading and auto-reusing previous downloaded content to improve performance.\n- [x] Independent components. Use the downloader, caching system, and image processors separately as you need.\n- [x] Prefetching images and showing them from the cache to boost your app.\n- [x] Extensions for `UIImageView`, `NSImageView`, `NSButton`, `UIButton`, `NSTextAttachment`, `WKInterfaceImage`, `TVMonogramView` and `CPListItem` to directly set an image from a URL.\n- [x] Built-in transition animation when setting images.\n- [x] Customizable placeholder and indicator while loading images.\n- [x] Extensible image processing and image format easily.\n- [x] Low Data Mode support.\n- [x] SwiftUI support.\n- [x] Swift 6 & Swift Concurrency (strict mode) prepared.\n- [x] Load & cache for Live Photo.\n\n### Kingfisher 101\n\nThe simplest use-case is setting an image to an image view with the `UIImageView` extension:\n\n```swift\nimport Kingfisher\n\nlet url = URL(string: \"https://example.com/image.png\")\nimageView.kf.setImage(with: url)\n```\n\nKingfisher will download the image from `url`, send it to both memory cache and disk cache, and display it in `imageView`. \nWhen you set it with the same URL later, the image will be retrieved from the cache and shown immediately.\n\nIt also works if you use SwiftUI:\n\n```swift\nvar body: some View {\n    KFImage(URL(string: \"https://example.com/image.png\")!)\n}\n```\n\n### A More Advanced Example\n\nWith the powerful options, you can do hard tasks with Kingfisher in a simple way. For example, the code below: \n\n1. Downloads a high-resolution image.\n2. Downsamples it to match the image view size.\n3. Makes it round cornered with a given radius.\n4. Shows a system indicator and a placeholder image while downloading.\n5. When prepared, it animates the small thumbnail image with a \"fade in\" effect. \n6. The original large image is also cached to disk for later use, to get rid of downloading it again in a detail view.\n7. A console log is printed when the task finishes, either for success or failure.\n\n```swift\nlet url = URL(string: \"https://example.com/high_resolution_image.png\")\nlet processor = DownsamplingImageProcessor(size: imageView.bounds.size)\n             |> RoundCornerImageProcessor(cornerRadius: 20)\nimageView.kf.indicatorType = .activity\nimageView.kf.setImage(\n    with: url,\n    placeholder: UIImage(named: \"placeholderImage\"),\n    options: [\n        .processor(processor),\n        .scaleFactor(UIScreen.main.scale),\n        .transition(.fade(1)),\n        .cacheOriginalImage\n    ])\n{\n    result in\n    switch result {\n    case .success(let value):\n        print(\"Task done for: \\(value.source.url?.absoluteString ?? \"\")\")\n    case .failure(let error):\n        print(\"Job failed: \\(error.localizedDescription)\")\n    }\n}\n```\n\nIt is a common situation I can meet in my daily work. Think about how many lines you need to write without\nKingfisher!\n\n### Method Chaining\n\nIf you are not a fan of the `kf` extension, you can also prefer to use the `KF` builder and chained the method \ninvocations. The code below is doing the same thing:\n\n```swift\n// Use `kf` extension\nimageView.kf.setImage(\n    with: url,\n    placeholder: placeholderImage,\n    options: [\n        .processor(processor),\n        .loadDiskFileSynchronously,\n        .cacheOriginalImage,\n        .transition(.fade(0.25)),\n        .lowDataMode(.network(lowResolutionURL))\n    ],\n    progressBlock: { receivedSize, totalSize in\n        // Progress updated\n    },\n    completionHandler: { result in\n        // Done\n    }\n)\n\n// Use `KF` builder\nKF.url(url)\n  .placeholder(placeholderImage)\n  .setProcessor(processor)\n  .loadDiskFileSynchronously()\n  .cacheMemoryOnly()\n  .fade(duration: 0.25)\n  .lowDataModeSource(.network(lowResolutionURL))\n  .onProgress { receivedSize, totalSize in  }\n  .onSuccess { result in  }\n  .onFailure { error in }\n  .set(to: imageView)\n```\n\nAnd even better, if later you want to switch to SwiftUI, just change the `KF` above to `KFImage`, and you've done:\n\n```swift\nstruct ContentView: View {\n    var body: some View {\n        KFImage.url(url)\n          .placeholder(placeholderImage)\n          .setProcessor(processor)\n          .loadDiskFileSynchronously()\n          .cacheMemoryOnly()\n          .fade(duration: 0.25)\n          .lowDataModeSource(.network(lowResolutionURL))\n          .onProgress { receivedSize, totalSize in  }\n          .onSuccess { result in  }\n          .onFailure { error in }\n    }\n}\n```\n\n## Requirements\n\n### Kingfisher 8.0\n\n- (UIKit/AppKit) iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+\n- (SwiftUI) iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+\n- Swift 5.9+\n\n### Kingfisher 7.0\n\n- (UIKit/AppKit) iOS 12.0+ / macOS 10.14+ / tvOS 12.0+ / watchOS 5.0+ / visionOS 1.0+\n- (SwiftUI) iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+\n- Swift 5.0+\n\n### Installation\n\nRefer to one of the following tutorials to install and use the framework:\n\n- [UIKit Tutorial](https://swiftpackageindex.com/onevcat/kingfisher/master/tutorials/kingfisher/gettingstarteduikit)\n- [SwiftUI Tutorial](https://swiftpackageindex.com/onevcat/kingfisher/master/tutorials/kingfisher/gettingstartedswiftui)\n\nAlternatively, you can follow either of the methods below.\n\n#### Swift Package Manager\n\n- File > Swift Packages > Add Package Dependency\n- Add `https://github.com/onevcat/Kingfisher.git`\n- Select \"Up to Next Major\" with \"8.0.0\"\n\n#### CocoaPods\n\n```ruby\nsource 'https://github.com/CocoaPods/Specs.git'\nplatform :ios, '13.0'\nuse_frameworks!\n\ntarget 'MyApp' do\n  pod 'Kingfisher', '~> 8.0'\nend\n```\n\n#### Pre-built Framework\n\n1. Open the release page, download the latest version of Kingfisher from the assets section. \n2. Drag the `Kingfisher.xcframework` into your project and add it to the target (usually the app target).\n3. Select your target, in the \"General\" Tab, find the \"Frameworks, Libraries, and Embedded Content\" section, set the `Embed Without Signing` to Kingfisher.\n\n## Documentation\n\nCheck the documentation and tutorials:\n\n- [Documentation Home](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher)\n- [Getting Started](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/gettingstarted)\n    - [UIKit Tutorial](https://swiftpackageindex.com/onevcat/kingfisher/master/tutorials/kingfisher/gettingstarteduikit)\n    - [SwiftUI Tutorial](https://swiftpackageindex.com/onevcat/kingfisher/master/tutorials/kingfisher/gettingstartedswiftui)\n- [Common Tasks - General](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/commontasks)\n    - [Common Tasks - Cache](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/commontasks_cache)\n    - [Common Tasks - Downloader](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/commontasks_downloader)\n    - [Common tasks - Processor](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/commontasks_processor)\n\n### Migrating\n\n- [Kingfisher 8.0 Migration](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/migration-to-8)\n- [Kingfisher 7.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-7.0-Migration-Guide)\n\nIf you are using an even earlier version, see the guides below to know the steps for migrating.\n\n## Other\n\n### Future of Kingfisher\n\nI want to keep Kingfisher lightweight. This framework focuses on providing a simple solution for downloading and caching images. This doesn’t mean the framework can’t be improved. Kingfisher is far from perfect, so necessary and useful updates will be made to make it better.\n\n### Developments and Tests\n\nAny contributing and pull requests are warmly welcome. However, before you plan to implement some features or try to fix an uncertain issue, it is recommended to open a discussion first. It would be appreciated if your pull requests could build with all tests green. :)\n\n### About the logo\n\nThe logo of Kingfisher is inspired by [Tangram (七巧板)](http://en.wikipedia.org/wiki/Tangram), a dissection puzzle consisting of seven flat shapes from China. I believe she's a kingfisher bird instead of a swift, but someone insists that she is a pigeon. I guess I should give her a name. Hi, guys, do you have any suggestions?\n\n### Contact\n\nFollow and contact me on [Twitter](http://twitter.com/onevcat) or [Sina Weibo](http://weibo.com/onevcat). If you find an issue, [open a ticket](https://github.com/onevcat/Kingfisher/issues/new). Pull requests are warmly welcome as well.\n\n## Backers & Sponsors\n\nOpen-source projects cannot live long without your help. If you find Kingfisher to be useful, please consider supporting this \nproject by becoming a sponsor. Your user icon or company logo shows up [on my blog](https://onevcat.com/tabs/about/) with a link to your home page. \n\nBecome a sponsor through [GitHub Sponsors](https://github.com/sponsors/onevcat). :heart:\n\nSpecial thanks to:\n\n[![imgly](https://user-images.githubusercontent.com/1812216/106253726-271ed000-6218-11eb-98e0-c9c681925770.png)](https://img.ly/)\n\n[![emergetools](https://github-production-user-asset-6210df.s3.amazonaws.com/1019875/254794187-d44f6f50-993f-42e3-b79c-960f69c4adc1.png)](https://www.emergetools.com)\n\n\n\n### License\n\nKingfisher is released under the MIT license. See LICENSE for details.\n"
  },
  {
    "path": "Sources/Cache/CacheSerializer.swift",
    "content": "//\n//  CacheSerializer.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2016/09/02.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport CoreGraphics\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// A `CacheSerializer` is used to convert some data to an image object after retrieving it from disk storage,\n/// and vice versa, to convert an image to a data object for storing it to the disk storage.\npublic protocol CacheSerializer: Sendable {\n    \n    /// Retrieves the serialized data from a provided image and optional original data for caching to disk.\n    ///\n    /// - Parameters:\n    ///   - image: The image to be serialized.\n    ///   - original: The original data that was just downloaded.\n    ///   If the image is retrieved from the cache instead of being downloaded, it will be `nil`.\n    /// - Returns: The data object for storing to disk, or `nil` when no valid data can be serialized.\n    func data(with image: KFCrossPlatformImage, original: Data?) -> Data?\n\n    /// Retrieves an image from the provided serialized data.\n    ///\n    /// - Parameters:\n    ///   - data: The data from which an image should be deserialized.\n    ///   - options: The parsed options for deserialization.\n    /// - Returns: A deserialized image, or `nil` when no valid image can be deserialized.\n    func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?\n    \n    /// Indicates whether this serializer prefers to cache the original data in its implementation.\n    ///\n    /// If `true`, during storing phase, the original data is preferred to be stored to the disk if exists. When\n    /// retrieving image from the disk cache, after creating the image from the loaded data, Kingfisher will continue\n    /// to apply the processor to get the final image.\n    ///\n    /// By default, it is `false`, and the actual processed image is assumed to be serialized to and later deserialized\n    /// from the disk. That means the processed version of the image is stored and loaded.\n    var originalDataUsed: Bool { get }\n}\n\npublic extension CacheSerializer {\n    var originalDataUsed: Bool { false }\n}\n\n/// Represents a basic and default `CacheSerializer` used in the Kingfisher disk cache system.\n///\n/// It can serialize and deserialize images in PNG, JPEG, and GIF formats. For images other than these formats, a \n/// normalized ``KingfisherWrapper/pngRepresentation()`` will be used.\n///\n/// When converting an `image` to the data, it will only be converted to the corresponding data type when `original`\n/// contains valid PNG, JPEG, and GIF format data. If the `original` is provided but not valid, or if `original` is\n/// `nil`, the input `image` will be encoded as PNG data.\n///\n/// If `original` is `nil` but the input `image` contains embedded GIF data (for example, a cached animated image\n/// created from GIF data), the serializer will prefer the embedded GIF data and store it as GIF instead of falling\n/// back to PNG.\n///\n/// > Tip: If you create a new image instance from an animated image in a custom processor, use\n/// > ``KingfisherWrapper/copyKingfisherState(to:)`` to propagate the embedded animated data to the new image.\npublic struct DefaultCacheSerializer: CacheSerializer {\n    \n    /// The default general cache serializer utilized throughout Kingfisher's caching mechanism.\n    public static let `default` = DefaultCacheSerializer()\n\n    /// The compression quality used when converting an image to lossy format data (such as JPEG).\n    ///\n    /// Default is 1.0.\n    public var compressionQuality: CGFloat = 1.0\n\n    /// Determines whether the original data should be prioritized during image serialization.\n    ///\n    /// If set to `true`, the original input data will be initially inspected and used, unless the data is `nil`.\n    /// In the event of a `nil` data, the serialization process will revert to generating data from the image.\n    ///\n    /// > This value is used as ``CacheSerializer/originalDataUsed-d2v9``.\n    public var preferCacheOriginalData: Bool = false\n\n    public var originalDataUsed: Bool { preferCacheOriginalData }\n    \n    /// Creates a cache serializer that serializes and deserializes images in PNG, JPEG, and GIF formats.\n    ///\n    /// > Prefer to use the ``DefaultCacheSerializer/default`` value unless you need to specify your own properties.\n    public init() { }\n\n    public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {\n        let format: ImageFormat = {\n            if let original = original { return original.kf.imageFormat }\n\n            if let animatedData = image.kf.gifRepresentation(), animatedData.kf.imageFormat == .GIF {\n                return .GIF\n            }\n            return .unknown\n        }()\n\n        if preferCacheOriginalData {\n            if let original = original { return original }\n            if format == .GIF { return image.kf.gifRepresentation() }\n        }\n\n        return image.kf.data(format: format, compressionQuality: compressionQuality)\n    }\n    \n    public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)\n    }\n}\n"
  },
  {
    "path": "Sources/Cache/DiskStorage.swift",
    "content": "//\n//  DiskStorage.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/10/15.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n\n/// Represents the concepts related to storage that stores a specific type of value in disk.\n///\n/// This serves as a namespace for memory storage types. A ``DiskStorage/Backend`` with a particular\n/// ``DiskStorage/Config`` is used to define the storage.\n///\n/// Refer to these composite types for further details.\npublic enum DiskStorage {\n\n    /// Represents a storage backend for the ``DiskStorage``.\n    ///\n    /// The value is serialized to binary data and stored as a file in the file system under a specified location.\n    ///\n    /// You can configure a ``DiskStorage/Backend`` in its ``DiskStorage/Backend/init(config:)`` by passing a\n    /// ``DiskStorage/Config`` value or by modifying the ``DiskStorage/Backend/config`` property after it has been\n    /// created. The ``DiskStorage/Backend`` will use the file's attributes to keep track of a file for its expiration\n    /// or size limitation.\n    public final class Backend<T: DataTransformable>: @unchecked Sendable where T: Sendable {\n        \n        private let propertyQueue = DispatchQueue(label: \"com.onevcat.kingfisher.DiskStorage.Backend.propertyQueue\")\n        \n        private var _config: Config\n        /// The configuration used for this disk storage.\n        ///\n        /// It is a value you can set and use to configure the storage as needed.\n        public var config: Config {\n            get { propertyQueue.sync { _config } }\n            set { propertyQueue.sync { _config = newValue } }\n        }\n\n        /// The final storage URL on disk of the disk storage ``DiskStorage/Backend``, considering the\n        /// ``DiskStorage/Config/name`` and the  ``DiskStorage/Config/cachePathBlock``.\n        public let directoryURL: URL\n\n        let metaChangingQueue: DispatchQueue\n\n        // A shortcut (which contains false-positive) to improve matching performance.\n        var maybeCached : Set<String>?\n        let maybeCachedCheckingQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.maybeCachedCheckingQueue\")\n\n        // `false` if the storage initialized with an error.\n        // This prevents unexpected forcibly crash when creating storage in the default cache.\n        private var storageReady: Bool = true\n\n        /// Creates a disk storage with the given ``DiskStorage/Config``.\n        ///\n        /// - Parameter config: The configuration used for this disk storage.\n        /// - Throws: An error if the folder for storage cannot be obtained or created.\n        public convenience init(config: Config) throws {\n            self.init(noThrowConfig: config, creatingDirectory: false)\n            try prepareDirectory()\n        }\n\n        // If `creatingDirectory` is `false`, the directory preparation will be skipped.\n        // We need to call `prepareDirectory` manually after this returns.\n        init(noThrowConfig config: Config, creatingDirectory: Bool) {\n            var config = config\n\n            let creation = Creation(config)\n            self.directoryURL = creation.directoryURL\n\n            // Break any possible retain cycle set by outside.\n            config.cachePathBlock = nil\n            _config = config\n\n            metaChangingQueue = DispatchQueue(label: creation.cacheName)\n            setupCacheChecking()\n\n            if creatingDirectory {\n                try? prepareDirectory()\n            }\n        }\n\n        private func setupCacheChecking() {\n            DispatchQueue.global(qos: .default).async {\n                do {\n                    let allFiles = try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path)\n                    let maybeCached = Set(allFiles)\n                    self.maybeCachedCheckingQueue.async {\n                        self.maybeCached = maybeCached\n                    }\n                } catch {\n                    self.maybeCachedCheckingQueue.async {\n                        // Just disable the functionality if we fail to initialize it properly. This will just revert to\n                        // the behavior which is to check file existence on disk directly.\n                        self.maybeCached = nil\n                    }\n                }\n            }\n        }\n\n        // Creates the storage folder.\n        private func prepareDirectory() throws {\n            let fileManager = config.fileManager\n            let path = directoryURL.path\n\n            guard !fileManager.fileExists(atPath: path) else { return }\n\n            do {\n                try fileManager.createDirectory(\n                    atPath: path,\n                    withIntermediateDirectories: true,\n                    attributes: nil)\n            } catch {\n                self.storageReady = false\n                throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error))\n            }\n        }\n\n        /// Stores a value in the storage under the specified key and expiration policy.\n        ///\n        /// - Parameters:\n        ///   - value: The value to be stored.\n        ///   - key: The key to which the `value` will be stored. If there is already a value under the key, the old\n        ///          value will be overwritten by the new `value`.\n        ///   - expiration: The expiration policy used by this storage action.\n        ///   - writeOptions: Data writing options used for the new files.\n        ///   - forcedExtension: The file extension, if exists.\n        /// - Throws: An error during converting the value to a data format or during writing it to disk.\n        public func store(\n            value: T,\n            forKey key: String,\n            expiration: StorageExpiration? = nil,\n            writeOptions: Data.WritingOptions = [],\n            forcedExtension: String? = nil\n        ) throws\n        {\n            guard storageReady else {\n                throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))\n            }\n\n            let expiration = expiration ?? config.expiration\n            // The expiration indicates that already expired, no need to store.\n            guard !expiration.isExpired else { return }\n            \n            let data: Data\n            do {\n                data = try value.toData()\n            } catch {\n                throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error))\n            }\n\n            let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension)\n            do {\n                try data.write(to: fileURL, options: writeOptions)\n            } catch {\n                if error.isFolderMissing {\n                    // The whole cache folder is deleted. Try to recreate it and write file again.\n                    do {\n                        try prepareDirectory()\n                        try data.write(to: fileURL, options: writeOptions)\n                    } catch {\n                        throw KingfisherError.cacheError(\n                            reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error)\n                        )\n                    }\n                } else {\n                    throw KingfisherError.cacheError(\n                        reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error)\n                    )\n                }\n            }\n\n            let now = Date()\n            let attributes: [FileAttributeKey : Any] = [\n                // The last access date.\n                .creationDate: now.fileAttributeDate,\n                // The estimated expiration date.\n                .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate\n            ]\n            do {\n                try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path)\n            } catch {\n                try? config.fileManager.removeItem(at: fileURL)\n                throw KingfisherError.cacheError(\n                    reason: .cannotSetCacheFileAttribute(\n                        filePath: fileURL.path,\n                        attributes: attributes,\n                        error: error\n                    )\n                )\n            }\n\n            maybeCachedCheckingQueue.async {\n                self.maybeCached?.insert(fileURL.lastPathComponent)\n            }\n        }\n\n        /// Retrieves a value from the storage.\n        /// - Parameters:\n        ///   - key: The cache key of the value.\n        ///   - forcedExtension: The file extension, if exists.\n        ///   - extendingExpiration: The expiration policy used by this retrieval action.\n        /// - Throws: An error during converting the data to a value or during the operation of disk files.\n        /// - Returns: The value under `key` if it is valid and found in the storage; otherwise, `nil`.\n        public func value(\n            forKey key: String,\n            forcedExtension: String? = nil,\n            extendingExpiration: ExpirationExtending = .cacheTime\n        ) throws -> T? {\n            try value(\n                forKey: key,\n                referenceDate: Date(),\n                actuallyLoad: true,\n                extendingExpiration: extendingExpiration,\n                forcedExtension: forcedExtension\n            )\n        }\n\n        func value(\n            forKey key: String,\n            referenceDate: Date,\n            actuallyLoad: Bool,\n            extendingExpiration: ExpirationExtending,\n            forcedExtension: String?\n        ) throws -> T?\n        {\n            guard storageReady else {\n                throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))\n            }\n\n            let fileManager = config.fileManager\n            let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension)\n            let filePath = fileURL.path\n\n            let fileMaybeCached = maybeCachedCheckingQueue.sync {\n                return maybeCached?.contains(fileURL.lastPathComponent) ?? true\n            }\n            guard fileMaybeCached else {\n                return nil\n            }\n            guard fileManager.fileExists(atPath: filePath) else {\n                return nil\n            }\n\n            let meta: FileMeta\n            do {\n                let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey]\n                meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys)\n            } catch {\n                throw KingfisherError.cacheError(\n                    reason: .invalidURLResource(error: error, key: key, url: fileURL))\n            }\n\n            if meta.expired(referenceDate: referenceDate) {\n                return nil\n            }\n            if !actuallyLoad { return T.empty }\n\n            do {\n                let data = try Data(contentsOf: fileURL)\n                let obj = try T.fromData(data)\n                metaChangingQueue.async {\n                    meta.extendExpiration(with: self.config.fileManager, extendingExpiration: extendingExpiration)\n                }\n                return obj\n            } catch {\n                throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error))\n            }\n        }\n\n        /// Determines whether there is valid cached data under a given key.\n        /// \n        /// - Parameters:\n        ///   - key: The cache key of the value.\n        ///   - forcedExtension: The file extension, if exists.\n        /// - Returns: `true` if there is valid data under the key and file extension; otherwise, `false`.\n        ///\n        /// > This method does not actually load the data from disk, so it is faster than directly loading the cached\n        /// value by checking the nullability of the\n        /// ``DiskStorage/Backend/value(forKey:forcedExtension:extendingExpiration:)`` method.\n        public func isCached(forKey key: String, forcedExtension: String? = nil) -> Bool {\n            return isCached(forKey: key, referenceDate: Date(), forcedExtension: forcedExtension)\n        }\n\n        /// Determines whether there is valid cached data under a given key and a reference date.\n        ///\n        /// - Parameters:\n        ///   - key: The cache key of the value.\n        ///   - referenceDate: A reference date to check whether the cache is still valid.\n        ///   - forcedExtension: The file extension, if exists.\n        ///\n        /// - Returns: `true` if there is valid data under the key; otherwise, `false`.\n        ///\n        /// If you pass `Date()` as the `referenceDate`, this method is identical to\n        /// ``DiskStorage/Backend/isCached(forKey:forcedExtension:)``. Use the `referenceDate` to determine whether the\n        /// cache is still valid for a future date.\n        public func isCached(forKey key: String, referenceDate: Date, forcedExtension: String? = nil) -> Bool {\n            do {\n                let result = try value(\n                    forKey: key,\n                    referenceDate: referenceDate,\n                    actuallyLoad: false,\n                    extendingExpiration: .none,\n                    forcedExtension: forcedExtension\n                )\n                return result != nil\n            } catch {\n                return false\n            }\n        }\n\n        /// Removes a value from a specified key.\n        /// - Parameters:\n        ///   - key: The cache key of the value.\n        ///   - forcedExtension: The file extension, if exists.\n        /// - Throws: An error during the removal of the value.\n        public func remove(forKey key: String, forcedExtension: String? = nil) throws {\n            let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension)\n            try removeFile(at: fileURL)\n        }\n\n        func removeFile(at url: URL) throws {\n            try config.fileManager.removeItem(at: url)\n        }\n\n        /// Removes all values in this storage.\n        /// - Throws: An error during the removal of the values.\n        public func removeAll() throws {\n            try removeAll(skipCreatingDirectory: false)\n        }\n\n        func removeAll(skipCreatingDirectory: Bool) throws {\n            try config.fileManager.removeItem(at: directoryURL)\n            if !skipCreatingDirectory {\n                try prepareDirectory()\n            }\n        }\n        \n        /// The URL of the cached file with a given computed `key`.\n        /// - Parameters:\n        ///   - key: The final computed key used when caching the image. Please note that usually this is not\n        /// the ``Source/cacheKey`` of an image ``Source``. It is the computed key with the processor identifier\n        /// considered.\n        ///   - forcedExtension: The file extension, if exists.\n        /// - Returns: The expected file URL on the disk based on the `key` and the `forcedExtension`.\n        ///\n        /// This method does not guarantee that an image is already cached at the returned URL. It just provides the URL\n        /// where the image should be if it exists in the disk storage, with the given key and file extension.\n        ///\n        public func cacheFileURL(forKey key: String, forcedExtension: String? = nil) -> URL {\n            let fileName = cacheFileName(forKey: key, forcedExtension: forcedExtension)\n            return directoryURL.appendingPathComponent(fileName, isDirectory: false)\n        }\n        \n        func cacheFileName(forKey key: String, forcedExtension: String? = nil) -> String {\n            let baseName = config.usesHashedFileName ? key.kf.sha256 : key\n            \n            if let ext = fileExtension(key: key, forcedExtension: forcedExtension) {\n                return \"\\(baseName).\\(ext)\"\n            }\n            \n            return baseName\n        }\n        \n        func fileExtension(key: String, forcedExtension: String?) -> String? {\n            if let ext = forcedExtension ?? config.pathExtension {\n                return ext\n            }\n        \n            if config.usesHashedFileName && config.autoExtAfterHashedFileName {\n                return key.kf.ext\n            }\n        \n            return nil\n        }\n\n        func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] {\n            let fileManager = config.fileManager\n\n            guard let directoryEnumerator = fileManager.enumerator(\n                at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else\n            {\n                throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL))\n            }\n\n            guard let urls = directoryEnumerator.allObjects as? [URL] else {\n                throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL))\n            }\n            return urls\n        }\n\n        /// Removes all expired values from this storage.\n        /// - Throws: A file manager error during the removal of the file.\n        /// - Returns: The URLs for the removed files.\n        public func removeExpiredValues() throws -> [URL] {\n            return try removeExpiredValues(referenceDate: Date())\n        }\n\n        func removeExpiredValues(referenceDate: Date) throws -> [URL] {\n            let propertyKeys: [URLResourceKey] = [\n                .isDirectoryKey,\n                .contentModificationDateKey\n            ]\n\n            let urls = try allFileURLs(for: propertyKeys)\n            let keys = Set(propertyKeys)\n            let expiredFiles = urls.filter { fileURL in\n                do {\n                    let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys)\n                    if meta.isDirectory {\n                        return false\n                    }\n                    return meta.expired(referenceDate: referenceDate)\n                } catch {\n                    return true\n                }\n            }\n            try expiredFiles.forEach { url in\n                try removeFile(at: url)\n            }\n            return expiredFiles\n        }\n\n        /// Removes all size-exceeded values from this storage.\n        /// - Throws: A file manager error during the removal of the file.\n        /// - Returns: The URLs for the removed files.\n        ///\n        /// This method checks ``DiskStorage/Config/sizeLimit`` and removes cached files in an LRU\n        /// (Least Recently Used) way.\n        public func removeSizeExceededValues() throws -> [URL] {\n\n            if config.sizeLimit == 0 { return [] } // Back compatible. 0 means no limit.\n\n            var size = try totalSize()\n            if size < config.sizeLimit { return [] }\n\n            let propertyKeys: [URLResourceKey] = [\n                .isDirectoryKey,\n                .creationDateKey,\n                .fileSizeKey\n            ]\n            let keys = Set(propertyKeys)\n\n            let urls = try allFileURLs(for: propertyKeys)\n            var pendings: [FileMeta] = urls.compactMap { fileURL in\n                guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else {\n                    return nil\n                }\n                return meta\n            }\n            // Sort by last access date. Most recent file first.\n            pendings.sort(by: FileMeta.lastAccessDate)\n\n            var removed: [URL] = []\n            let target = config.sizeLimit / 2\n            while size > target, let meta = pendings.popLast() {\n                size -= UInt(meta.fileSize)\n                try removeFile(at: meta.url)\n                removed.append(meta.url)\n            }\n            return removed\n        }\n\n        /// Gets the total file size of the cache folder in bytes.\n        public func totalSize() throws -> UInt {\n            let propertyKeys: [URLResourceKey] = [.fileSizeKey]\n            let urls = try allFileURLs(for: propertyKeys)\n            let keys = Set(propertyKeys)\n            let totalSize: UInt = urls.reduce(0) { size, fileURL in\n                do {\n                    let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys)\n                    return size + UInt(meta.fileSize)\n                } catch {\n                    return size\n                }\n            }\n            return totalSize\n        }\n    }\n}\n\nextension DiskStorage {\n    \n    /// Represents the configuration used in a ``DiskStorage/Backend``.\n    public struct Config: @unchecked Sendable {\n\n        /// The file size limit on disk of the storage in bytes. \n        ///\n        /// `0` means no limit.\n        public var sizeLimit: UInt\n\n        /// The `StorageExpiration` used in this disk storage.\n        ///\n        /// The default is `.days(7)`, which means that the disk cache will expire in one week if not accessed anymore.\n        public var expiration: StorageExpiration = .days(7)\n\n        /// The preferred extension of the cache item. It will be appended to the file name as its extension.\n        ///\n        /// The default is `nil`, which means that the cache file does not contain a file extension.\n        public var pathExtension: String? = nil\n\n        /// Whether the cache file name will be hashed before storing.\n        ///\n        /// The default is `true`, which means that file name is hashed to protect user information (for example, the\n        /// original download URL which is used as the cache key).\n        public var usesHashedFileName = true\n\n        \n        /// Whether the image extension will be extracted from the original file name and appended to the hashed file\n        /// name, which will be used as the cache key on disk.\n        ///\n        /// The default is `false`.\n        public var autoExtAfterHashedFileName = false\n        \n        /// A closure that takes in the initial directory path and generates the final disk cache path.\n        ///\n        /// You can use it to fully customize your cache path.\n        public var cachePathBlock: (@Sendable (_ directory: URL, _ cacheName: String) -> URL)! = {\n            (directory, cacheName) in\n            return directory.appendingPathComponent(cacheName, isDirectory: true)\n        }\n\n        /// The desired name of the disk cache.\n        ///\n        /// This name will be used as a part of the cache folder name by default.\n        public let name: String\n        \n        let fileManager: FileManager\n        let directory: URL?\n\n        /// Creates a config value based on the given parameters.\n        ///\n        /// - Parameters:\n        ///   - name: The name of the cache. It is used as part of the storage folder and to identify the disk storage.\n        ///   Two storages with the same `name` would share the same folder on the disk, and this should be prevented.\n        ///   - sizeLimit: The size limit in bytes for all existing files in the disk storage.\n        ///   - fileManager: The `FileManager` used to manipulate files on the disk. The default is `FileManager.default`.\n        ///   - directory: The URL where the disk storage should reside. The storage will use this as the root folder,\n        ///   and append a path that is constructed by the input `name`. The default is `nil`, indicating that\n        ///   the cache directory under the user domain mask will be used.\n        public init(\n            name: String,\n            sizeLimit: UInt,\n            fileManager: FileManager = .default,\n            directory: URL? = nil)\n        {\n            self.name = name\n            self.fileManager = fileManager\n            self.directory = directory\n            self.sizeLimit = sizeLimit\n        }\n    }\n}\n\nextension DiskStorage {\n    struct FileMeta {\n    \n        let url: URL\n        \n        let lastAccessDate: Date?\n        let estimatedExpirationDate: Date?\n        let isDirectory: Bool\n        let fileSize: Int\n        \n        static func lastAccessDate(lhs: FileMeta, rhs: FileMeta) -> Bool {\n            return lhs.lastAccessDate ?? .distantPast > rhs.lastAccessDate ?? .distantPast\n        }\n        \n        init(fileURL: URL, resourceKeys: Set<URLResourceKey>) throws {\n            let meta = try fileURL.resourceValues(forKeys: resourceKeys)\n            self.init(\n                fileURL: fileURL,\n                lastAccessDate: meta.creationDate,\n                estimatedExpirationDate: meta.contentModificationDate,\n                isDirectory: meta.isDirectory ?? false,\n                fileSize: meta.fileSize ?? 0)\n        }\n        \n        init(\n            fileURL: URL,\n            lastAccessDate: Date?,\n            estimatedExpirationDate: Date?,\n            isDirectory: Bool,\n            fileSize: Int)\n        {\n            self.url = fileURL\n            self.lastAccessDate = lastAccessDate\n            self.estimatedExpirationDate = estimatedExpirationDate\n            self.isDirectory = isDirectory\n            self.fileSize = fileSize\n        }\n\n        func expired(referenceDate: Date) -> Bool {\n            return estimatedExpirationDate?.isPast(referenceDate: referenceDate) ?? true\n        }\n        \n        func extendExpiration(with fileManager: FileManager, extendingExpiration: ExpirationExtending) {\n            guard let lastAccessDate = lastAccessDate,\n                  let lastEstimatedExpiration = estimatedExpirationDate else\n            {\n                return\n            }\n\n            let attributes: [FileAttributeKey : Any]\n\n            switch extendingExpiration {\n            case .none:\n                // not extending expiration time here\n                return\n            case .cacheTime:\n                let originalExpiration: StorageExpiration =\n                    .seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate))\n                attributes = [\n                    .creationDate: Date().fileAttributeDate,\n                    .modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate\n                ]\n            case .expirationTime(let expirationTime):\n                attributes = [\n                    .creationDate: Date().fileAttributeDate,\n                    .modificationDate: expirationTime.estimatedExpirationSinceNow.fileAttributeDate\n                ]\n            }\n\n            try? fileManager.setAttributes(attributes, ofItemAtPath: url.path)\n        }\n    }\n}\n\nextension DiskStorage {\n    struct Creation {\n        let directoryURL: URL\n        let cacheName: String\n\n        init(_ config: Config) {\n            let url: URL\n            if let directory = config.directory {\n                url = directory\n            } else {\n                url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]\n            }\n\n            cacheName = \"com.onevcat.Kingfisher.ImageCache.\\(config.name)\"\n            directoryURL = config.cachePathBlock(url, cacheName)\n        }\n    }\n}\n\nfileprivate extension Error {\n    var isFolderMissing: Bool {\n        let nsError = self as NSError\n        guard nsError.domain == NSCocoaErrorDomain, nsError.code == 4 else {\n            return false\n        }\n        guard let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError else {\n            return false\n        }\n        guard underlyingError.domain == NSPOSIXErrorDomain, underlyingError.code == 2 else {\n            return false\n        }\n        return true\n    }\n}\n"
  },
  {
    "path": "Sources/Cache/FormatIndicatedCacheSerializer.swift",
    "content": "//\n//  RequestModifier.swift\n//  Kingfisher\n//\n//  Created by Junyu Kuang on 5/28/17.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport CoreGraphics\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// The ``FormatIndicatedCacheSerializer`` enables you to specify an image format for serialized caches.\n///\n/// It can serialize and deserialize PNG, JPEG, and GIF images. For images other than these formats, a normalized \n/// ``KingfisherWrapper/pngRepresentation()`` will be used.\n///\n/// **Example:**\n///\n/// ```swift\n/// let profileImageSize = CGSize(width: 44, height: 44)\n///\n/// // A round corner image.\n/// let imageProcessor = RoundCornerImageProcessor(\n///     cornerRadius: profileImageSize.width / 2, targetSize: profileImageSize)\n///\n/// let optionsInfo: KingfisherOptionsInfo = [\n///     .cacheSerializer(FormatIndicatedCacheSerializer.png),\n///     .processor(imageProcessor)\n/// ]\n///\n/// // A URL pointing to a JPEG image.\n/// let url = URL(string: \"https://example.com/image.jpg\")!\n///\n/// // The image will always be cached as PNG format to preserve the alpha channel for the round rectangle.\n/// // When you load it from the cache later, it will still be round cornered.\n/// // Otherwise, the corner part would be filled by a white color (since JPEG does not contain an alpha channel).\n/// imageView.kf.setImage(with: url, options: optionsInfo)\n/// ```\npublic struct FormatIndicatedCacheSerializer: CacheSerializer {\n    \n    /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the PNG format. \n    ///\n    /// If the image cannot be represented in the PNG format, it will fallback to its actual format determined by the\n    /// `original` data in ``CacheSerializer/data(with:original:)``.\n    public static let png = FormatIndicatedCacheSerializer(imageFormat: .PNG, jpegCompressionQuality: nil)\n    \n    /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format. If the image cannot be\n    /// represented by JPEG format, it will fallback to its real format which is determined by `original` data.\n    /// The compression quality is 1.0 when using this serializer. If you need to set a customized compression quality,\n    /// use `jpeg(compressionQuality:)`.\n    ///\n    \n    /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the JPEG format.\n    ///\n    /// If the image cannot be represented in the JPEG format, it will fallback to its actual format determined by the\n    /// `original` data in ``CacheSerializer/data(with:original:)``.\n    ///\n    /// > The compression quality is 1.0 when using this serializer. To set a customized compression quality,\n    /// use ``FormatIndicatedCacheSerializer/jpeg(compressionQuality:)``.\n    public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: 1.0)\n\n    /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the JPEG format.\n    ///\n    /// - Parameter compressionQuality: The compression quality when converting image to JPEG data.\n    ///\n    /// If the image cannot be represented in the JPEG format, it will fallback to its actual format determined by the\n    /// `original` data in ``CacheSerializer/data(with:original:)``.\n    public static func jpeg(compressionQuality: CGFloat) -> FormatIndicatedCacheSerializer {\n        return FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: compressionQuality)\n    }\n    \n    /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the GIF format.\n    ///\n    /// If the image cannot be represented in the GIF format, it will fallback to its actual format determined by the\n    /// `original` data in ``CacheSerializer/data(with:original:)``.\n    public static let gif = FormatIndicatedCacheSerializer(imageFormat: .GIF, jpegCompressionQuality: nil)\n    \n    // The specified image format.\n    private let imageFormat: ImageFormat\n\n    // The compression quality used for lossy image formats (like JPEG).\n    private let jpegCompressionQuality: CGFloat?\n    \n    public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {\n        \n        func imageData(withFormat imageFormat: ImageFormat) -> Data? {\n            return autoreleasepool { () -> Data? in\n                switch imageFormat {\n                case .PNG: return image.kf.pngRepresentation()\n                case .JPEG: return image.kf.jpegRepresentation(compressionQuality: jpegCompressionQuality ?? 1.0)\n                case .GIF: return image.kf.gifRepresentation()\n                case .unknown: return nil\n                }\n            }\n        }\n        \n        // generate data with indicated image format\n        if let data = imageData(withFormat: imageFormat) {\n            return data\n        }\n        \n        let originalFormat = original?.kf.imageFormat ?? .unknown\n        \n        // generate data with original image's format\n        if originalFormat != imageFormat, let data = imageData(withFormat: originalFormat) {\n            return data\n        }\n        \n        return original ?? image.kf.normalized.kf.pngRepresentation()\n    }\n    \n    public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)\n    }\n}\n"
  },
  {
    "path": "Sources/Cache/ImageCache.swift",
    "content": "//\n//  ImageCache.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\nextension Notification.Name {\n    \n    /// This notification is sent when the disk cache is cleared, either due to expired cached files or the total size\n    /// exceeding the maximum allowed size.\n    ///\n    /// The `object` of this notification is the ``ImageCache`` object that sends the notification. You can retrieve a\n    /// list of removed hashes (files) by accessing the array under the ``KingfisherDiskCacheCleanedHashKey`` key in\n    /// the `userInfo` of the received notification object. By checking the array, you can determine the hash codes\n    /// of the removed files.\n    /// \n    /// > Invoking the `clearDiskCache` method manually will not trigger this notification.\n    public static let KingfisherDidCleanDiskCache =\n        Notification.Name(\"com.onevcat.Kingfisher.KingfisherDidCleanDiskCache\")\n}\n\n/// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCache` notification.\npublic let KingfisherDiskCacheCleanedHashKey = \"com.onevcat.Kingfisher.cleanedHash\"\n\n/// The type of cache for a cached image.\npublic enum CacheType: Sendable {\n    /// The image is not yet cached when retrieving it.\n    ///\n    /// This indicates that the image was recently downloaded or generated rather than being retrieved from either\n    /// memory or disk cache.\n    case none\n    \n    /// The image is cached in memory and retrieved from there.\n    case memory\n    \n    /// The image is cached in disk and retrieved from there.\n    case disk\n    \n    /// Indicates whether the cache type represents the image is already cached or not.\n    public var cached: Bool {\n        switch self {\n        case .memory, .disk: return true\n        case .none: return false\n        }\n    }\n}\n\n/// Represents the result of the caching operation.\npublic struct CacheStoreResult: Sendable {\n    \n    /// The caching result for memory cache.\n    ///\n    /// Caching an image to memory will never fail.\n    public let memoryCacheResult: Result<(), Never>\n    \n    /// The caching result for disk cache.\n    ///\n    /// If an error occurs during the caching operation, you can retrieve it from the `.failure` case of this value.\n    /// Usually, the error contains a ``KingfisherError/CacheErrorReason``.\n    public let diskCacheResult: Result<(), KingfisherError>\n}\n\nextension KFCrossPlatformImage: CacheCostCalculable {\n    /// The cost of an image.\n    ///\n    /// It is an estimated size represented as a bitmap, measured in bytes of all pixels. A larger cost indicates that\n    /// when cached in memory, it occupies more memory space. This cost contributes to the\n    /// ``MemoryStorage/Config/countLimit``.\n    public var cacheCost: Int { return kf.cost }\n}\n\nextension Data: DataTransformable {\n    public func toData() throws -> Data {\n        self\n    }\n\n    public static func fromData(_ data: Data) throws -> Data {\n        data\n    }\n\n    public static let empty = Data()\n}\n\n\n/// Represents the result of the operation to retrieve an image from the cache.\npublic enum ImageCacheResult: Sendable {\n    \n    /// The image can be retrieved from the disk cache.\n    case disk(KFCrossPlatformImage)\n    \n    /// The image can be retrieved from the memory cache.\n    case memory(KFCrossPlatformImage)\n    \n    /// The image does not exist in the cache.\n    case none\n    \n    /// Extracts the image from cache result. \n    ///\n    /// It returns the associated `Image` value for ``ImageCacheResult/disk(_:)`` and ``ImageCacheResult/memory(_:)``\n    /// case. For ``ImageCacheResult/none`` case, returns `nil`.\n    public var image: KFCrossPlatformImage? {\n        switch self {\n        case .disk(let image): return image\n        case .memory(let image): return image\n        case .none: return nil\n        }\n    }\n    \n    /// Returns the corresponding ``CacheType`` value based on the result type of `self`.\n    public var cacheType: CacheType {\n        switch self {\n        case .disk: return .disk\n        case .memory: return .memory\n        case .none: return .none\n        }\n    }\n}\n\n/// Represents a hybrid caching system composed of a ``MemoryStorage`` and a ``DiskStorage``.\n///\n/// ``ImageCache`` serves as a high-level abstraction for storing an image and its data in memory and on disk, as well\n/// as retrieving them. You can define configurations for the memory cache backend and disk cache backend, and the the\n/// unified methods to store images to the cache or retrieve images from either the memory cache or the disk cache.\n///\n/// > While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create\n/// your own cache object and configure its storages according to your needs. This class also provides an interface for\n/// configuring the memory and disk storage.\nopen class ImageCache: @unchecked Sendable {\n\n    // MARK: Singleton\n    /// The default ``ImageCache`` object.\n    ///\n    /// Kingfisher uses this value for its related methods if no other cache is specified. \n    ///\n    /// > Warning: The `name` of this default cache is reserved as \"default\", and you should not use this name for any\n    /// of your custom caches. Otherwise, different caches might become mixed up and corrupted.\n    public static let `default` = ImageCache(name: \"default\")\n\n    // MARK: Public Properties\n    /// The ``MemoryStorage/Backend`` object for the memory cache used in this cache.\n    ///\n    /// This storage stores loaded images in memory with a reasonable expire duration and a maximum memory usage.\n    ///\n    /// > To modify the configuration of a storage, just set the storage ``MemoryStorage/Config`` and its properties.\n    public let memoryStorage: MemoryStorage.Backend<KFCrossPlatformImage>\n    \n    /// The ``DiskStorage/Backend`` object for the disk cache used in this cache.\n    ///\n    /// This storage stores loaded images on disk with a reasonable expire duration and a maximum disk usage.\n    ///\n    /// > To modify the configuration of a storage, just set the storage ``DiskStorage/Config`` and its properties.\n    public let diskStorage: DiskStorage.Backend<Data>\n    \n    private let ioQueue: DispatchQueue\n    \n    /// A closure that specifies the disk cache path based on a given path and the cache name.\n    public typealias DiskCachePathClosure = @Sendable (URL, String) -> URL\n\n    // MARK: Initializers\n\n    /// Creates an ``ImageCache`` with a customized ``MemoryStorage`` and ``DiskStorage``.\n    ///\n    /// - Parameters:\n    ///   - memoryStorage: The ``MemoryStorage/Backend`` object to be used in the image memory cache.\n    ///   - diskStorage: The ``DiskStorage/Backend`` object to be used in the image disk cache.\n    public init(\n        memoryStorage: MemoryStorage.Backend<KFCrossPlatformImage>,\n        diskStorage: DiskStorage.Backend<Data>)\n    {\n        self.memoryStorage = memoryStorage\n        self.diskStorage = diskStorage\n        let ioQueueName = \"com.onevcat.Kingfisher.ImageCache.ioQueue.\\(UUID().uuidString)\"\n        ioQueue = DispatchQueue(label: ioQueueName)\n\n        Task { @MainActor in\n            let notifications: [(Notification.Name, Selector)]\n            #if !os(macOS) && !os(watchOS)\n            notifications = [\n                (UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)),\n                (UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)),\n                (UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache))\n            ]\n            #elseif os(macOS)\n            notifications = [\n                (NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)),\n            ]\n            #else\n            notifications = []\n            #endif\n            notifications.forEach {\n                NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil)\n            }\n        }\n    }\n    \n    /// Creates an ``ImageCache`` with a given `name`.\n    ///\n    /// Both the ``MemoryStorage`` and the ``DiskStorage`` will be created with a default configuration based on the `name`.\n    ///\n    /// - Parameter name: The name of the cache object. It is used to set up disk cache directories and IO queues. \n    /// You should not use the same `name` for different caches; otherwise, the disk storages would conflict with each\n    /// other. The `name` should not be an empty string.\n    ///\n    /// > Warning: The `name` \"default\" is reserved to be used as the name of ``ImageCache/default`` in Kingfisher,\n    /// and you should not use this name for any of your custom caches. Otherwise, different caches might become mixed\n    /// up and corrupted.\n    public convenience init(name: String) {\n        self.init(noThrowName: name, cacheDirectoryURL: nil, diskCachePathClosure: nil)\n    }\n\n    /// Creates an ``ImageCache`` with a given `name`, the cache directory `path`, and a closure to modify the cache\n    /// directory.\n    ///\n    /// - Parameters:\n    ///   - name: The name of the cache object. It is used to set up disk cache directories and IO queues.\n    /// You should not use the same `name` for different caches; otherwise, the disk storages would conflict with each\n    /// other. The `name` should not be an empty string.\n    ///   - cacheDirectoryURL: The location of the cache directory URL on disk. It will be passed internally to the \n    ///   initializer of the ``DiskStorage`` as the disk cache directory. If `nil`, the cache directory under the user\n    ///   domain mask will be used.\n    ///   - diskCachePathClosure: A closure that takes in an optional initial path string and generates the final disk \n    ///   cache path. You can use it to fully customize your cache path.\n    /// - Throws: An error that occurs during the creation of the image cache, such as being unable to create a \n    /// directory at the given path.\n    ///\n    /// > Warning: The `name` \"default\" is reserved to be used as the name of ``ImageCache/default`` in Kingfisher,\n    /// and you should not use this name for any of your custom caches. Otherwise, different caches might become mixed\n    /// up and corrupted.\n    public convenience init(\n        name: String,\n        cacheDirectoryURL: URL?,\n        diskCachePathClosure: DiskCachePathClosure? = nil\n    ) throws\n    {\n        if name.isEmpty {\n            fatalError(\"[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.\")\n        }\n\n        let memoryStorage = ImageCache.createMemoryStorage()\n\n        let config = ImageCache.createConfig(\n            name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure\n        )\n        let diskStorage = try DiskStorage.Backend<Data>(config: config)\n        self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)\n    }\n\n    convenience init(\n        noThrowName name: String,\n        cacheDirectoryURL: URL?,\n        diskCachePathClosure: DiskCachePathClosure?\n    )\n    {\n        if name.isEmpty {\n            fatalError(\"[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.\")\n        }\n\n        let memoryStorage = ImageCache.createMemoryStorage()\n\n        let config = ImageCache.createConfig(\n            name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure\n        )\n        let diskStorage = DiskStorage.Backend<Data>(noThrowConfig: config, creatingDirectory: true)\n        self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)\n    }\n\n    private static func createMemoryStorage() -> MemoryStorage.Backend<KFCrossPlatformImage> {\n        let totalMemory = ProcessInfo.processInfo.physicalMemory\n        let costLimit = totalMemory / 4\n        let memoryStorage = MemoryStorage.Backend<KFCrossPlatformImage>(config:\n            .init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit)))\n        return memoryStorage\n    }\n\n    private static func createConfig(\n        name: String,\n        cacheDirectoryURL: URL?,\n        diskCachePathClosure: DiskCachePathClosure? = nil\n    ) -> DiskStorage.Config\n    {\n        var diskConfig = DiskStorage.Config(\n            name: name,\n            sizeLimit: 0,\n            directory: cacheDirectoryURL\n        )\n        if let closure = diskCachePathClosure {\n            diskConfig.cachePathBlock = closure\n        }\n        return diskConfig\n    }\n    \n    deinit {\n        NotificationCenter.default.removeObserver(self)\n    }\n\n    // MARK: Storing Images\n    \n    /// Stores an image to the cache.\n    ///\n    /// - Parameters:\n    ///   - image: The image that to be stored.\n    ///   - original: The original data of the image. This value will be forwarded to the provided `serializer` for\n    ///   further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for\n    ///   caching in disk. It checks the image format based on the `original` data to determine the appropriate image\n    ///   format to use. For other types of `serializer`, it depends on their implementation details on how to use this\n    ///   original data.\n    ///   - key: The key used for caching the image.\n    ///   - options: The options which contains configurations for caching the image.\n    ///   - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory.\n    ///   Otherwise, it is cached in both memory storage and disk storage. The default is `true`.\n    ///   - completionHandler: A closure which is invoked when the cache operation finishes.\n    open func store(\n        _ image: KFCrossPlatformImage,\n        original: Data? = nil,\n        forKey key: String,\n        options: KingfisherParsedOptionsInfo,\n        toDisk: Bool = true,\n        completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil\n    )\n    {\n        let identifier = options.processor.identifier\n        let callbackQueue = options.callbackQueue\n        \n        let computedKey = key.computedKey(with: identifier)\n        // Memory storage should not throw.\n        memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)\n        \n        guard toDisk else {\n            if let completionHandler = completionHandler {\n                let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))\n                callbackQueue.execute { completionHandler(result) }\n            }\n            return\n        }\n        \n        ioQueue.async {\n            let serializer = options.cacheSerializer\n            if let data = serializer.data(with: image, original: original) {\n                self.syncStoreToDisk(\n                    data,\n                    forKey: key,\n                    forcedExtension: options.forcedExtension,\n                    processorIdentifier: identifier,\n                    callbackQueue: callbackQueue,\n                    expiration: options.diskCacheExpiration,\n                    writeOptions: options.diskStoreWriteOptions,\n                    completionHandler: completionHandler)\n            } else {\n                guard let completionHandler = completionHandler else { return }\n                \n                let diskError = KingfisherError.cacheError(\n                    reason: .cannotSerializeImage(image: image, original: original, serializer: serializer))\n                let result = CacheStoreResult(\n                    memoryCacheResult: .success(()),\n                    diskCacheResult: .failure(diskError))\n                callbackQueue.execute { completionHandler(result) }\n            }\n        }\n    }\n\n    /// Stores an image in the cache.\n    ///\n    /// - Parameters:\n    ///   - image: The image to be stored.\n    ///   - original: The original data of the image. This value will be forwarded to the provided `serializer` for \n    ///   further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for\n    ///   caching in disk. It checks the image format based on the `original` data to determine the appropriate image\n    ///   format to use. For other types of `serializer`, it depends on their implementation details on how to use this\n    ///   original data.\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The identifier of the processor being used for caching. If you are using a processor for the \n    ///   image, pass the identifier of the processor to this parameter.\n    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the\n    ///   disk storage configuration instead.\n    ///   - serializer: The ``CacheSerializer`` used to convert the `image` and `original` to the data that will be\n    ///   stored to disk. By default, the ``DefaultCacheSerializer/default`` will be used.\n    ///   - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory.\n    ///   Otherwise, it is cached in both memory storage and disk storage. The default is `true`.\n    ///   - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is\n    ///   ``CallbackQueue/untouch``. Under this default ``CallbackQueue/untouch`` queue, if `toDisk` is `false`, it\n    ///   means the `completionHandler` will be invoked from the caller queue of this method; if `toDisk` is `true`,\n    ///   the `completionHandler` will be called from an internal file IO queue. To change this behavior, specify\n    ///   another ``CallbackQueue`` value.\n    ///   - completionHandler: A closure that is invoked when the cache operation finishes.\n    open func store(\n        _ image: KFCrossPlatformImage,\n        original: Data? = nil,\n        forKey key: String,\n        processorIdentifier identifier: String = \"\",\n        forcedExtension: String? = nil,\n        cacheSerializer serializer: any CacheSerializer = DefaultCacheSerializer.default,\n        toDisk: Bool = true,\n        callbackQueue: CallbackQueue = .untouch,\n        completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil\n    )\n    {\n        struct TempProcessor: ImageProcessor {\n            let identifier: String\n            func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n                return nil\n            }\n        }\n        \n        let options = KingfisherParsedOptionsInfo([\n            .processor(TempProcessor(identifier: identifier)),\n            .cacheSerializer(serializer),\n            .callbackQueue(callbackQueue),\n            .forcedCacheFileExtension(forcedExtension)\n        ])\n        store(\n            image,\n            original: original,\n            forKey: key,\n            options: options,\n            toDisk: toDisk,\n            completionHandler: completionHandler\n        )\n    }\n    \n    /// Store some data to the disk.\n    /// \n    /// - Parameters:\n    ///   - data: The data to be stored.\n    ///   - key: The key used for caching the data.\n    ///   - identifier: The identifier of the processor being used for caching. If you are using a processor for the\n    ///   image, pass the identifier of the processor to this parameter.\n    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the\n    ///   disk storage configuration instead.\n    ///   - expiration: The expiration policy used by this storage action.\n    ///   - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is\n    ///   ``CallbackQueue/untouch``. Under this default ``CallbackQueue/untouch`` queue, if `toDisk` is `false`, it\n    ///   means the `completionHandler` will be invoked from the caller queue of this method; if `toDisk` is `true`,\n    ///   the `completionHandler` will be called from an internal file IO queue. To change this behavior, specify\n    ///   another ``CallbackQueue`` value.\n    ///   - completionHandler: A closure that is invoked when the cache operation finishes.\n    open func storeToDisk(\n        _ data: Data,\n        forKey key: String,\n        processorIdentifier identifier: String = \"\",\n        forcedExtension: String? = nil,\n        expiration: StorageExpiration? = nil,\n        callbackQueue: CallbackQueue = .untouch,\n        completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil)\n    {\n        ioQueue.async {\n            self.syncStoreToDisk(\n                data,\n                forKey: key,\n                forcedExtension: forcedExtension,\n                processorIdentifier: identifier,\n                callbackQueue: callbackQueue,\n                expiration: expiration,\n                completionHandler: completionHandler\n            )\n        }\n    }\n    \n    private func syncStoreToDisk(\n        _ data: Data,\n        forKey key: String,\n        forcedExtension: String?,\n        processorIdentifier identifier: String = \"\",\n        callbackQueue: CallbackQueue = .untouch,\n        expiration: StorageExpiration? = nil,\n        writeOptions: Data.WritingOptions = [],\n        completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil)\n    {\n        let computedKey = key.computedKey(with: identifier)\n        let result: CacheStoreResult\n        do {\n            try self.diskStorage.store(\n                value: data,\n                forKey: computedKey,\n                expiration: expiration,\n                writeOptions: writeOptions,\n                forcedExtension: forcedExtension\n            )\n            result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))\n        } catch {\n            let diskError: KingfisherError\n            if let error = error as? KingfisherError {\n                diskError = error\n            } else {\n                diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error))\n            }\n            \n            result = CacheStoreResult(\n                memoryCacheResult: .success(()),\n                diskCacheResult: .failure(diskError)\n            )\n        }\n        if let completionHandler = completionHandler {\n            callbackQueue.execute { completionHandler(result) }\n        }\n    }\n\n    // MARK: Removing Images\n\n    /// Removes the image for the given key from the cache.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The identifier of the processor being used for caching. If you are using a processor for the \n    ///   image, pass the identifier of the processor to this parameter.\n    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the\n    ///   disk storage configuration instead.\n    ///   - fromMemory: Whether this image should be removed from memory storage or not. If `false`, the image won't be \n    ///   removed from the memory storage. The default is `true`.\n    ///   - fromDisk: Whether this image should be removed from the disk storage or not. If `false`, the image won't be\n    ///    removed from the disk storage. The default is `true`.\n    ///   - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is\n    ///   ``CallbackQueue/untouch``.\n    ///   - completionHandler: A closure that is invoked when the cache removal operation finishes.\n    open func removeImage(\n        forKey key: String,\n        processorIdentifier identifier: String = \"\",\n        forcedExtension: String? = nil,\n        fromMemory: Bool = true,\n        fromDisk: Bool = true,\n        callbackQueue: CallbackQueue = .untouch,\n        completionHandler: (@Sendable () -> Void)? = nil\n    )\n    {\n        removeImage(\n            forKey: key,\n            processorIdentifier: identifier,\n            forcedExtension: forcedExtension,\n            fromMemory: fromMemory,\n            fromDisk: fromDisk,\n            callbackQueue: callbackQueue,\n            completionHandler: { _ in completionHandler?() } // This is a version which ignores error.\n        )\n    }\n    \n    func removeImage(\n        forKey key: String,\n        processorIdentifier identifier: String = \"\",\n        forcedExtension: String?,\n        fromMemory: Bool = true,\n        fromDisk: Bool = true,\n        callbackQueue: CallbackQueue = .untouch,\n        completionHandler: (@Sendable ((any Error)?) -> Void)? = nil)\n    {\n        let computedKey = key.computedKey(with: identifier)\n\n        if fromMemory {\n            memoryStorage.remove(forKey: computedKey)\n        }\n        \n        @Sendable func callHandler(_ error: (any Error)?) {\n            if let completionHandler = completionHandler {\n                callbackQueue.execute { completionHandler(error) }\n            }\n        }\n        \n        if fromDisk {\n            ioQueue.async{\n                do {\n                    try self.diskStorage.remove(forKey: computedKey, forcedExtension: forcedExtension)\n                    callHandler(nil)\n                } catch {\n                    callHandler(error)\n                }\n            }\n        } else {\n            callHandler(nil)\n        }\n    }\n\n    // MARK: Getting Images\n\n    /// Retrieves an image for a given key from the cache, either from memory storage or disk storage.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - options: The ``KingfisherParsedOptionsInfo`` options setting used for retrieving the image.\n    ///   - callbackQueue: The callback queue on which the `completionHandler` is invoked.\n    ///   The default is ``CallbackQueue/mainCurrentOrAsync``.\n    ///   - completionHandler: A closure that is invoked when the image retrieval operation finishes. If the image \n    ///   retrieval operation finishes without any problems, an ``ImageCacheResult`` value will be sent to this closure\n    ///   as a result. Otherwise, a ``KingfisherError`` result with detailed failure reason will be sent.\n    open func retrieveImage(\n        forKey key: String,\n        options: KingfisherParsedOptionsInfo,\n        callbackQueue: CallbackQueue = .mainCurrentOrAsync,\n        completionHandler: (@Sendable (Result<ImageCacheResult, KingfisherError>) -> Void)?)\n    {\n        // No completion handler. No need to start working and early return.\n        guard let completionHandler = completionHandler else { return }\n\n        // Try to check the image from memory cache first.\n        if let image = retrieveImageInMemoryCache(forKey: key, options: options) {\n            callbackQueue.execute { completionHandler(.success(.memory(image))) }\n        } else if options.fromMemoryCacheOrRefresh {\n            callbackQueue.execute { completionHandler(.success(.none)) }\n        } else {\n\n            // Begin to disk search.\n            self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) {\n                result in\n                switch result {\n                case .success(let image):\n\n                    guard let image = image else {\n                        // No image found in disk storage.\n                        callbackQueue.execute { completionHandler(.success(.none)) }\n                        return\n                    }\n\n                    // Cache the disk image to memory.\n                    // We are passing `false` to `toDisk`, the memory cache does not change\n                    // callback queue, we can call `completionHandler` without another dispatch.\n                    var cacheOptions = options\n                    cacheOptions.callbackQueue = .untouch\n                    self.store(\n                        image,\n                        forKey: key,\n                        options: cacheOptions,\n                        toDisk: false)\n                    {\n                        _ in\n                        callbackQueue.execute { completionHandler(.success(.disk(image))) }\n                    }\n                case .failure(let error):\n                    callbackQueue.execute { completionHandler(.failure(error)) }\n                }\n            }\n        }\n    }\n\n    /// Retrieves an image for a given key from the cache, either from memory storage or disk storage.\n    ///\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - options: The ``KingfisherOptionsInfo`` options setting used for retrieving the image.\n    ///   - callbackQueue: The callback queue on which the `completionHandler` is invoked.\n    ///   The default is ``CallbackQueue/mainCurrentOrAsync``.\n    ///   - completionHandler: A closure that is invoked when the image retrieval operation finishes. If the image\n    ///   retrieval operation finishes without any problems, an ``ImageCacheResult`` value will be sent to this closure\n    ///   as a result. Otherwise, a ``KingfisherError`` result with detailed failure reason will be sent.\n    ///\n    /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead,\n    /// override the version ``ImageCache/retrieveImageInDiskCache(forKey:options:callbackQueue:completionHandler:)``\n    /// accepts a ``KingfisherParsedOptionsInfo`` value.\n    open func retrieveImage(\n        forKey key: String,\n        options: KingfisherOptionsInfo? = nil,\n        callbackQueue: CallbackQueue = .mainCurrentOrAsync,\n        completionHandler: (@Sendable (Result<ImageCacheResult, KingfisherError>) -> Void)?\n    )\n    {\n        retrieveImage(\n            forKey: key,\n            options: KingfisherParsedOptionsInfo(options),\n            callbackQueue: callbackQueue,\n            completionHandler: completionHandler)\n    }\n\n    /// Retrieves an image associated with a given key from the memory storage.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - options: The ``KingfisherParsedOptionsInfo`` options setting used to fetch the image.\n    /// - Returns: The image stored in the memory cache if it exists and is valid. If the image does not exist or has\n    ///  already expired, `nil` is returned.\n    open func retrieveImageInMemoryCache(\n        forKey key: String,\n        options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?\n    {\n        let computedKey = key.computedKey(with: options.processor.identifier)\n        return memoryStorage.value(\n            forKey: computedKey,\n            extendingExpiration: options.memoryCacheAccessExtendingExpiration\n        )\n    }\n\n    /// Retrieves an image associated with a given key from the memory storage.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image.\n    /// - Returns: The image stored in the memory cache if it exists and is valid. If the image does not exist or has\n    ///  already expired, `nil` is returned.\n    ///\n    /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead,\n    /// override the version ``ImageCache/retrieveImageInMemoryCache(forKey:options:)-2xj0`` that accepts a\n    ///  ``KingfisherParsedOptionsInfo`` value.\n    open func retrieveImageInMemoryCache(\n        forKey key: String,\n        options: KingfisherOptionsInfo? = nil) -> KFCrossPlatformImage?\n    {\n        return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options))\n    }\n\n    func retrieveImageInDiskCache(\n        forKey key: String,\n        options: KingfisherParsedOptionsInfo,\n        callbackQueue: CallbackQueue = .untouch,\n        completionHandler: @escaping @Sendable (Result<KFCrossPlatformImage?, KingfisherError>) -> Void)\n    {\n        let computedKey = key.computedKey(with: options.processor.identifier)\n        let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue)\n        loadingQueue.execute {\n            do {\n                var image: KFCrossPlatformImage? = nil\n                if let data = try self.diskStorage.value(\n                    forKey: computedKey,\n                    forcedExtension: options.forcedExtension,\n                    extendingExpiration: options.diskCacheAccessExtendingExpiration\n                ) {\n                    image = options.cacheSerializer.image(with: data, options: options)\n                }\n                if options.backgroundDecode {\n                    image = image?.kf.decoded(scale: options.scaleFactor)\n                }\n                callbackQueue.execute { [image] in completionHandler(.success(image)) }\n            } catch let error as KingfisherError {\n                callbackQueue.execute { completionHandler(.failure(error)) }\n            } catch {\n                assertionFailure(\"The internal thrown error should be a `KingfisherError`.\")\n            }\n        }\n    }\n    \n    /// Retrieves an image associated with a given key from the disk storage.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image.\n    ///   - callbackQueue: The callback queue on which the `completionHandler` is invoked.\n    ///   The default is ``CallbackQueue/untouch``.\n    ///   - completionHandler: A closure that is invoked when the operation is finished.\n    open func retrieveImageInDiskCache(\n        forKey key: String,\n        options: KingfisherOptionsInfo? = nil,\n        callbackQueue: CallbackQueue = .untouch,\n        completionHandler: @escaping @Sendable (Result<KFCrossPlatformImage?, KingfisherError>) -> Void)\n    {\n        retrieveImageInDiskCache(\n            forKey: key,\n            options: KingfisherParsedOptionsInfo(options),\n            callbackQueue: callbackQueue,\n            completionHandler: completionHandler)\n    }\n\n    // MARK: Cleaning\n    /// Clears the memory and disk storage of this cache. \n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked.\n    ///\n    /// - Parameter handler: A closure that is invoked when the cache clearing operation finishes.\n    ///                      This `handler` will be called from the main queue.\n    public func clearCache(completion handler: (@Sendable () -> Void)? = nil) {\n        clearMemoryCache()\n        clearDiskCache(completion: handler)\n    }\n    \n    /// Clears the memory storage of this cache.\n    @objc public func clearMemoryCache() {\n        memoryStorage.removeAll()\n    }\n    \n    /// Clears the disk storage of this cache. \n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked.\n    ///\n    /// - Parameter handler: A closure that is invoked when the cache clearing operation finishes.\n    ///                      This `handler` will be called from the main queue.\n    open func clearDiskCache(completion handler: (@Sendable () -> Void)? = nil) {\n        ioQueue.async {\n            do {\n                try self.diskStorage.removeAll()\n            } catch _ { }\n            if let handler = handler {\n                DispatchQueue.main.async { handler() }\n            }\n        }\n    }\n    \n    /// Clears the expired images from the memory and disk storage.\n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked.\n    open func cleanExpiredCache(completion handler: (@Sendable () -> Void)? = nil) {\n        cleanExpiredMemoryCache()\n        cleanExpiredDiskCache(completion: handler)\n    }\n\n    /// Clears the expired images from the memory storage.\n    open func cleanExpiredMemoryCache() {\n        memoryStorage.removeExpired()\n    }\n    \n    /// Clears the expired images from disk storage. \n    ///\n    /// This is an async operation.\n    @objc func cleanExpiredDiskCache() {\n        cleanExpiredDiskCache(completion: nil)\n    }\n\n    /// Clears the expired images from disk storage.\n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked.\n    ///\n    /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.\n    ///                      This `handler` will be called from the main queue.\n    open func cleanExpiredDiskCache(completion handler: (@Sendable () -> Void)? = nil) {\n        ioQueue.async {\n            do {\n                var removed: [URL] = []\n                let removedExpired = try self.diskStorage.removeExpiredValues()\n                removed.append(contentsOf: removedExpired)\n\n                let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues()\n                removed.append(contentsOf: removedSizeExceeded)\n\n                if !removed.isEmpty {\n                    DispatchQueue.main.async { [removed] in\n                        let cleanedHashes = removed.map { $0.lastPathComponent }\n                        NotificationCenter.default.post(\n                            name: .KingfisherDidCleanDiskCache,\n                            object: self,\n                            userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])\n                    }\n                }\n\n                if let handler = handler {\n                    DispatchQueue.main.async { handler() }\n                }\n            } catch {}\n        }\n    }\n\n#if !os(macOS) && !os(watchOS)\n    /// Clears the expired images from disk storage when the app is in the background. \n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked.\n    ///\n    /// In most cases, you should not call this method explicitly. It will be called automatically when a\n    ///  `UIApplicationDidEnterBackgroundNotification` is received.\n    @MainActor\n    @objc public func backgroundCleanExpiredDiskCache() {\n        // if 'sharedApplication()' is unavailable, then return\n        guard let sharedApplication = KingfisherWrapper<UIApplication>.shared else { return }\n\n        actor BackgroundTaskState {\n            private var value: UIBackgroundTaskIdentifier? = nil\n\n            func setValue(_ newValue: UIBackgroundTaskIdentifier) {\n                value = newValue\n            }\n\n            func takeValidValueAndInvalidate() -> UIBackgroundTaskIdentifier? {\n                guard let task = value, task != .invalid else { return nil }\n                value = .invalid\n                return task\n            }\n        }\n\n        let taskState = BackgroundTaskState()\n\n        let endBackgroundTaskIfNeeded: @Sendable () -> Void = {\n            Task { @MainActor in\n                guard let bgTask = await taskState.takeValidValueAndInvalidate() else { return }\n                guard let sharedApplication = KingfisherWrapper<UIApplication>.shared else { return }\n                #if compiler(>=6)\n                sharedApplication.endBackgroundTask(bgTask)\n                #else\n                await sharedApplication.endBackgroundTask(bgTask)\n                #endif\n            }\n        }\n\n        let createdTask = sharedApplication.beginBackgroundTask(\n            withName: \"Kingfisher:backgroundCleanExpiredDiskCache\",\n            expirationHandler: endBackgroundTaskIfNeeded\n        )\n\n        Task { await taskState.setValue(createdTask) }\n\n        cleanExpiredDiskCache {\n            Task { @MainActor in\n                endBackgroundTaskIfNeeded()\n            }\n        }\n    }\n#endif\n\n    // MARK: Image Cache State\n\n    /// Returns the cache type for a given `key` and `identifier` combination.\n    ///\n    /// This method is used to check whether an image is cached in the current cache. It also provides information on\n    ///  which kind of cache the image can be found in the return value.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The processor identifier used for this image. The default value is the\n    ///    ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.\n    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the\n    ///   disk storage configuration instead.\n    /// \n    /// - Returns: A ``CacheType`` instance that indicates the cache status. ``CacheType/none`` indicates that the\n    /// image is not in the cache or that it has already expired.\n    open func imageCachedType(\n        forKey key: String,\n        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,\n        forcedExtension: String? = nil\n    ) -> CacheType\n    {\n        let computedKey = key.computedKey(with: identifier)\n        if memoryStorage.isCached(forKey: computedKey) { return .memory }\n        if diskStorage.isCached(forKey: computedKey, forcedExtension: forcedExtension) { return .disk }\n        return .none\n    }\n\n    /// Checks cache type for a given key and processor identifier combination asynchronously.\n    ///\n    /// This method is an opt-in alternative to ``imageCachedType(forKey:processorIdentifier:forcedExtension:)``.\n    /// It performs any disk existence/meta check on the cache's I/O queue to avoid blocking the calling thread.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The processor identifier used for this image. The default value is the\n    ///     ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.\n    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the\n    ///     disk storage configuration instead.\n    ///   - callbackQueue: The callback queue on which the `completionHandler` is invoked. Default is `.mainCurrentOrAsync`.\n    ///   - completionHandler: Called with the resolved ``CacheType``.\n    public func imageCachedTypeAsync(\n        forKey key: String,\n        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,\n        forcedExtension: String? = nil,\n        callbackQueue: CallbackQueue = .mainCurrentOrAsync,\n        completionHandler: @escaping @Sendable (CacheType) -> Void\n    ) {\n        let computedKey = key.computedKey(with: identifier)\n\n        // Memory cache check remains synchronous (no I/O).\n        if memoryStorage.isCached(forKey: computedKey) {\n            callbackQueue.execute {\n                completionHandler(.memory)\n            }\n            return\n        }\n\n        // Disk cache check on the I/O queue.\n        ioQueue.async { [weak self] in\n            guard let self else {\n                callbackQueue.execute {\n                    completionHandler(.none)\n                }\n                return\n            }\n\n            let cached = self.diskStorage.isCached(forKey: computedKey, forcedExtension: forcedExtension)\n            let result: CacheType = cached ? .disk : .none\n            callbackQueue.execute {\n                completionHandler(result)\n            }\n        }\n    }\n\n    /// Checks cache type for a given key and processor identifier combination asynchronously.\n    ///\n    /// This is an `async`/`await` convenience wrapper of\n    /// ``imageCachedTypeAsync(forKey:processorIdentifier:forcedExtension:callbackQueue:completionHandler:)``.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The processor identifier used for this image.\n    ///   - forcedExtension: The expected extension of the file.\n    ///\n    /// - Returns: A ``CacheType`` instance that indicates the cache status.\n    public func imageCachedTypeAsync(\n        forKey key: String,\n        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,\n        forcedExtension: String? = nil\n    ) async -> CacheType {\n        await withCheckedContinuation { continuation in\n            imageCachedTypeAsync(\n                forKey: key,\n                processorIdentifier: identifier,\n                forcedExtension: forcedExtension,\n                callbackQueue: .untouch\n            ) { cacheType in\n                continuation.resume(returning: cacheType)\n            }\n        }\n    }\n    \n    /// Returns whether the file exists in the cache for a given `key` and `identifier` combination.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The processor identifier used for this image. The default value is the\n    ///    ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.\n    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the\n    ///   disk storage configuration instead.\n    ///\n    /// - Returns: A `Bool` value indicating whether a cache matches the given `key` and `identifier` combination.\n    ///\n    /// > The return value does not contain information about the kind of storage the cache matches from.\n    /// > To obtain information about the cache type according to ``CacheType``, use\n    ///  ``ImageCache/imageCachedType(forKey:processorIdentifier:forcedExtension:)`` instead.\n    public func isCached(\n        forKey key: String,\n        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,\n        forcedExtension: String? = nil\n    ) -> Bool\n    {\n        return imageCachedType(forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension).cached\n    }\n    \n    /// Retrieves the hash used as the cache file name for the key.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The processor identifier used for this image. The default value is the\n    ///    ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.\n    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the\n    ///   disk storage configuration instead.\n    /// \n    /// - Returns: The hash used as the cache file name.\n    ///\n    /// > By default, for a given combination of `key` and `identifier`, the ``ImageCache`` instance uses the value\n    /// returned by this method as the cache file name. You can use this value to check and match the cache file if \n    /// needed.\n    open func hash(\n        forKey key: String,\n        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,\n        forcedExtension: String? = nil\n    ) -> String\n    {\n        let computedKey = key.computedKey(with: identifier)\n        return diskStorage.cacheFileName(forKey: computedKey, forcedExtension: forcedExtension)\n    }\n    \n    /// Calculates the size taken by the disk storage.\n    ///\n    /// It represents the total file size of all cached files in the ``ImageCache/diskStorage`` on disk in bytes.\n    ///\n    /// - Parameter handler: Called when the size calculation is complete. This closure is invoked from the main queue.\n    open func calculateDiskStorageSize(\n        completion handler: @escaping (@Sendable (Result<UInt, KingfisherError>) -> Void)\n    ) {\n        ioQueue.async {\n            do {\n                let size = try self.diskStorage.totalSize()\n                DispatchQueue.main.async { handler(.success(size)) }\n            } catch let error as KingfisherError {\n                DispatchQueue.main.async { handler(.failure(error)) }\n            } catch {\n                assertionFailure(\"The internal thrown error should be a `KingfisherError`.\")\n            }\n        }\n    }\n    \n    /// Retrieves the cache path for the key.\n    ///\n    /// It is useful for projects with a web view or for anyone who needs access to the local file path.\n    /// For instance, replacing the `<img src='path_for_key'>` tag in your HTML.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The processor identifier used for this image. The default value is the\n    ///    ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor.\n    ///   - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the\n    ///   disk storage configuration instead.\n    /// \n    /// - Returns: The disk path of the cached image under the given `key` and `identifier`.\n    ///\n    /// > This method does not guarantee that there is an image already cached in the returned path. It simply provides\n    /// > the path where the image should be if it exists in the disk storage.\n    /// >\n    /// > You could use the ``ImageCache/isCached(forKey:processorIdentifier:forcedExtension:)`` method to check whether the image is\n    /// cached under that key on disk if necessary.\n    open func cachePath(\n        forKey key: String,\n        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,\n        forcedExtension: String? = nil\n    ) -> String\n    {\n        let computedKey = key.computedKey(with: identifier)\n        return diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension).path\n    }\n    \n    /// Returns the file URL if a disk cache file is existing for the target key, identifier and forcedExtension\n    /// combination. Otherwise, if the requested cache value is not on the disk as a file, `nil`.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the item.\n    ///   - identifier: The processor identifier used for this image. It involves into calculating the final cache key.\n    ///   - forcedExtension: The expected extension of the file.\n    /// - Returns: The file URL if a disk cache file is existing for the combination. Otherwise, `nil`.\n    open func cacheFileURLIfOnDisk(\n        forKey key: String,\n        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,\n        forcedExtension: String? = nil\n    ) -> URL?\n    {\n        let computedKey = key.computedKey(with: identifier)\n        return diskStorage.isCached(\n            forKey: computedKey,\n            forcedExtension: forcedExtension\n        ) ? diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension) : nil\n    }\n    \n    // MARK: - Concurrency\n    \n    /// Stores an image to the cache.\n    ///\n    /// - Parameters:\n    ///   - image: The image that to be stored.\n    ///   - original: The original data of the image. This value will be forwarded to the provided `serializer` for\n    ///   further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for\n    ///   caching in disk. It checks the image format based on the `original` data to determine the appropriate image\n    ///   format to use. For other types of `serializer`, it depends on their implementation details on how to use this\n    ///   original data.\n    ///   - key: The key used for caching the image.\n    ///   - options: The options which contains configurations for caching the image.\n    ///   - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory.\n    ///   Otherwise, it is cached in both memory storage and disk storage. The default is `true`.\n    open func store(\n        _ image: KFCrossPlatformImage,\n        original: Data? = nil,\n        forKey key: String,\n        options: KingfisherParsedOptionsInfo,\n        toDisk: Bool = true\n    ) async throws {\n        try await withCheckedThrowingContinuation { continuation in\n            store(image, original: original, forKey: key, options: options, toDisk: toDisk) {\n                continuation.resume(with: $0.diskCacheResult)\n            }\n        }\n    }\n    \n    /// Stores an image in the cache.\n    ///\n    /// - Parameters:\n    ///   - image: The image to be stored.\n    ///   - original: The original data of the image. This value will be forwarded to the provided `serializer` for\n    ///   further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for\n    ///   caching in disk. It checks the image format based on the `original` data to determine the appropriate image\n    ///   format to use. For other types of `serializer`, it depends on their implementation details on how to use this\n    ///   original data.\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The identifier of the processor being used for caching. If you are using a processor for the\n    ///   image, pass the identifier of the processor to this parameter.\n    ///   - forcedExtension: The file extension, if exists.\n    ///   - serializer: The ``CacheSerializer`` used to convert the `image` and `original` to the data that will be\n    ///   stored to disk. By default, the ``DefaultCacheSerializer/default`` will be used.\n    ///   - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory.\n    ///   Otherwise, it is cached in both memory storage and disk storage. The default is `true`.\n    open func store(\n        _ image: KFCrossPlatformImage,\n        original: Data? = nil,\n        forKey key: String,\n        processorIdentifier identifier: String = \"\",\n        forcedExtension: String? = nil,\n        cacheSerializer serializer: any CacheSerializer = DefaultCacheSerializer.default,\n        toDisk: Bool = true\n    ) async throws {\n        try await withCheckedThrowingContinuation { continuation in\n            store(\n                image,\n                original: original,\n                forKey: key,\n                processorIdentifier: identifier,\n                forcedExtension: forcedExtension,\n                cacheSerializer: serializer,\n                toDisk: toDisk) {\n                    // Only `diskCacheResult` can fail\n                    continuation.resume(with: $0.diskCacheResult)\n                }\n        }\n    }\n    \n    open func storeToDisk(\n        _ data: Data,\n        forKey key: String,\n        processorIdentifier identifier: String = \"\",\n        forcedExtension: String? = nil,\n        expiration: StorageExpiration? = nil\n    ) async throws\n    {\n        try await withCheckedThrowingContinuation { continuation in\n            storeToDisk(\n                data,\n                forKey: key,\n                processorIdentifier: identifier,\n                forcedExtension: forcedExtension,\n                expiration: expiration) {\n                    // Only `diskCacheResult` can fail\n                    continuation.resume(with: $0.diskCacheResult)\n                }\n        }\n    }\n    \n    /// Removes the image for the given key from the cache.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - identifier: The identifier of the processor being used for caching. If you are using a processor for the\n    ///   image, pass the identifier of the processor to this parameter.\n    ///   - forcedExtension: The file extension, if exists.\n    ///   - fromMemory: Whether this image should be removed from memory storage or not. If `false`, the image won't be\n    ///   removed from the memory storage. The default is `true`.\n    ///   - fromDisk: Whether this image should be removed from the disk storage or not. If `false`, the image won't be\n    ///    removed from the disk storage. The default is `true`.\n    open func removeImage(\n        forKey key: String,\n        processorIdentifier identifier: String = \"\",\n        forcedExtension: String? = nil,\n        fromMemory: Bool = true,\n        fromDisk: Bool = true\n    ) async throws {\n        return try await withCheckedThrowingContinuation { continuation in\n            removeImage(\n                forKey: key,\n                processorIdentifier: identifier,\n                forcedExtension: forcedExtension,\n                fromMemory: fromMemory,\n                fromDisk: fromDisk,\n                completionHandler: { error in\n                    if let error {\n                        continuation.resume(throwing: error)\n                    } else {\n                        continuation.resume()\n                    }\n                }\n            )\n        }\n    }\n    \n    /// Retrieves an image for a given key from the cache, either from memory storage or disk storage.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - options: The ``KingfisherParsedOptionsInfo`` options setting used for retrieving the image.\n    /// - Returns:\n    /// If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value.\n    ///\n    /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework.\n    open func retrieveImage(\n        forKey key: String,\n        options: KingfisherParsedOptionsInfo\n    ) async throws -> ImageCacheResult {\n        try await withCheckedThrowingContinuation { continuation in\n            retrieveImage(forKey: key, options: options) { continuation.resume(with: $0) }\n        }\n    }\n    \n    /// Retrieves an image for a given key from the cache, either from memory storage or disk storage.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - options: The ``KingfisherOptionsInfo`` options setting used for retrieving the image.\n    ///\n    /// - Returns: If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value.\n    ///\n    /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework.\n    ///\n    /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead,\n    /// override the version ``ImageCache/retrieveImage(forKey:options:callbackQueue:completionHandler:)-1jjo3`` that\n    /// accepts a ``KingfisherParsedOptionsInfo`` value.\n    open func retrieveImage(\n        forKey key: String,\n        options: KingfisherOptionsInfo? = nil\n    ) async throws -> ImageCacheResult {\n        try await withCheckedThrowingContinuation { continuation in\n            retrieveImage(forKey: key, options: options) { continuation.resume(with: $0) }\n        }\n    }\n    \n    /// Retrieves an image associated with a given key from the disk storage.\n    ///\n    /// - Parameters:\n    ///   - key: The key used for caching the image.\n    ///   - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image.\n    ///\n    /// - Returns: The image stored in the disk cache if it exists and is valid. If the image does not exist or has\n    ///  already expired, `nil` is returned.\n    ///\n    /// - Returns: If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value.\n    ///\n    /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework.\n    ///  ``KingfisherParsedOptionsInfo`` value.\n    open func retrieveImageInDiskCache(\n        forKey key: String,\n        options: KingfisherOptionsInfo? = nil\n    ) async throws -> KFCrossPlatformImage? {\n        try await withCheckedThrowingContinuation { continuation in\n            retrieveImageInDiskCache(forKey: key, options: options) {\n                continuation.resume(with: $0)\n            }\n        }\n    }\n    \n    /// Clears the memory and disk storage of this cache.\n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns.\n    open func clearCache() async {\n        await withCheckedContinuation { continuation in\n            clearCache { continuation.resume() }\n        }\n    }\n    \n    /// Clears the disk storage of this cache.\n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns.\n    open func clearDiskCache() async {\n        await withCheckedContinuation { continuation in\n            clearDiskCache { continuation.resume() }\n        }\n    }\n    \n    /// Clears the expired images from the memory and disk storage.\n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns.\n    open func cleanExpiredCache() async {\n        await withCheckedContinuation { continuation in\n            cleanExpiredCache { continuation.resume() }\n        }\n    }\n    \n    /// Clears the expired images from disk storage.\n    ///\n    /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns.\n    open func cleanExpiredDiskCache() async {\n        await withCheckedContinuation { continuation in\n            cleanExpiredDiskCache { continuation.resume() }\n        }\n    }\n    \n    /// Calculates the size taken by the disk storage.\n    ///\n    /// It represents the total file size of all cached files in the ``ImageCache/diskStorage`` on disk in bytes.\n    open var diskStorageSize: UInt {\n        get async throws {\n            try await withCheckedThrowingContinuation { continuation in\n                calculateDiskStorageSize { continuation.resume(with: $0) }\n            }\n        }\n    }\n    \n}\n\n// Concurrency\n\n\n#if !os(macOS) && !os(watchOS)\n// MARK: - For App Extensions\nextension UIApplication: KingfisherCompatible { }\nextension KingfisherWrapper where Base: UIApplication {\n    public static var shared: UIApplication? {\n        let selector = NSSelectorFromString(\"sharedApplication\")\n        guard Base.responds(to: selector) else { return nil }\n        guard let unmanaged = Base.perform(selector) else { return nil }\n        return unmanaged.takeUnretainedValue() as? UIApplication\n    }\n}\n#endif\n\nextension String {\n    func computedKey(with identifier: String) -> String {\n        if identifier.isEmpty {\n            return self\n        } else {\n            return appending(\"@\\(identifier)\")\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Cache/MemoryStorage.swift",
    "content": "//\n//  MemoryStorage.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/10/15.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Represents the concepts related to storage that stores a specific type of value in memory.\n///\n/// This serves as a namespace for memory storage types. A ``MemoryStorage/Backend`` with a particular\n/// ``MemoryStorage/Config`` is used to define the storage.\n/// \n/// Refer to these composite types for further details.\npublic enum MemoryStorage {\n\n    /// Represents a storage that stores a specific type of value in memory.\n    ///\n    /// It provides fast access but has a limited storage size. The stored value type needs to conform to the\n    /// ``CacheCostCalculable`` protocol, and its ``CacheCostCalculable/cacheCost`` will be used to determine the cost\n    /// of the cache item's size in the memory.\n    ///\n    /// You can configure a ``MemoryStorage/Backend`` in its ``MemoryStorage/Backend/init(config:)`` method by passing\n    /// a ``MemoryStorage/Config`` value or by modifying the ``MemoryStorage/Backend/config`` property after it's\n    /// created.\n    ///\n    /// The ``MemoryStorage`` backend has an upper limit on the total cost size in memory and item count. All items in\n    /// the storage have an expiration date. When retrieved, if the target item is already expired, it will be\n    /// recognized as if it does not exist in the storage.\n    ///\n    /// The `MemoryStorage` also includes a scheduled self-cleaning task to evict expired items from memory.\n    ///\n    /// > This class is thready safe.\n    public final class Backend<T: CacheCostCalculable>: @unchecked Sendable where T: Sendable {\n        \n        let storage = NSCache<NSString, StorageObject<T>>()\n\n        // Keys track the objects once inside the storage.\n        //\n        // For object removing triggered by user, the corresponding key would be also removed. However, for the object\n        // removing triggered by cache rule/policy of system, the key will be remained there until next `removeExpired`\n        // happens.\n        //\n        // Breaking the strict tracking could save additional locking behaviors and improve the cache performance.\n        // See https://github.com/onevcat/Kingfisher/issues/1233\n        var keys = Set<String>()\n\n        private var cleanTimer: Timer? = nil\n        private let lock = NSLock()\n\n        /// The configuration used in this storage.\n        ///\n        /// It is a value you can set and use to configure the storage as needed.\n        public var config: Config {\n            didSet {\n                storage.totalCostLimit = config.totalCostLimit\n                storage.countLimit = config.countLimit\n                cleanTimer?.invalidate()\n                cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in\n                    guard let self = self else { return }\n                    self.removeExpired()\n                }\n            }\n        }\n\n        /// Creates a ``MemoryStorage/Backend`` with a given ``MemoryStorage/Config`` value.\n        ///\n        /// - Parameter config: The configuration used to create the storage. It determines the maximum size limitation,\n        /// default expiration settings, and more.\n        public init(config: Config) {\n            self.config = config\n            storage.totalCostLimit = config.totalCostLimit\n            storage.countLimit = config.countLimit\n\n            cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in\n                guard let self = self else { return }\n                self.removeExpired()\n            }\n        }\n\n        /// Removes the expired values from the storage.\n        public func removeExpired() {\n            lock.lock()\n            defer { lock.unlock() }\n            for key in keys {\n                let nsKey = key as NSString\n                guard let object = storage.object(forKey: nsKey) else {\n                    // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule.\n                    // We didn't remove the key yet until now, since we do not want to introduce additional lock.\n                    // See https://github.com/onevcat/Kingfisher/issues/1233\n                    keys.remove(key)\n                    continue\n                }\n                if object.isExpired {\n                    storage.removeObject(forKey: nsKey)\n                    keys.remove(key)\n                }\n            }\n        }\n        \n        /// Stores a value in the storage under the specified key and expiration policy.\n        ///\n        /// - Parameters:\n        ///   - value: The value to be stored.\n        ///   - key: The key to which the `value` will be stored.\n        ///   - expiration: The expiration policy used by this storage action.\n        public func store(\n            value: T,\n            forKey key: String,\n            expiration: StorageExpiration? = nil)\n        {\n            storeNoThrow(value: value, forKey: key, expiration: expiration)\n        }\n\n        // The no throw version for storing value in cache. Kingfisher knows the detail so it\n        // could use this version to make syntax simpler internally.\n        func storeNoThrow(\n            value: T,\n            forKey key: String,\n            expiration: StorageExpiration? = nil)\n        {\n            lock.lock()\n            defer { lock.unlock() }\n            let expiration = expiration ?? config.expiration\n            // The expiration indicates that already expired, no need to store.\n            guard !expiration.isExpired else { return }\n            \n            let object: StorageObject<T>\n            if config.keepWhenEnteringBackground {\n                object = BackgroundKeepingStorageObject(value, expiration: expiration)\n            } else {\n                object = StorageObject(value, expiration: expiration)\n            }\n            storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)\n            keys.insert(key)\n        }\n        \n        /// Gets a value from the storage.\n        ///\n        /// - Parameters:\n        ///   - key: The cache key of the value.\n        ///   - extendingExpiration: The expiration policy used by this retrieval action.\n        /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`.\n        public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {\n            guard let object = storage.object(forKey: key as NSString) else {\n                return nil\n            }\n            if object.isExpired {\n                return nil\n            }\n            object.extendExpiration(extendingExpiration)\n            return object.value\n        }\n\n        /// Determines whether there is valid cached data under a given key.\n        ///\n        /// - Parameter key: The cache key of the value.\n        /// - Returns: `true` if there is valid data under the key, otherwise `false`.\n        public func isCached(forKey key: String) -> Bool {\n            guard let _ = value(forKey: key, extendingExpiration: .none) else {\n                return false\n            }\n            return true\n        }\n\n        /// Removes a value from a specified key.\n        ///\n        /// - Parameter key: The cache key of the value.\n        public func remove(forKey key: String) {\n            lock.lock()\n            defer { lock.unlock() }\n            storage.removeObject(forKey: key as NSString)\n            keys.remove(key)\n        }\n\n        /// Removes all values in this storage.\n        public func removeAll() {\n            lock.lock()\n            defer { lock.unlock() }\n            storage.removeAllObjects()\n            keys.removeAll()\n        }\n    }\n}\n\nextension MemoryStorage {\n    /// Represents the configuration used in a ``MemoryStorage/Backend``.\n    public struct Config {\n\n        /// The total cost limit of the storage.\n        ///\n        /// This counts up the value of ``CacheCostCalculable/cacheCost``. If adding this object to the cache causes\n        /// the cache’s total cost to rise above totalCostLimit, the cache may automatically evict objects until its\n        /// total cost falls below this value.\n        public var totalCostLimit: Int\n\n        /// The item count limit of the memory storage.\n        ///\n        /// The default value is `Int.max`, which means no hard limitation of the item count.\n        public var countLimit: Int = .max\n\n        /// The ``StorageExpiration`` used in this memory storage.\n        ///\n        /// The default is `.seconds(300)`, which means that the memory cache will expire in 5 minutes if not accessed.\n        public var expiration: StorageExpiration = .seconds(300)\n\n        /// The time interval between the storage performing cleaning work for sweeping expired items.\n        public var cleanInterval: TimeInterval\n        \n        /// Determine whether newly added items to memory cache should be purged when the app goes to the background.\n        ///\n        /// By default, cached items in memory will be purged as soon as the app goes to the background to ensure a\n        /// minimal memory footprint. Enabling this prevents this behavior and keeps the items alive in the cache even\n        /// when your app is not in the foreground.\n        ///\n        /// The default value is `false`. After setting it to `true`, only newly added cache objects are affected. \n        /// Existing objects that were already in the cache while this value was `false` will still be purged when the\n        /// app enters the background.\n        public var keepWhenEnteringBackground: Bool = false\n\n        /// Creates a configuration from a given ``MemoryStorage/Config/totalCostLimit`` value and a\n        ///  ``MemoryStorage/Config/cleanInterval``.\n        ///\n        /// - Parameters:\n        ///   - totalCostLimit: The total cost limit of the storage in bytes.\n        ///   - cleanInterval: The time interval between the storage performing cleaning work for sweeping expired items.\n        ///   The default is 120, which means auto eviction happens once every two minutes.\n        ///\n        /// > Other properties of the ``MemoryStorage/Config`` will use their default values when created.\n        public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) {\n            self.totalCostLimit = totalCostLimit\n            self.cleanInterval = cleanInterval\n        }\n    }\n}\n\nextension MemoryStorage {\n    \n    class BackgroundKeepingStorageObject<T>: StorageObject<T>, NSDiscardableContent {\n        var accessing = true\n        func beginContentAccess() -> Bool {\n            if value != nil {\n                accessing = true\n            } else {\n                accessing = false\n            }\n            return accessing\n        }\n        \n        func endContentAccess() {\n            accessing = false\n        }\n        \n        func discardContentIfPossible() {\n            value = nil\n        }\n        \n        func isContentDiscarded() -> Bool {\n            return value == nil\n        }\n    }\n    \n    class StorageObject<T> {\n        var value: T?\n        let expiration: StorageExpiration\n        \n        private(set) var estimatedExpiration: Date\n        \n        init(_ value: T, expiration: StorageExpiration) {\n            self.value = value\n            self.expiration = expiration\n            \n            self.estimatedExpiration = expiration.estimatedExpirationSinceNow\n        }\n\n        func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) {\n            switch extendingExpiration {\n            case .none:\n                return\n            case .cacheTime:\n                self.estimatedExpiration = expiration.estimatedExpirationSinceNow\n            case .expirationTime(let expirationTime):\n                self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow\n            }\n        }\n        \n        var isExpired: Bool {\n            return estimatedExpiration.isPast\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Cache/Storage.swift",
    "content": "//\n//  Storage.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/10/15.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Constants for certain time intervals.\nstruct TimeConstants {\n    // Seconds in a day, a.k.a 86,400s, roughly.\n    static let secondsInOneDay = 86_400\n}\n\n/// Represents the expiration strategy utilized in storage.\npublic enum StorageExpiration: Sendable {\n    \n    /// The item never expires.\n    case never\n    \n    /// The item expires after a duration of the provided number of seconds from now.\n    case seconds(TimeInterval)\n    \n    /// The item expires after a duration of the provided number of days from now.\n    case days(Int)\n    \n    /// The item expires after a specified date.\n    case date(Date)\n    \n    /// Indicates that the item has already expired.\n    ///\n    /// Use this to bypass the cache.\n    case expired\n\n    \n    func estimatedExpirationSince(_ date: Date) -> Date {\n        switch self {\n        case .never: \n            return .distantFuture\n        case .seconds(let seconds):\n            return date.addingTimeInterval(seconds)\n        case .days(let days):\n            let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay * days)\n            return date.addingTimeInterval(duration)\n        case .date(let ref):\n            return ref\n        case .expired:\n            return .distantPast\n        }\n    }\n    \n    var estimatedExpirationSinceNow: Date {\n        estimatedExpirationSince(Date())\n    }\n    \n    var isExpired: Bool {\n        timeInterval <= 0\n    }\n\n    var timeInterval: TimeInterval {\n        switch self {\n        case .never: return .infinity\n        case .seconds(let seconds): return seconds\n        case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay * days)\n        case .date(let ref): return ref.timeIntervalSinceNow\n        case .expired: return -(.infinity)\n        }\n    }\n}\n\n/// Represents the expiration extension strategy used in storage after access.\npublic enum ExpirationExtending: Sendable {\n    /// The item expires after the original time, without extension after access.\n    case none\n    /// The item expiration extends to the original cache time after each access.\n    case cacheTime\n    /// The item expiration extends by the provided time after each access.\n    case expirationTime(_ expiration: StorageExpiration)\n}\n\n/// Represents types for which the memory cost can be calculated.\npublic protocol CacheCostCalculable {\n    var cacheCost: Int { get }\n}\n\n/// Represents types that can be converted to and from data.\npublic protocol DataTransformable {\n    \n    /// Converts the current value to a `Data` representation.\n    /// - Returns: The data object which can represent the value of the conforming type.\n    /// - Throws: If any error happens during the conversion.\n    func toData() throws -> Data\n    \n    /// Convert some data to the value.\n    /// - Parameter data: The data object which should represent the conforming value.\n    /// - Returns: The converted value of the conforming type.\n    /// - Throws: If any error happens during the conversion.\n    static func fromData(_ data: Data) throws -> Self\n    \n    /// An empty object of `Self`.\n    ///\n    /// > In the cache, when the data is not actually loaded, this value will be returned as a placeholder.\n    /// > This variable should be returned quickly without any heavy operation inside.\n    static var empty: Self { get }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/CommonTasks/CommonTasks.md",
    "content": "# Common Tasks\n\nBelow is a code snippet designed to address the most commonly encountered tasks. You are encouraged to freely integrate \nthis snippet into your upcoming projects.\n\n@Metadata {\n    @PageImage(purpose: card, source: \"common-tasks-card\"))\n    @PageColor(blue)\n}\n\n## Overview\n\nThis document provides a comprehensive guide to the most prevalent use cases. The included code snippet is tailored for \niOS development. Nevertheless, it can be adapted for other platforms, such as macOS or tvOS, with minimal modifications.\nThis typically involves substituting specific classes (for instance, replacing `UIImage` with `NSImage`). \n\nTo explore detailed instructions for specific components within the Kingfisher framework, please refer to the \nsubsequent documentation:\n\n#### Common Tasks for Main Components\n\n@Links(visualStyle: list) {\n    - <doc:CommonTasks_Cache>\n    - <doc:CommonTasks_Downloader>\n    - <doc:CommonTasks_Processor>\n}\n\n#### Other Topics\n\n@Links(visualStyle: list) {\n    - <doc:Topic_Prefetch>\n    - <doc:Topic_ImageDataProvider>\n    - <doc:Topic_Indicator>\n    - <doc:Topic_Retry>\n    - <doc:Topic_LowDataMode> \n    - <doc:Topic_PerformanceTips>\n}\n\n## Most Common Tasks\n\nThe view extension-based APIs for `UIImageView`, `NSImageView`, `UIButton`, and `NSButton` are recommended as your \nprimary choice. They simplify and enhance the elegance of your code.\n\n### Setting Image with a `URL`\n\n```swift\nlet url = URL(string: \"https://example.com/image.jpg\")\nimageView.kf.setImage(with: url)\n```\n\nThis code performs the following actions:\n\n1. Verifies if an image is cached using the key `url.absoluteString`.\n2. Retrieves and assigns the image to `imageView.image` if found in cache (memory or disk).\n3. If absent, initiates a request and downloads from `url`.\n4. Transforms the downloaded data into a `UIImage`.\n5. Stores the image in both memory and disk caches.\n6. Updates `imageView.image` with the new image.\n\nSubsequent calls to `setImage` with the same URL will only execute steps 1 and 2, unless the cache has been cleared.\n\n### Showing a Placeholder\n\n```swift\nlet image = UIImage(named: \"default_profile_icon\")\nimageView.kf.setImage(with: url, placeholder: image)\n```\n\nThe `imageView` will display the `image` as the placeholder during its download from the `url`.\n\n> You can also employ a custom `UIView` or `NSView` as a placeholder by making it conform to the `Placeholder` protocol:\n> \n> ```swift\n> class MyView: UIView { /* Implementation of your view */ }\n> \n> extension MyView: Placeholder { /* This can be left empty */ }\n> \n> imageView.kf.setImage(with: url, placeholder: MyView())\n> ```\n> \n> The instance of `MyView` will be dynamically added to or removed from the `imageView` as required.\n\n### Showing a Loading Indicator while Downloading\n\n```swift\nimageView.kf.indicatorType = .activity\nimageView.kf.setImage(with: url)\n```\n\nThis shows a `UIActivityIndicatorView` in center of image view while downloading.\n\n### Fading in Downloaded Image\n\nFor UIKit/AppKit:\n```swift\nimageView.kf.setImage(with: url, options: [.transition(.fade(0.2))])\n```\n\nFor SwiftUI (recommended):\n```swift\nKFImage(url)\n    .fade(duration: 0.2)\n    // Or use native SwiftUI transitions:\n    .loadTransition(.opacity, animation: .easeInOut(duration: 0.2))\n```\n\n> Note: In SwiftUI applications, use `loadTransition` for native SwiftUI transitions instead of the options-based `transition`. This provides better integration with the SwiftUI animation system.\n\n### Completion Handler\n\n```swift\nimageView.kf.setImage(with: url) { result in\n    // `result` is either a `.success(RetrieveImageResult)` or a `.failure(KingfisherError)`\n    switch result {\n    case .success(let value):\n        // The image was set to image view:\n        print(value.image)\n\n        // From where the image was retrieved:\n        // - .none - Just downloaded.\n        // - .memory - Got from memory cache.\n        // - .disk - Got from disk cache.\n        print(value.cacheType)\n\n        // The source object which contains information like `url`.\n        print(value.source)\n\n    case .failure(let error):\n        print(error) // The error happens\n    }\n}\n```\n\n### Getting an Image without Setting to UI\n\nOccasionally, you might need to retrieve an image using Kingfisher without assigning it to an image view. In such\ncases, use ``KingfisherManager/retrieveImage(with:options:progressBlock:)-80fw1``\n\n\n```swift\nKingfisherManager.shared.retrieveImage(with: url) { result in \n    // Do something with `result`\n}\n```\n"
  },
  {
    "path": "Sources/Documentation.docc/CommonTasks/CommonTasks_Cache.md",
    "content": "# Common Tasks - Cache\n\nCommon tasks related to the ``ImageCache`` in Kingfisher.\n\n## Overview\n\nKingfisher employs a hybrid ``ImageCache`` for managing cached images, comprising both memory and disk storage. It \noffers high-level APIs for cache management. Unless otherwise specified, the ``ImageCache/default`` instance is \nused throughout Kingfisher.\n\n### Using another cache key\n\nBy default, the URL is converted into a string to generate the cache key. For network URLs, `absoluteString` is \nutilized. You can customize the key by creating an ``ImageResource`` object with a specified key.\n\n```swift\nlet resource = ImageResource(\n    downloadURL: url, \n    cacheKey: \"my_cache_key\"\n)\nimageView.kf.setImage(with: resource)\n```\n\nKingfisher uses the `cacheKey` to locate images in the cache. Ensure you use a distinct key for each different image.\n\n#### Checking whether an image in the cache\n\n```swift\nlet cache = ImageCache.default\nlet cached = cache.isCached(forKey: cacheKey)\n\n// To know where the cached image is:\nlet cacheType = cache.imageCachedType(forKey: cacheKey)\n// `.memory`, `.disk` or `.none`.\n```\n\n> Note: ``ImageCache/imageCachedType(forKey:processorIdentifier:forcedExtension:)`` may touch the disk cache\n> synchronously when checking `.disk`. If you want to avoid blocking the calling thread (for example, in a scrolling\n> UI on the main thread), use the opt-in async API instead:\n>\n> ```swift\n> cache.imageCachedTypeAsync(forKey: cacheKey) { cacheType in\n>     // `.memory`, `.disk` or `.none`.\n> }\n>\n> // Or with async/await:\n> let cacheType = await cache.imageCachedTypeAsync(forKey: cacheKey)\n> ```\n\nIf a processor is applied when retrieving an image, the processed image will be cached. In this scenario, remember to \nalso include the processor identifier when manipulating the cache:\n\n```swift\nlet processor = RoundCornerImageProcessor(cornerRadius: 20)\nimageView.kf.setImage(with: url, options: [.processor(processor)])\n\n// Later\ncache.isCached(forKey: cacheKey, processorIdentifier: processor.identifier)\n```\n\n#### Getting an image from the cache\n\n```swift\ncache.retrieveImage(forKey: \"cacheKey\") { result in\n    switch result {\n    case .success(let value):\n        print(value.cacheType)\n\n        // If the `cacheType is `.none`, `image` will be `nil`.\n        print(value.image)\n\n    case .failure(let error):\n        print(error)\n    }\n}\n```\n\n#### Set limit for the cache\n\nFor memory storage, you can set its ``MemoryStorage/Config/totalCostLimit`` and ``MemoryStorage/Config/countLimit``:\n\n```swift\n// Limit memory cache size to 300 MB.\ncache.memoryStorage.config.totalCostLimit = 300 * 1024 * 1024\n\n// Limit memory cache to hold 150 images at most. \ncache.memoryStorage.config.countLimit = 150\n```\n\nThe default ``MemoryStorage/Config/totalCostLimit`` for the memory cache is set to 25% of the device's total memory, \nwith no limit on the ``MemoryStorage/Config/countLimit``.\n\nFor disk storage, you have the option to set a ``DiskStorage/Config/sizeLimit`` to manage the space used on the file \nsystem.\n\n```swift\n// Limit disk cache size to 1 GB.\ncache.diskStorage.config.sizeLimit = 1000 * 1024 * 1024\n```\n\n#### Set the default expiration for cache\n\nBoth memory and disk storage in Kingfisher have default expiration settings. Images in memory storage expire 5 minutes \nafter the last access, whereas images in disk storage expire after one week. These values can be modified as follows:\n\n```swift\n// Set memory image expires after 10 minutes.\ncache.memoryStorage.config.expiration = .seconds(600)\n\n// Set disk image never expires.\ncache.diskStorage.config.expiration = .never\n```\n\nTo override this default expiration for a specific image when caching it, include an option as follows during image \nsetting:\n\n```swift\n// This image will never expire in memory cache.\nimageView.kf.setImage(with: url, options: [.memoryCacheExpiration(.never)])\n```\n\nThe expired memory cache is purged every 2 minutes by default. To adjust this frequency:\n\n```swift\n// Check memory clean up every 30 seconds.\ncache.memoryStorage.config.cleanInterval = 30\n```\n\n#### Store images to cache manually\n\nBy default, view extension methods and ``KingfisherManager`` automatically store retrieved images in the cache. \nHowever, you can also manually store an image to the cache:\n\n```swift\nlet image: UIImage = //...\ncache.store(image, forKey: cacheKey)\n```\n\nIf you possess the original data of the image, pass it along to ``ImageCache``. This assists Kingfisher in determining \nthe appropriate format for storing the image:\n\n```swift\nlet data: Data = //...\nlet image: UIImage = //...\ncache.store(image, original: data, forKey: cacheKey)\n```\n\n#### Remove images from cache manually\n\nKingfisher manages its cache automatically. But you still can manually remove a certain image from cache:\n\n```swift\ncache.removeImage(forKey: cacheKey)\n```\n\nOr, with more control:\n\n```swift\ncache.removeImage(\n    forKey: cacheKey,\n    processorIdentifier: processor.identifier,\n    fromMemory: false,\n    fromDisk: true)\n{\n    print(\"Removed!\")\n}\n```\n\n#### Clear the cache\n\n```swift\n// Remove all.\ncache.clearMemoryCache()\ncache.clearDiskCache { print(\"Done\") }\n\n// Remove only expired.\ncache.cleanExpiredMemoryCache()\ncache.cleanExpiredDiskCache { print(\"Done\") }\n```\n\n#### Report the disk storage size\n\n```swift\nImageCache.default.calculateDiskStorageSize { result in\n    switch result {\n    case .success(let size):\n        print(\"Disk cache size: \\(Double(size) / 1024 / 1024) MB\")\n    case .failure(let error):\n        print(error)\n    }\n}\n```\n\n#### Create your own cache and use it\n\n```swift\n// The `name` parameter is used to identify the disk cache bound to the `ImageCache`.\nlet cache = ImageCache(name: \"my-own-cache\")\nimageView.kf.setImage(with: url, options: [.targetCache(cache)])\n```\n\n#### Skipping cache searching, force downloading image again \n\n```swift\nimageView.kf.setImage(with: url, options: [.forceRefresh])\n```\n\n#### Only search cache for the image, do not download if not existing\n\nThis makes your app to an \"offline\" mode.\n\n```swift\nimageView.kf.setImage(with: url, options: [.onlyFromCache])\n```\n\nIf the image does not exist in the cache, an ``KingfisherError/CacheErrorReason/imageNotExisting(key:)`` error will be\ntriggered.\n\n#### Waiting for cache to finish\n\nStoring images in the disk cache is asynchronous and doesn't need to be completed before setting the image view and \ninvoking the completion handler in view extension methods. This means that the disk cache might not be fully updated at\nthe time the completion handler is executed, as shown below:\n\n```swift\nimageView.kf.setImage(with: url) { _ in\n    ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) { result in\n        switch result {\n        case .success(let image):\n            // `image` might be `nil` here.\n        case .failure: break\n        }\n    }\n}\n```\n\nFor most scenarios, this asynchronous behavior isn't an issue. However, if your logic relies on the existence of the \ndisk cache, use the `.waitForCache` option. With this option, Kingfisher will delay the execution of the handler until \nthe disk cache operation is complete:\n\n```swift\nimageView.kf.setImage(with: url, options: [.waitForCache]) { _ in\n    ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) { result in\n        switch result {\n        case .success(let image):\n            // `image` exists.\n        case .failure: break\n        }\n    }\n}\n```\n\nThis consideration applies specifically to disk image caching, which involves asynchronous I/O operations. In contrast,\nmemory cache operations are synchronous, ensuring that the image is always available in the memory cache.\n"
  },
  {
    "path": "Sources/Documentation.docc/CommonTasks/CommonTasks_Downloader.md",
    "content": "# Common Tasks - Downloader\n\nCommon tasks related to the ``ImageDownloader`` in Kingfisher.\n\n## Overview\n\n``ImageDownloader`` wraps a `URLSession` for downloading an image from the Internet. Similar to ``ImageCache``, there\nis a ``ImageDownloader/default`` downloader for downloading tasks.\n\n### Download an image manually\n\nTypically, you might use Kingfisher's view extension methods or ``KingfisherManager`` for image retrieval. These methods \nprioritize searching the cache to avoid unnecessary downloads. If you need to download an image without caching it, \nconsider the following approach:\n\n```swift\nlet downloader = ImageDownloader.default\ndownloader.downloadImage(with: url) { result in\n    switch result {\n    case .success(let value):\n        print(value.image)\n    case .failure(let error):\n        print(error)\n    }\n}\n```\n\n### Modify a request before sending\n\nWhen managing access to your image resources with permission controls, you can customize the request using a\n``KingfisherOptionsInfoItem/requestModifier(_:)``:\n\n```swift\nlet modifier = AnyModifier { request in\n    var r = request\n    r.setValue(\"abc\", forHTTPHeaderField: \"Access-Token\")\n    return r\n}\ndownloader.downloadImage(with: url, options: [.requestModifier(modifier)]) { \n    result in\n    // ...\n}\n\n// This option also works for view extension methods.\nimageView.kf.setImage(with: url, options: [.requestModifier(modifier)])\n```\n\n### Use async request modifier\n\nIf an asynchronous operation is required before modifying the request, create a type that conforms to \n``AsyncImageDownloadRequestModifier``:\n\n```swift\nclass AsyncModifier: AsyncImageDownloadRequestModifier {\n    var onDownloadTaskStarted: ((DownloadTask?) -> Void)?\n\n    func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) {\n        var r = request\n        someAsyncOperation { result in\n            r.someProperty = result.property\n            reportModified(r)\n        }\n    }\n}\n```\n\nSimilarly, use the ``KingfisherOptionsInfoItem/requestModifier(_:)`` to apply this modifier. In such scenarios, the\n``KingfisherWrapper/setImage(with:placeholder:options:progressBlock:completionHandler:)-8lmr3`` or\n``ImageDownloader/downloadImage(with:options:completionHandler:)-2ztyq`` method will no longer return a ``DownloadTask``\ndirectly, as the download task isn't initiated instantly. To reference the task, monitor the\n``AsyncImageDownloadRequestModifier/onDownloadTaskStarted`` callback.\n\n```swift\nlet modifier = AsyncModifier()\nmodifier.onDownloadTaskStarted = { task in\n    if let task = task {\n        print(\"A download task started: \\(task)\")\n    }\n}\nlet nilTask = imageView.kf.setImage(with: url, options: [.requestModifier(modifier)])\n```\n\n### Cancel a download task\n\nOnce the download has started, a ``DownloadTask`` will be created and returned. This can be used to cancel an ongoing \ndownload task.\n\n```swift\nlet task = downloader.downloadImage(with: url) { result in\n    // ...\n    case .failure(let error):\n        print(error.isTaskCancelled) // true\n    }\n\n}\n\n// After some time, but before the download task completes.\ntask?.cancel()\n```\n\nIf you call ``DownloadTask/cancel()`` after the task has already finished, no action will be taken.\n\nLikewise, the view extension methods return a ``DownloadTask`` as well. This allows you to store the task and cancel it \nif needed:\n\n```swift\nlet task = imageView.kf.set(with: url)\ntask?.cancel()\n```\n\nAlternatively, you can invoke ``KingfisherWrapper/cancelDownloadTask()-2gg15`` on the image view to cancel the \n**current downloading task**.\n\n```swift\nlet task1 = imageView.kf.set(with: url1)\nlet task2 = imageView.kf.set(with: url2)\n\nimageView.kf.cancelDownloadTask()\n// `task2` will be cancelled, but `task1` is still running. \n// However, the downloaded image for `task1` will not be set because the image view expects a result from `url2`.\n```\n\n### Authentication with `NSURLCredential`\n\nThe ``ImageDownloader`` defaults to `.performDefaultHandling` upon receiving a server challenge. To supply custom \ncredentials, configure an ``ImageDownloader/authenticationChallengeResponder``:\n\n```swift\n// In ViewController\nImageDownloader.default.authenticationChallengeResponder = self\n\nextension ViewController: AuthenticationChallengeResponsable {\n\n    var disposition: URLSession.AuthChallengeDisposition { /* */ }\n    let credential: URLCredential? { /* */ }\n\n    func downloader(\n        _ downloader: ImageDownloader,\n        didReceive challenge: URLAuthenticationChallenge,\n        completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)\n    {\n        // Provide your `AuthChallengeDisposition` and `URLCredential`\n        completionHandler(disposition, credential)\n    }\n\n    func downloader(\n        _ downloader: ImageDownloader,\n        task: URLSessionTask,\n        didReceive challenge: URLAuthenticationChallenge,\n        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)\n    {\n        // Provide your `AuthChallengeDisposition` and `URLCredential`\n        completionHandler(disposition, credential)\n    }\n}\n```\n\n### Set customize timeout\n\nThe default download timeout for a request is 15 seconds. To customize this for the downloader:\n\n```swift\n// Set the timeout to 1 minute.\ndownloader.downloadTimeout = 60\n```\n\nFor setting a timeout specific to a request, utilize a ``KingfisherOptionsInfoItem/requestModifier(_:)``:\n\n```swift\nlet modifier = AnyModifier { request in\n    var r = request\n    r.timeoutInterval = 60\n    return r\n}\ndownloader.downloadImage(with: url, options: [.requestModifier(modifier)])\n```\n"
  },
  {
    "path": "Sources/Documentation.docc/CommonTasks/CommonTasks_Processor.md",
    "content": "# Common tasks - Processor\n\nCommon tasks related to the ``ImageProcessor`` in Kingfisher.\n\n## Overview\n\n``ImageProcessor`` is used to transform an image (or data) into another image. By supplying a processor to \n``KingfisherManager`` when setting the image, it can be applied to the downloaded data. The processed image will then \nbe sent to the image view and stored in the cache.\n\n### Use the default processor\n\n```swift\n// Just without anything\nimageView.kf.setImage(with: url)\n// It equals to\nimageView.kf.setImage(with: url, options: [.processor(DefaultImageProcessor.default)])\n```\n\n> The ``DefaultImageProcessor`` converts downloaded data into a corresponding image object. \n> It supports PNG, JPEG, and GIF formats.\n\n### Built-in Processors\n\n@Row {\n    @Column(size: 3) {\n        ```swift\n        // Round corner\n        RoundCornerImageProcessor(cornerRadius: 20)\n        ```\n    }\n    \n    @Column {\n        ![A screenshot of the power picker user interface with four powers displayed – ice, fire, wind, and lightning](common-tasks-card)\n    }\n}\n\n```swift\n// Round corner\nlet processor = RoundCornerImageProcessor(cornerRadius: 20)\n\n// Downsampling\nlet processor = DownsamplingImageProcessor(size: CGSize(width: 100, height: 100))\n\n// Cropping\nlet processor = CroppingImageProcessor(size: CGSize(width: 100, height: 100), anchor: CGPoint(x: 0.5, y: 0.5))\n\n// Blur\nlet processor = BlurImageProcessor(blurRadius: 5.0)\n\n// Overlay with a color & fraction\nlet processor = OverlayImageProcessor(overlay: .red, fraction: 0.7)\n\n// Tint with a color\nlet processor = TintImageProcessor(tint: .blue)\n\n// Adjust color\nlet processor = ColorControlsProcessor(brightness: 1.0, contrast: 0.7, saturation: 1.1, inputEV: 0.7)\n\n// Black & White\nlet processor = BlackWhiteProcessor()\n\n// Blend (iOS)\nlet processor = BlendImageProcessor(blendMode: .darken, alpha: 1.0, backgroundColor: .lightGray)\n\n// Compositing\nlet processor = CompositingImageProcessor(compositingOperation: .darken, alpha: 1.0, backgroundColor: .lightGray)\n\n// Use the process in view extension methods.\nimageView.kf.setImage(with: url, options: [.processor(processor)])\n```\n\n### Multiple Processors\n\n```swift\n// First blur the image, then make it round cornered.\nlet processor = BlurImageProcessor(blurRadius: 4) |> RoundCornerImageProcessor(cornerRadius: 20)\nimageView.kf.setImage(with: url, options: [.processor(processor)])\n```\n\n### Creating your own processor\n\nMake a type conforming to `ImageProcessor` by implementing `identifier` and `process`:\n\n> important: ``ImageProcessor/identifier`` is used to determine the cache key when this processor is applied. It is your\n> responsibility to keep it the same for processors with the same properties/functionality.\n\n```swift\nstruct MyProcessor: ImageProcessor {\n\n    let someValue: Int\n\n    var identifier: String { \"com.yourdomain.myprocessor-\\(someValue)\" }\n    \n    // Convert input data/image to target image and return it.\n    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? {\n        switch item {\n        case .image(let image):\n            // A previous processor already converted the image to an image object.\n            // You can do whatever you want to apply to the image and return the result.\n            return image\n        case .data(let data):\n            // Your own way to convert some data to an image.\n            return createAnImage(data: data)\n        }\n    }\n}\n```\n\nThen pass it to the ``KingfisherWrapper/setImage(with:placeholder:options:completionHandler:)-9h820`` methods:\n\n```swift\nlet processor = MyProcessor(someValue: 10)\nlet url = URL(string: \"https://example.com/my_image.png\")\nimageView.kf.setImage(with: url, options: [.processor(processor)])\n```\n\n### Creating a processor from CIFilter\n\nIf you have a prepared `CIFilter`, you can create a processor quickly from it.\n\n```swift\nstruct MyCIFilter: CIImageProcessor {\n\n    let identifier = \"com.yourdomain.myCIFilter\"\n    \n    let filter = Filter { input in\n        guard let filter = CIFilter(name: \"xxx\") else { return nil }\n        filter.setValue(input, forKey: kCIInputBackgroundImageKey)\n        return filter.outputImage\n    }\n}\n```\n"
  },
  {
    "path": "Sources/Documentation.docc/CommonTasks/CommonTasks_Serializer.md",
    "content": "# Common Tasks - Serializer\n\n``CacheSerializer`` is utilized to convert data into an image object for retrieval from disk cache, and conversely, \nfor storing images to the disk cache.\n\n### Use the default serializer\n\n```swift\n// Just without anything\nimageView.kf.setImage(with: url)\n// It equals to\nimageView.kf.setImage(with: url, options: [.cacheSerializer(DefaultCacheSerializer.default)])\n```\n\n``DefaultCacheSerializer`` is responsible for converting cached data into a corresponding image object and vice versa. \nIt supports PNG, JPEG, and GIF formats by default.\n\nWhen storing an image to disk, if the `original` data is available (for example, when the image is just downloaded),\n``DefaultCacheSerializer`` uses it to determine the format.\nIf `original` is `nil` (for example, when the image is retrieved from cache), Kingfisher will try to infer the format:\nif the image still carries embedded GIF data, it will be stored as GIF; otherwise it falls back to encoding as PNG.\n\n### Notes for animated images and custom processors\n\nFor animated images, Kingfisher keeps the original GIF bytes as an internal associated object on the image instance.\nIf a custom ``ImageProcessor`` creates and returns a new `UIImage`/`NSImage` instance from an animated image, this\ninternal animated data is not automatically carried over, and the disk cache may fall back to encoding as PNG (first\nframe only).\n\nTo avoid this:\n\n- For animated inputs, return the input image directly when possible.\n- If you need to create a new image instance in the `.image` branch, copy Kingfisher internal states to the new image\n  by calling ``KingfisherWrapper/copyKingfisherState(to:)``.\n- If your goal is to avoid re-encoding, consider caching original data by using a serializer with\n  ``CacheSerializer/originalDataUsed`` enabled (e.g. configure ``DefaultCacheSerializer/preferCacheOriginalData``).\n\n### Enforce a format\n\nTo enforce a specific image format, use ``FormatIndicatedCacheSerializer``, which offers serializers for all supported\nformats: ``FormatIndicatedCacheSerializer/png``, ``FormatIndicatedCacheSerializer/jpeg``, and \n``FormatIndicatedCacheSerializer/gif``.\n\n#### Use PNG serializer when rounding image corner\n\nWhile ``DefaultCacheSerializer`` aims to preserve the original format of input image data, there are scenarios where \nthis behavior might not meet your needs. For example, when using a ``RoundCornerImageProcessor``, it's often desirable \nto maintain an alpha channel for transparency around the corners. JPEG images, lacking an alpha channel, would not \nsupport this transparency when saved. To ensure the presence of an alpha channel by converting images to PNG, you can\nset the PNG serializer explicitly:\n\n```swift\nlet roundCorner = RoundCornerImageProcessor(cornerRadius: 20)\nimageView.kf.setImage(with: url, \n    options: [.processor(roundCorner), \n              .cacheSerializer(FormatIndicatedCacheSerializer.png)]\n)\n```\n\n### Creating customized serializer\n\nMake a type conforming to `CacheSerializer` by implementing `data(with:original:)` and `image(with:options:)`:\nTo create a type that conforms to ``CacheSerializer``, implement the ``CacheSerializer/data(with:original:)`` \nand ``CacheSerializer/image(with:options:)``:\n\n```swift\nstruct MyCacheSerializer: CacheSerializer {\n    func data(with image: Image, original: Data?) -> Data? {\n        return MyFramework.data(of: image)\n    }\n    \n    func image(with data: Data, options: KingfisherParsedOptionsInfo?) -> Image? {\n        return MyFramework.createImage(from: data)\n    }\n}\n```\n\nThen pass it to the ``KingfisherWrapper/setImage(with:placeholder:options:completionHandler:)-9h820`` methods:\n\n```swift\nlet serializer = MyCacheSerializer()\nlet url = URL(string: \"https://yourdomain.com/example.png\")\nimageView.kf.setImage(with: url, options: [.cacheSerializer(serializer)])\n```\n"
  },
  {
    "path": "Sources/Documentation.docc/Documentation.md",
    "content": "# ``Kingfisher``\n\n@Metadata {\n    @PageImage(\n        purpose: icon, \n        source: \"logo\", \n        alt: \"The logo icon of Kingfisher\")\n    @PageColor(blue)\n}\n\nA lightweight, pure-Swift library for downloading and caching images from the web.\n\n## Overview\n\nKingfisher is a powerful, pure-Swift library for downloading and caching images from the web. It provides you a chance \nto use a pure-Swift way to work with remote images in your next app, regardless you are using UIKit, AppKit or SwiftUI.\n\nWith Kingfisher, you can easily:\n\n- **Download** the images from a remote URL and display it in an image view or button.\n- **Cache** the images in both the memory and the disk. When loading for the next time, it shows immediately without\ndownloading again.\n- **Process** the downloaded images with pre-defined or customized processors. \n\n### Featured\n\n@Links(visualStyle: detailedGrid) {\n    - <doc:GettingStarted>\n    - <doc:CommonTasks>\n}\n\n\n## Topics\n\n### Essentials\n\n- <doc:GettingStarted>\n- <doc:CommonTasks>\n\n### Loading Images in Simple Way\n\n- ``KingfisherCompatible``\n- ``KingfisherWrapper/setImage(with:placeholder:options:completionHandler:)-8qfkr``\n- ``KingfisherManager``\n- ``Source``\n\n### Loading Options\n\n- ``KingfisherOptionsInfoItem``\n\n### Image Downloader\n\n- <doc:CommonTasks_Downloader>\n- ``ImageDownloader``\n- ``ImagePrefetcher``\n- ``DownloadTask``\n\n### Image Processor\n\n@Links(visualStyle: detailedGrid) {\n    - <doc:CommonTasks_Processor>\n    - ``ImageProcessor``\n}\n\n### Image Cache & Serializer\n\n- <doc:CommonTasks_Cache>\n- <doc:CommonTasks_Serializer>\n- ``ImageCache``\n- ``CacheSerializer``\n\n### GIF\n\n- ``AnimatedImageView``\n- ``GIFAnimatedImage``\n\n### Live Photo\n\n- <doc:Topic_LivePhoto>\n- ``KingfisherWrapper/setImage(with:options:completionHandler:)-1to8a``\n\n### SwiftUI\n\n- ``KFImage``\n\n### Help & Communication\n\n- <doc:MigrationGuide>\n- <doc:CHANGELOG>\n\n"
  },
  {
    "path": "Sources/Documentation.docc/GettingStarted.md",
    "content": "# Getting Started\n\n@Metadata {\n    @PageImage(purpose: card, source: \"getting-started-card\"))\n    @PageColor(blue)\n}\n\nInstalls Kingfisher to your project, setup everything and some starter examples of the core functionality.\n\n## Overview\n\nKingfisher is designed to facilitate the downloading and caching of remote images in the simplest way possible. \nAs such, the basic usage of Kingfisher is straightforward. We offer two step-by-step tutorials to help you understand \nand utilize Kingfisher's fundamental features in both UIKit and SwiftUI environments. The tutorials will cover the \nfollowing aspects:\n\n##### Installing Kingfisher\nLearn how to integrate Kingfisher into your project setup.\n\n##### Loading and Displaying Images\nDiscover how to effortlessly fetch and display images from remote URLs using convenient view extensions.\n\n##### Processing Images with Processors\n\nUnderstand how to manipulate and transform images using the ImageProcessor functionality.\n\n##### Inspecting and Managing Image Cache\n\nGain insights into how to check the image cache status and handle image caching.\n\n## Tutorials\n\nBy following these tutorials, you will acquire a preliminary understanding of Kingfisher, laying the groundwork for \npotential advanced usage in the future.\n\nChoose the approach you prefer to begin the tutorial (UIKit or SwiftUI):\n\n@Links(visualStyle: list) {\n    - <doc:GettingStartedUIKit>\n    - <doc:GettingStartedSwiftUI>\n}\n\n> tip: In addition to UIKit and SwiftUI, Kingfisher also offers support for use in AppKit. This extends Kingfisher's \n> versatility across different Apple platforms, providing a unified API for handling remote images.\n>\n> If you are interested in utilizing Kingfisher within an AppKit context, we recommend referring to the UIKit \n> tutorials as a starting point. Most of the concept, even the APIs, are shared.\n"
  },
  {
    "path": "Sources/Documentation.docc/MigrationGuide/Migration-To-6.md",
    "content": "# Migrating from v5 to v6\n\nMigrating Kingfisher from version 5 to version 6.\n\n## Overview\n\nKingfisher 6.0 contains some breaking changes if you want to upgrade from the previous version. \n\nDepending on your use cases of Kingfisher 5.x, it may take no effort or at most several minutes to fix errors and warnings after upgrading. If you are not using Kingfisher with SwiftUI, and have no warnings in your code related to Kingfisher, then you are already done and feel free to upgrade to the latest version. Otherwise, please read the sections below before performing the upgrade.\n\n### SwiftUI support\n\nKingfisher started to support SwiftUI from [5.8.0](https://github.com/onevcat/Kingfisher/releases/tag/5.8.0). At that time, a new framework was added to handle all SwiftUI-related things. Search for `KingfisherSwiftUI` in your SwiftUI code, or check if there is a `Kingfisher/SwiftUI` entry in your Podfile. If there is, then you need to perform some change of the integrating way before continuing.\n\nIn Kingfisher 6, to make the project structure simpler, as well as treat SwiftUI as the first citizen in the library, we combined the library for SwiftUI into the main Kingfisher target.\n\nThat means, there is no `KingfisherSwiftUI` or `Kingfisher/SwiftUI` anymore. If you installed it through:\n\n- Carthage: Remove `KingfisherSwiftUI` from \"Linked Frameworks and Libraries\" and all \"KingfisherSwiftUI.framework\" related lines from the \"copy-framework\".\n- CocoaPods: Remove `pod 'Kingfisher/SwiftUI'` from your Podfile. To continue using Kingfisher, you still need to keep or add back `pod 'Kingfisher'` entry. Then, run `pod install` again.\n- Swift Package Manager: Since now there is only one framework, all the old \"static\" and \"dynamic\" variants are removed. We suggest a clean reinstallation for the new version. Check the [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide) for more.\n\nWhen it is done, you can now replace any `import KingfisherSwiftUI` with `import Kingfisher`.\n\n### Removing legacy deprecated code\n\nAll deprecated types, methods and properties are removed from the code base. Before upgrading, please make sure there is no warnings left in your project which complain the using of deprecated code. All deprecated things have replacement and with the help of warning message, adapting to new code should be easy enough.\n\nIf you are curious about what are exactly removed, check [these commits](https://github.com/onevcat/Kingfisher/pull/1525/files).\n"
  },
  {
    "path": "Sources/Documentation.docc/MigrationGuide/Migration-To-7.md",
    "content": "# Migrating from v6 to v7\n\nMigrating Kingfisher from version 6 to version 7.\n\n## Overview\n\nKingfisher 7.0 contains some breaking changes if you want to upgrade from the previous version. In this documentation, we will cover most of the noticeable API changes.\n\n### Deploy target\n\nThe UIKit/AppKit part of Kingfisher now supports from:\n\n- iOS 12.0\n- macOS 10.14\n- tvOS 12.0\n- watchOS 5.0\n\n> We do not have proper simulator support or device of versions before those. So dropping any older versions give us a chance to make sure the project works properly on all supported versions. This also fixes a compiling issue when building with Xcode 13 with SPM.\n\nThe SwiftUI part of Kingfisher now supports from \n\n- iOS 14\n- macOS 11.0\n- tvOS 14.0\n- watchOS 7.0\n\n> On iOS 13, there is no `@StateObject` property wrapper, which makes it very tricky when loading data properly across difference view body evaluating. For a stable data model in Kingfisher's SwiftUI, we need to drop iOS 13 and all other platform versions from the same year.\n\n### Migration Steps\n\nThe main breaking changes happens to the SwiftUI support. By following the steps you should be able to migrate to the new version.\n\n- Make sure you do not have any warning from Kingfisher. All previous deprecated methods and properties are removed in version 7. If you are still using some of the deprecated methods, follow the help message to fix them first before migrating.\n- The original ``KFImage`` initializers: `init(source:isLoaded:)` and `init(_:isLoaded:)` are removed. Or strictly speaking, the `isLoaded` parameter is removed. If you are not using the `isLoaded` binding before, the transition to the new initializer ``KFImage/init(source:)`` and ``KFImage/init(_:)`` is transparent.\n    - The `isLoaded` binding was a mis-use of binding and it did not do what is expected. If you need to get a state of loading of a ``KFImage``, change a `@State` yourself in the related ``KFImage`` lifecycle modifier: such as ``KFImage/onSuccess(_:)`` and ``KFImage/onFailure(_:)``.\n    - All of the `isLoaded` parameter are also removed from the chain-able ``KF`` shorthand.\n- If you are using ``KFImage/loadImmediately(_:)`` to get workaround of [#1660](https://github.com/onevcat/Kingfisher/issues/1660), it is not necessary in the new version anymore. You will have a warning and please just remove it.\n"
  },
  {
    "path": "Sources/Documentation.docc/MigrationGuide/Migration-To-8.md",
    "content": "# Migrating from v7 to v8\n\nThis guide assists you in updating Kingfisher from version 7 to version 8.\n\n## Overview\n\nKingfisher 8.0 introduces breaking changes from its predecessor. This document highlights the major updates and significant API modifications.\n\n## Deployment Target\n\nStarting with Kingfisher 8.0, the minimum supported versions are:\n\n- iOS 13.0\n- macOS 10.15\n- tvOS 13.0\n- watchOS 6.0\n- visionOS 1.0\n\n## Migration Steps and Insights\n\nFirst, ensure there are no existing warnings from Kingfisher. Several deprecated methods and properties have been removed in version 8.\n\nFor the breaking changes, review the sections below for any utilized features and symbols.\n\n### MainActor Requirement\n\nAs support for Swift Concurrency is introduced in Kingfisher 8, some APIs, usually the view extension ones, require the `MainActor` attribute. Ensure your codebase is updated to include this attribute where necessary. For usage in `UIViewController` and `UIView`, since they are already implicitly under `MainActor`, no additional changes are required. For other cases, if you encounter a compiler error:\n\n\n```swift\nclass Foo {\n    func bar() {\n        UIImageView().kf.setImage(with: URL(string: \"https://example.com/image.png\"))\n    }\n}\n```\n\n> warning:\n>\n> Call to main actor-isolated instance method 'setImage(with:placeholder:options:completionHandler:)' in a synchronous nonisolated context.\n\nTry to limit the access to the `MainActor`. For example, add the `MainActor` attribute to the method:\n\n```swift\nclass Foo {\n    @MainActor\n    func bar() {\n        UIImageView().kf.setImage(with: URL(string: \"https://example.com/image.png\"))\n    }\n}\n```\n\nThe concurrence support in Kingfisher 8 is not yet fully \"strictly-compatible\". That means if you set `SWIFT_STRICT_CONCURRENCY` to `Complete`, you may still see some warnings. The current status of Swift Concurrency does not contain all the necessary isolation for us to make the library fully compatible. We are working on it and will provide a fully compatible version in the future.\n\n### Disk Cache Changes\n\nVersion 8 updates the disk cache hash calculation method, invalidating existing caches. Kingfisher's disk cache is resilient, automatically re-downloading and caching data if missing. Typically, no action is required unless your application's logic heavily relies on the disk cache, which is generally not recommended.\n\n### Swift Concurrency APIs\n\nKingfisher now embraces Swift's `async` keyword, enhancing most asynchronous APIs previously implemented with completion handlers. While the traditional APIs remain in struct and class types, some protocol methods have transitioned to `async` without the traditional ones. \n\nEnsure your implementations conform to these changes.\n\n#### `ImageDownloadRedirectHandler` Protocol\n\nThe `handleHTTPRedirection(for:response:newRequest:completionHandler:)` method has been replaced with an asynchronous counterpart. Update your implementation accordingly:\n\n```swift\n// Old\nextension YourType: ImageDownloadRedirectHandler {\n    func handleHTTPRedirection(\n        for task: Kingfisher.SessionDataTask,\n        response: HTTPURLResponse, \n        newRequest: URLRequest, \n        completionHandler: @escaping (URLRequest?) -> Void\n    ) {\n        // Do something with the result, potentially in async way\n        requestUpdater.update(newRequest) { result in\n            completionHandler(result)\n        }\n    }\n}\n```\n\n```swift\n// New\nextension YourType: ImageDownloadRedirectHandler {\n    func handleHTTPRedirection(\n        for task: Kingfisher.SessionDataTask,\n        response: HTTPURLResponse, \n        newRequest: URLRequest\n    ) async -> URLRequest? {\n        let result = await requestUpdater.update(newRequest)\n        return result\n    }\n}\n```\n\n#### `AsyncImageDownloadRequestModifier` Protocol\n\nThe `modified(for:reportModified:)` method is now asynchronous. Reimplement it if used:\n\n```swift\n// Old\nextension YourType: AsyncImageDownloadRequestModifier {\n    func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) {\n        reportModified(request)\n    }\n}\n```\n\n```swift\n// New\nextension YourType: AsyncImageDownloadRequestModifier {\n    func modified(for request: URLRequest) async -> URLRequest? {\n        return request\n    }\n}\n```\n\n#### `AuthenticationChallengeResponsible` Protocol\n\nThe following methods have been updated to async versions:\n\n- `downloader(_:didReceive:completionHandler:)`\n- `downloader(_:task:didReceive:completionHandler:)`\n\nEnsure your implementation is current:\n\n```swift\n// Old\nextension YourType: AuthenticationChallengeResponsible {\n    func downloader(\n        _ downloader: ImageDownloader,\n        didReceive challenge: URLAuthenticationChallenge,\n        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void\n    )\n    {\n        generateCredential { credential in\n            completionHandler(.useCredential, credential)\n        }\n    }\n\n    func downloader(\n        _ downloader: ImageDownloader,\n        task: URLSessionTask,\n        didReceive challenge: URLAuthenticationChallenge,\n        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void\n    )\n    {\n        generateCredential { credential in\n            completionHandler(.useCredential, credential)\n        }\n    }\n}\n```\n\n```swift\n// New\nextension YourType: AuthenticationChallengeResponsible {\n    func downloader(\n        _ downloader: ImageDownloader,\n        didReceive challenge: URLAuthenticationChallenge\n    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?)\n    {\n        let credential = await generateCredential()\n        return (.useCredential, credential)\n    }\n\n    func downloader(\n        _ downloader: ImageDownloader,\n        task: URLSessionTask,\n        didReceive challenge: URLAuthenticationChallenge\n    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?)\n    {\n        let credential = await generateCredential()\n        return (.useCredential, credential)\n    }\n}\n```\n\n### Type Adjustments\n\n#### `ColorElement`\n\n`Filter.ColorElement` has evolved from a typealias for a tuple to a `struct`. Instantiate `ColorElement` using its initializer:\n\n```swift\nlet brightness, contrast, saturation, inputEV: CGFloat\n\n// Old\nlet colorElement: Filter.ColorElement = (brightness, contrast, saturation, inputEV)\n\n// New\nlet colorElement = Filter.ColorElement(brightness, contrast, saturation, inputEV)\n```\n\n#### `DownloadTask`\n\n`DownloadTask` has been redefined as a `class` instead of a `struct`.\n\nFor `ImageDownloader.download` methods that previously returned optional `DownloadTask`` values, now return non-optional values instead. For example:\n\n```swift\n// old\nopen func downloadImage(\n  with url: URL,\n  options: KingfisherParsedOptionsInfo,\n  completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil\n) -> DownloadTask?\n\n// new\nopen func downloadImage(\n  with url: URL,\n  options: KingfisherParsedOptionsInfo,\n  completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil\n) -> DownloadTask\n```\n\nTo check if a download task is valid, instead of checking `nil`, use `isInitialized` instead:\n\n```swift\n// old\nlet downloadTask: DownloadTask? = downloader.downloadImage(with: url, options: options)\n\nfunc doSomethingWithTask() {\n    if let task = downloadTask {\n        // Do something with the task, for example, cancel it\n    }\n}\n\n// new\nlet downloadTask: DownloadTask = downloader.downloadImage(with: url, options: options)\n\nfunc doSomethingWithTask() {\n    if downloadTask.isInitialized {\n        // Do something with the task, for example, cancel it\n    }\n}\n```\n\n##### Cancel Token of DownloadTask\n\nIn the current implementation, the cancel token of a `DownloadTask` is an optional value, meaning it does not exist until the download task has actually started. \n\nTypically, there is no need to interact directly with the cancel token; you can simply invoke the `cancel()` method to terminate an ongoing download task.\n"
  },
  {
    "path": "Sources/Documentation.docc/MigrationGuide.md",
    "content": "# Migration Guide\n\nHow to migrate from an earlier version of Kingfisher to the latest one.\n\n@Links(visualStyle: list) {\n    - <doc:Migration-To-8>\n}\n\n\n### Archived\n\nIf you are still using an even earlier version, check the archived guide below and follow them to migrate to v7 first.\n\n@Links(visualStyle: list) {\n    - <doc:Migration-To-7>\n    - <doc:Migration-To-6>\n}\n\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-SampleCell-1.swift",
    "content": "import UIKit\n\nclass SampleCell: UITableViewCell {\n    \n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-SampleCell-2.swift",
    "content": "import UIKit\n\nclass SampleCell: UITableViewCell {\n    var sampleImageView: UIImageView = {\n        let imageView = UIImageView(frame: .zero)\n        imageView.translatesAutoresizingMaskIntoConstraints = false\n        return imageView\n    }()\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-SampleCell-3.swift",
    "content": "import UIKit\n\nclass SampleCell: UITableViewCell {\n    var sampleImageView: UIImageView = {\n        let imageView = UIImageView(frame: .zero)\n        imageView.translatesAutoresizingMaskIntoConstraints = false\n        return imageView\n    }()\n    \n    var sampleLabel: UILabel = {\n        let label = UILabel(frame: .zero)\n        label.translatesAutoresizingMaskIntoConstraints = false\n        return label\n    }()\n    \n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        \n        contentView.addSubview(sampleImageView)\n        NSLayoutConstraint.activate([\n            sampleImageView.widthAnchor.constraint(equalToConstant: 64),\n            sampleImageView.heightAnchor.constraint(equalToConstant: 64),\n            sampleImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),\n            sampleImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)\n        ])\n        \n        contentView.addSubview(sampleLabel)\n        NSLayoutConstraint.activate([\n            sampleLabel.leadingAnchor.constraint(equalTo: sampleImageView.trailingAnchor, constant: 12),\n            sampleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)\n        ])\n        \n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-1.swift",
    "content": "import UIKit\n\nclass ViewController: UIViewController {\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        // Do any additional setup after loading the view.\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-10.swift",
    "content": "override func viewDidLoad() {\n    super.viewDidLoad()\n    // Do any additional setup after loading the view.\n    print(KingfisherManager.shared)\n    \n    tableView.dataSource = self\n    view.addSubview(tableView)\n    NSLayoutConstraint.activate([\n        tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),\n        tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),\n        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),\n        tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)\n    ])\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-11.swift",
    "content": "override func viewDidLoad() {\n    super.viewDidLoad()\n    // Do any additional setup after loading the view.\n    print(KingfisherManager.shared)\n    \n    tableView.dataSource = self\n    view.addSubview(tableView)\n    NSLayoutConstraint.activate([\n        tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),\n        tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),\n        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),\n        tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)\n    ])\n    \n    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {\n        KingfisherManager.shared.cache.calculateDiskStorageSize { result in\n            switch result {\n            case .success(let size):\n                print(\"Size: \\(Double(size) / 1024 / 1024) MB\")\n            case .failure(let error):\n                print(\"Some error: \\(error)\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-12.swift",
    "content": "override func viewDidLoad() {\n    super.viewDidLoad()\n    // Do any additional setup after loading the view.\n    print(KingfisherManager.shared)\n    \n    tableView.dataSource = self\n    view.addSubview(tableView)\n    NSLayoutConstraint.activate([\n        tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),\n        tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),\n        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),\n        tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)\n    ])\n    \n    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {\n        KingfisherManager.shared.cache.calculateDiskStorageSize { result in\n            switch result {\n            case .success(let size):\n                let sizeInMB = Double(size) / 1024 / 1024\n                let alert = UIAlertController(title: nil, message: String(format: \"Kingfisher Disk Cache: %.2fMB\", sizeInMB), preferredStyle: .alert)\n                alert.addAction(UIAlertAction(title: \"Purge\", style: .destructive) { _ in\n                    \n                })\n                alert.addAction(UIAlertAction(title: \"Cancel\", style: .cancel))\n                self.present(alert, animated: true)\n            case .failure(let error):\n                print(\"Some error: \\(error)\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-13.swift",
    "content": "override func viewDidLoad() {\n    super.viewDidLoad()\n    // Do any additional setup after loading the view.\n    print(KingfisherManager.shared)\n    \n    tableView.dataSource = self\n    view.addSubview(tableView)\n    NSLayoutConstraint.activate([\n        tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),\n        tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),\n        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),\n        tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)\n    ])\n    \n    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {\n        KingfisherManager.shared.cache.calculateDiskStorageSize { result in\n            switch result {\n            case .success(let size):\n                let sizeInMB = Double(size) / 1024 / 1024\n                let alert = UIAlertController(title: nil, message: String(format: \"Kingfisher Disk Cache: %.2fMB\", sizeInMB), preferredStyle: .alert)\n                alert.addAction(UIAlertAction(title: \"Purge\", style: .destructive) { _ in\n                    KingfisherManager.shared.cache.clearCache {\n                        self.tableView.reloadData()\n                    }\n                })\n                alert.addAction(UIAlertAction(title: \"Cancel\", style: .cancel))\n                self.present(alert, animated: true)\n            case .failure(let error):\n                print(\"Some error: \\(error)\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-2.swift",
    "content": "import UIKit\nimport Kingfisher\n\nclass ViewController: UIViewController {\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        // Do any additional setup after loading the view.\n        print(KingfisherManager.shared)\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-3.swift",
    "content": "import UIKit\nimport Kingfisher\n\nclass ViewController: UIViewController {\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        // Do any additional setup after loading the view.\n        print(KingfisherManager.shared)\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-4.swift",
    "content": "import UIKit\nimport Kingfisher\n\nclass ViewController: UIViewController {\n\n    lazy var tableView: UITableView = {\n        let tableView = UITableView(frame: .zero)\n        tableView.register(SampleCell.self, forCellReuseIdentifier: \"SampleCell\")\n        tableView.translatesAutoresizingMaskIntoConstraints = false\n        tableView.rowHeight = 80\n        return tableView\n    }()\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        // Do any additional setup after loading the view.\n        print(KingfisherManager.shared)\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-5.swift",
    "content": "import UIKit\nimport Kingfisher\n\nclass ViewController: UIViewController {\n\n    lazy var tableView: UITableView = {\n        let tableView = UITableView(frame: .zero)\n        tableView.register(SampleCell.self, forCellReuseIdentifier: \"SampleCell\")\n        tableView.translatesAutoresizingMaskIntoConstraints = false\n        tableView.rowHeight = 80\n        return tableView\n    }()\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        // Do any additional setup after loading the view.\n        print(KingfisherManager.shared)\n        \n        tableView.dataSource = self\n        view.addSubview(tableView)\n        NSLayoutConstraint.activate([\n            tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),\n            tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),\n            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),\n            tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)\n        ])\n    }\n}\n\nextension ViewController: UITableViewDataSource {\n    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        1\n    }\n    \n    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"SampleCell\", for: indexPath) as! SampleCell\n        cell.sampleLabel.text = \"Index \\(indexPath.row)\"\n        cell.sampleImageView.backgroundColor = .lightGray\n        return cell\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-6-0.swift",
    "content": "extension ViewController: UITableViewDataSource {\n    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        1\n    }\n    \n    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"SampleCell\", for: indexPath) as! SampleCell\n        cell.sampleLabel.text = \"Index \\(indexPath.row)\"\n        cell.sampleImageView.backgroundColor = .lightGray\n        return cell\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-6.swift",
    "content": "extension ViewController: UITableViewDataSource {\n    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        1\n    }\n    \n    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"SampleCell\", for: indexPath) as! SampleCell\n        cell.sampleLabel.text = \"Index \\(indexPath.row)\"\n        \n        let urlPrefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher\"\n        let url = URL(string: \"\\(urlPrefix)-1.jpg\")\n        cell.sampleImageView.kf.setImage(with: url)\n        \n        cell.sampleImageView.backgroundColor = .lightGray\n        return cell\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-7.swift",
    "content": "extension ViewController: UITableViewDataSource {\n    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        10\n    }\n    \n    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"SampleCell\", for: indexPath) as! SampleCell\n        cell.sampleLabel.text = \"Index \\(indexPath.row)\"\n        \n        let urlPrefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher\"\n        let url = URL(string: \"\\(urlPrefix)-\\(indexPath.row + 1).jpg\")\n        cell.sampleImageView.kf.setImage(with: url)\n        \n        cell.sampleImageView.backgroundColor = .lightGray\n        return cell\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-8.swift",
    "content": "extension ViewController: UITableViewDataSource {\n    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        10\n    }\n    \n    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"SampleCell\", for: indexPath) as! SampleCell\n        cell.sampleLabel.text = \"Index \\(indexPath.row)\"\n        \n        let urlPrefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher\"\n        let url = URL(string: \"\\(urlPrefix)-\\(indexPath.row + 1).jpg\")\n        \n        cell.sampleImageView.kf.indicatorType = .activity\n        \n        let roundCorner = RoundCornerImageProcessor(radius: .widthFraction(0.5), roundingCorners: [.topLeft, .bottomRight])\n        let pngSerializer = FormatIndicatedCacheSerializer.png\n        cell.sampleImageView.kf.setImage(\n            with: url,\n            options: [.processor(roundCorner), .cacheSerializer(pngSerializer)]\n        )\n        cell.sampleImageView.backgroundColor = .clear\n        return cell\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/01-ViewController-9.swift",
    "content": "// cell.sampleImageView.kf.setImage(with: url, options: [.processor(roundCorner)])\ncell.sampleImageView.kf.setImage(with: url, options: [.processor(roundCorner)]) { result in\n    switch result {\n    case .success(let imageResult):\n        print(\"Image loaded from cache: \\(imageResult.cacheType)\")\n    case .failure(let error):\n        print(\"Error: \\(error)\")\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-1.swift",
    "content": "import SwiftUI\n\nstruct ContentView: View {\n    var body: some View {\n        VStack {\n            Image(systemName: \"globe\")\n                .imageScale(.large)\n                .foregroundStyle(.tint)\n            Text(\"Hello, world!\")\n        }\n        .padding()\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-10.swift",
    "content": "@State var showAlert = false\n@State var cacheSizeResult: Result<UInt, KingfisherError>? = nil\n\nvar body: some View {\n    List {\n        Button(\"Check Cache\") {\n            KingfisherManager.shared.cache.calculateDiskStorageSize { result in\n                cacheSizeResult = result\n                showAlert = true\n            }\n        }\n        .alert(\n            \"Disk Cache\",\n            isPresented: $showAlert,\n            presenting: cacheSizeResult,\n            actions: { result in\n                // TODO: Actions\n            }, message: { result in\n                switch result {\n                case .success(let size):\n                    Text(\"Size: \\(Double(size) / 1024 / 1024) MB\")\n                case .failure(let error):\n                    Text(error.localizedDescription)\n                }\n            })\n        \n        ForEach(0 ..< 10) { i in\n            HStack {\n                KFImage(url(at: i))\n                // ...\n            }\n        }\n    }.listStyle(.plain)\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-11.swift",
    "content": "@State var showAlert = false\n@State var cacheSizeResult: Result<UInt, KingfisherError>? = nil\n\nvar body: some View {\n    List {\n        Button(\"Check Cache\") {\n            KingfisherManager.shared.cache.calculateDiskStorageSize { result in\n                cacheSizeResult = result\n                showAlert = true\n            }\n        }\n        .alert(\n            \"Disk Cache\",\n            isPresented: $showAlert,\n            presenting: cacheSizeResult,\n            actions: { result in\n                switch result {\n                case .success:\n                    Button(\"Clear\") {\n                        KingfisherManager.shared.cache.clearCache()\n                    }\n                    Button(\"Cancel\", role: .cancel) {}\n                case .failure:\n                    Button(\"OK\") { }\n                }\n            }, message: { result in\n                switch result {\n                case .success(let size):\n                    Text(\"Size: \\(Double(size) / 1024 / 1024) MB\")\n                case .failure(let error):\n                    Text(error.localizedDescription)\n                }\n            })\n        \n        ForEach(0 ..< 10) { i in\n            HStack {\n                KFImage(url(at: i))\n                // ...\n            }\n        }\n    }.listStyle(.plain)\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-2.swift",
    "content": "import SwiftUI\nimport Kingfisher\n\nstruct ContentView: View {\n    var body: some View {\n        VStack {\n            Image(systemName: \"globe\")\n                .imageScale(.large)\n                .foregroundStyle(.tint)\n            Text(\"Hello, world!\")\n        }\n        .padding()\n        .onAppear {\n            print(KingfisherManager.shared)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-3.swift",
    "content": "import SwiftUI\nimport Kingfisher\n\nstruct ContentView: View {\n    var body: some View {\n        List {\n            ForEach(0 ..< 10) { i in\n                HStack {\n                    Rectangle().fill(Color.gray)\n                        .frame(width: 64, height: 64)\n                    Text(\"Index \\(i)\")\n                }\n            }\n        }.listStyle(.plain)\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-4.swift",
    "content": "import SwiftUI\nimport Kingfisher\n\nstruct ContentView: View {\n    func url(at index: Int) -> URL? {\n        let urlPrefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher\"\n        return URL(string: \"\\(urlPrefix)-\\(index + 1).jpg\")\n    }\n    \n    var body: some View {\n        List {\n            ForEach(0 ..< 10) { i in\n                HStack {\n                    KFImage(url(at: i))\n                        .resizable()\n                        .frame(width: 64, height: 64)\n                    Text(\"Index \\(i)\")\n                }\n            }\n        }.listStyle(.plain)\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-5.swift",
    "content": "import SwiftUI\nimport Kingfisher\n\nstruct ContentView: View {\n    func url(at index: Int) -> URL? {\n        let urlPrefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher\"\n        return URL(string: \"\\(urlPrefix)-\\(index + 1).jpg\")\n    }\n    \n    var body: some View {\n        List {\n            ForEach(0 ..< 10) { i in\n                HStack {\n                    KFImage(url(at: i))\n                        .resizable()\n                        .roundCorner(\n                            radius: .widthFraction(0.5),\n                            roundingCorners: [.topLeft, .bottomRight]\n                        )\n                        .serialize(as: .PNG)\n                        .frame(width: 64, height: 64)\n                    Text(\"Index \\(i)\")\n                }\n            }\n        }.listStyle(.plain)\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-6.swift",
    "content": "import SwiftUI\nimport Kingfisher\n\nstruct ContentView: View {\n    func url(at index: Int) -> URL? {\n        let urlPrefix = \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher\"\n        return URL(string: \"\\(urlPrefix)-\\(index + 1).jpg\")\n    }\n    \n    var body: some View {\n        List {\n            ForEach(0 ..< 10) { i in\n                HStack {\n                    KFImage(url(at: i))\n                        .resizable()\n                        .roundCorner(\n                            radius: .widthFraction(0.5),\n                            roundingCorners: [.topLeft, .bottomRight]\n                        )\n                        .serialize(as: .PNG)\n                        .onSuccess { result in\n                            print(\"Image loaded from cache: \\(result.cacheType)\")\n                        }\n                        .onFailure { error in\n                            print(\"Error: \\(error)\")\n                        }\n                        .frame(width: 64, height: 64)\n                    Text(\"Index \\(i)\")\n                }\n            }\n        }.listStyle(.plain)\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-7.swift",
    "content": "var body: some View {\n    List {\n        ForEach(0 ..< 10) { i in\n            HStack {\n                KFImage(url(at: i))\n                // ...\n            }\n        }\n    }.listStyle(.plain)\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-8.swift",
    "content": "var body: some View {\n    List {\n        Button(\"Check Cache\") {\n            KingfisherManager.shared.cache.calculateDiskStorageSize { result in\n                switch result {\n                case .success(let size):\n                    print(\"Size: \\(Double(size) / 1024 / 1024) MB\")\n                case .failure(let error):\n                    print(\"Some error: \\(error)\")\n                }\n            }\n        }\n        ForEach(0 ..< 10) { i in\n            HStack {\n                KFImage(url(at: i))\n                // ...\n            }\n        }\n    }.listStyle(.plain)\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Resources/code-files/02-ContentView-9.swift",
    "content": "@State var showAlert = false\n@State var cacheSizeResult: Result<UInt, KingfisherError>? = nil\n\nvar body: some View {\n    List {\n        Button(\"Check Cache\") {\n            KingfisherManager.shared.cache.calculateDiskStorageSize { result in\n                cacheSizeResult = result\n                showAlert = true\n            }\n        }\n        ForEach(0 ..< 10) { i in\n            HStack {\n                KFImage(url(at: i))\n                // ...\n            }\n        }\n    }.listStyle(.plain)\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Topics/Topic_ImageDataProvider.md",
    "content": "# Understanding the ImageDataProvider\n\nLoading a local image or loading from data.\n\n## Overview\n\nKingfisher supports setting images from a local data source, allowing you to leverage its features for processing and\nmanaging local image data, bypassing the need for network downloads. \n\nThis allows for uniform API calls for both remote and local images, facilitating the reuse of familiar concepts, such \nas existing processors and cache serializers.\n\n### Image from local file\n\n``LocalFileImageDataProvider`` is a type that conforms to ``ImageDataProvider``. It is specifically designed for \nloading images from local file URLs:\n\n```swift\nlet url = URL(fileURLWithPath: path)\nlet provider = LocalFileImageDataProvider(fileURL: url)\nimageView.kf.setImage(with: provider)\n```\n\nYou can also pass options to it:\n\n```swift\nlet processor = RoundCornerImageProcessor(cornerRadius: 20)\nimageView.kf.setImage(with: provider, options: [.processor(processor)])\n```\n\n### Image from Base64 string\n\nUtilize ``Base64ImageDataProvider`` to supply an image from base64 encoded string. All standard features, including\ncaching and image processing, function identically to how they operate when retrieving images via a URL.\n\n```swift\nlet provider = Base64ImageDataProvider(base64String: \"\\/9j\\/4AAQSkZJRgABAQA...\", cacheKey: \"some-cache-key\")\nimageView.kf.setImage(with: provider)\n```\n\n### Generating image from AVAsset\n\nEmploy ``AVAssetImageDataProvider`` to create an image from a video URL or `AVAsset` at a specified time, \nleveraging Kingfisher's capabilities for handling video-based image sources.\n\n```swift\nlet provider = AVAssetImageDataProvider(\n    assetURL: URL(string: \"https://example.com/your_video.mp4\")!,\n    seconds: 15.0\n)\n```\n\n### Creating a customize ``ImageDataProvider``\n\nTo create your own image data provider, implement the ``ImageDataProvider`` protocol. This requires implementing a\n``ImageDataProvider/cacheKey`` for unique identification and a ``ImageDataProvider/data(handler:)`` method to supply\nimage data:\n\n```swift\nstruct UserNameLetterIconImageProvider: ImageDataProvider {\n    var cacheKey: String { return letter }\n    let letter: String\n    \n    init(userNameFirstLetter: String) {\n        self.letter = userNameFirstLetter\n    }\n    \n    func data(handler: @escaping (Result<Data, any Error>) -> Void) {\n        \n        // You can ignore these detail below.\n        // It generates some data for an image with `letter` being rendered in the center.\n\n        let rect = CGRect(x: 0, y: 0, width: 250, height: 250)\n        let renderer = UIGraphicsImageRenderer(size: rect.size)\n        let data = renderer.pngData { context in\n            UIColor.systemYellow.setFill()\n            context.fill(rect)\n            \n            let attributes = [\n                NSAttributedString.Key.foregroundColor: UIColor.white,\n                                      .font: UIFont.systemFont(ofSize: 200)\n            ]\n            \n            let textSize = letter.size(withAttributes: attributes)\n            let textRect = CGRect(\n                x: (rect.width - textSize.width) / 2,\n                y: (rect.height - textSize.height) / 2,\n                width: textSize.width,\n                height: textSize.height)\n            letter.draw(in: textRect, withAttributes: attributes)\n        }\n\n        // Provide the image data in `handler`.\n        handler(.success(data))\n    }\n}\n\n// Set image for user \"John\"\nlet provider = UserNameLetterIconImageProvider(userNameFirstLetter: \"J\")\nimageView.kf.setImage(\n    with: provider,\n    options: [.processor(RoundCornerImageProcessor(radius: .point(75)))]\n)\n```\n\nThis generates a result like:\n\n@Image(source: imagedataprovider-sample)\n\nYou might have noticed that ``ImageDataProvider/data(handler:)`` includes a callback. This allows you to supply the\nimage data asynchronously from a different thread, which is useful if processing on the main thread is too heavy.\n"
  },
  {
    "path": "Sources/Documentation.docc/Topics/Topic_Indicator.md",
    "content": "# Loading Indicator \n\nSetting and customizing indicator while loading.\n\n#### Using the standard indicator\n\n```swift\nimageView.kf.indicatorType = .activity\nimageView.kf.setImage(with: url)\n```\n\n#### Using an image as indicator\n\n```swift\nlet path = Bundle.main.path(forResource: \"loader\", ofType: \"gif\")!\nlet data = try! Data(contentsOf: URL(fileURLWithPath: path))\n\nimageView.kf.indicatorType = .image(imageData: data)\nimageView.kf.setImage(with: url)\n```\n\n#### Using a customized view\n\n```swift\nstruct MyIndicator: Indicator {\n    let view: UIView = UIView()\n    \n    func startAnimatingView() { view.isHidden = false }\n    func stopAnimatingView() { view.isHidden = true }\n    \n    init() {\n        view.backgroundColor = .red\n    }\n}\n\nlet i = MyIndicator()\nimageView.kf.indicatorType = .custom(indicator: i)\n```\n\n#### Updating indicator with percentage progress\n\n```swift\nimageView.kf.setImage(with: url, progressBlock: {\n    receivedSize, totalSize in\n    let percentage = (Float(receivedSize) / Float(totalSize)) * 100.0\n    print(\"downloading progress: \\(percentage)%\")\n    myIndicator.percentage = percentage\n})\n```\n\nThe `progressBlock` is called only when the server's response includes a \"Content-Length\" in the header.\n"
  },
  {
    "path": "Sources/Documentation.docc/Topics/Topic_LivePhoto.md",
    "content": "# Loading Live Photos\n\nLoad and cache Live Photos from network sources using Kingfisher.\n\n## Overview\n\nKingfisher provides a seamless way to load Live Photos, which consist of a still image and a video, from network sources. This guide will walk you through the process of utilizing Kingfisher's Live Photo support.\n\n## Live Photo Data Preparation\n\nBefore loading a Live Photo with Kingfisher, you need to prepare and host the data. Kingfisher can download and cache the live photo data from the network (usually your server or a CDN). This section demonstrates how to get the necessary data from a `PHAsset`.\n\nIf you've already set up the data and prepared the necessary URLs for the live photo components, you can skip to the next section to learn how to load it.\n\nAssuming you have a valid `PHAsset` from the Photos framework, here's a sample of how to extract its data:\n\n```swift\nlet asset: PHAsset = // ... your PHAsset\nif !asset.mediaSubtypes.contains(.photoLive) {\n    print(\"Not a live photo\")\n    return\n}\n\nlet resources = PHAssetResource.assetResources(for: asset)\nvar allData = [Data]()\n\nlet group = DispatchGroup()\ngroup.notify(queue: .main) {\n    allData.forEach { data in\n        // Upload data to your server\n        serverRequest.upload(data)\n    }\n}\n\nresources.forEach { resource in\n    group.enter()\n    var data = Data()\n    PHAssetResourceManager.default().requestData(for: resource, options: nil) { chunk in\n        data.append(chunk)\n    } completionHandler: { error in\n        defer { group.leave() }\n        if let error = error {\n            print(\"Error: \\(error)\")\n            return\n        }\n        allData.append(data)\n    }\n}\n```\n\nImportant notes:\n- This is a basic example showing how to retrieve data from a live photo asset.\n- Use [`PHAssetResource.type`]((https://developer.apple.com/documentation/photokit/phassetresource/1623987-type)) to get more information about each live photo resource. Typically, resources with `.photo` and `.pairedVideo` types are necessary for a minimal Live Photo.\n- Do not modify the metadata or actual data of the resources, as this may cause problems when loading in Kingfisher later.\n- When serving the files, it's recommended to include the file extensions (`.heic` for the still image, and `.mov` for the video) in the URL. While not mandatory, this helps Kingfisher identify the file type more accurately.\n- You can use [`PHAssetResource.originalFilename`](https://developer.apple.com/documentation/photokit/phassetresource/1623985-originalfilename) to get and preserve the original file extension.\n\n\n## Loading Live Photos\n\n### Step 1: Import Required Frameworks and Set Up PHLivePhotoView\n\n```swift\nimport Kingfisher\nimport PhotosUI\n\nlet livePhotoView = PHLivePhotoView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))\nview.addSubview(livePhotoView)\n```\n\n### Step 2: Prepare URLs\n\n```swift\nlet imageURL = URL(string: \"https://example.com/image.heic\")!\nlet videoURL = URL(string: \"https://example.com/video.mov\")!\nlet urls = [imageURL, videoURL]\n```\n\n### Step 3: Load the Live Photo\n\n```swift\nlivePhotoView.kf.setImage(with: urls) { result in\n    switch result {\n    case .success(let retrieveResult):\n        print(\"Live photo loaded: \\(retrieveResult.livePhoto)\")\n        print(\"Cache type: \\(retrieveResult.loadingInfo.cacheType)\")\n    case .failure(let error):\n        print(\"Error: \\(error)\")\n    }\n}\n```\n\nThe loaded live photo will be stored in the disk cache of Kingfisher to boost future loading requests. \n\n## Notes\n\n- Verify that the provided URLs are valid and accessible.\n- Loading may take time, especially for resources fetched over the network.\n- Certain `KingfisherOptionsInfo` options, such as custom processors, are not supported for Live Photos.\n- To load a Live Photo, its data must be cached on disk at least during the loading process. If you prefer not to retain the Live Photo data on disk, you can set a short disk cache expiration using options like `.diskCacheExpiration(.seconds(10))`, or manually clear the disk cache regularly after using.\n\n## Conclusion\n\nBy following these steps, you can efficiently load and cache Live Photos in your iOS applications using Kingfisher, enhancing the user experience with smooth integration of this dynamic content type."
  },
  {
    "path": "Sources/Documentation.docc/Topics/Topic_LowDataMode.md",
    "content": "# Low Data Mode\n\nLoading image and customizing behaviors for the Low Data Mode.\n\n## Overview\n\nStarting with iOS 13, Apple has introduced the option for users to enable \n [Low Data Mode](https://support.apple.com/en-us/102433) to reduce cellular and Wi-Fi data usage. To accommodate this \nsetting, you can offer an alternative version of your image, typically in lower resolution. Kingfisher will \nautomatically switch to this version when Low Data Mode is activated, helping to conserve data.\n\n```swift\nimageView.kf.setImage(\n    with: highResolutionURL, \n    options: [.lowDataSource(.network(lowResolutionURL)]\n)\n```\n\nIn the scenario described, if the user has not applied any network restrictions, the `highResolutionURL` will be \nutilized for fetching the image. However, if the device is in Low Data Mode and the `highResolutionURL` version is not\nfound in the cache, the `lowResolutionURL` will be selected as the fallback option to save data.\n\nGiven that the `.lowDataSource` option accepts any `Source` parameter, not just a URL, you have the flexibility to pass \nin a local image provider. This approach effectively eliminates the need for a downloading task, allowing for the use \nof locally stored images when operating under Low Data Mode or other restrictive network conditions.\n\n```swift\nimageView.kf.setImage(\n    with: highResolutionURL, \n    options: [\n        .lowDataSource(\n            .provider(LocalFileImageDataProvider(fileURL: localFileURL))\n        )\n    ]\n)\n```\n\n> For more about this topic, check <doc:Topic_ImageDataProvider> and ``ImageDataProvider`` documentation.\n\n> tip: If the `.lowDataSource` option is not specified, the `highResolutionURL` will be used by default, regardless of \n> the Low Data Mode setting on the device.\n"
  },
  {
    "path": "Sources/Documentation.docc/Topics/Topic_PerformanceTips.md",
    "content": "# Performance Tips\n\nSome useful tips for better performance when using Kingfisher.\n\n### Cancelling unnecessary downloading tasks\n\nOnce a download task is initiated, it will continue until completion, even if you set a different URL to the image view.\n\n```swift\nimageView.kf.setImage(with: url1) { result in \n    // `result` is `.failure(.imageSettingError(.notCurrentSourceTask))`\n    // due to another `setImage` below.\n    //\n    // But the download (and cache) is done normally.\n}\n\n// Set again immediately.\nimageView.kf.setImage(with: url2) { result in \n    // `result` is `.success`\n}\n```\n\nEven if the setting for `url1` ends in a `.failure` because it was overridden by `url2`, the download task itself \ncompletes. The downloaded image data is processed and cached accordingly.\n\nThe download and caching of the image at `url1` consume network resources, CPU time, memory, and battery. If there's a \nlikelihood the image from `url1` will be displayed to the user again, these resources are well spent. If you are certain \nthat the image from `url1` is no longer needed, cancelling the download before initiating another one can be a better \nidea:\n\n```swift\nimageView.kf.setImage(with: url1) { result in\n    // `result` is `.failure(.requestError(.taskCancelled))`\n    // Now the download task is cancelled.\n}\n\nimageView.kf.cancelDownloadTask()\nimageView.kf.setImage(with: url2) { result in\n    // `result` is `.success`\n}\n```\n\nThis approach is particularly useful in table views or collection views. When users scroll through the list quickly, \nmany image downloading tasks may be initiated. To optimize performance, you can cancel unnecessary tasks using the \n`didEndDisplaying` delegate method.\n\n```swift\nfunc collectionView(\n    _ collectionView: UICollectionView,\n    didEndDisplaying cell: UICollectionViewCell,\n    forItemAt indexPath: IndexPath)\n{\n    // This will cancel the unfinished downloading task when the cell disappearing.\n    cell.imageView.kf.cancelDownloadTask()\n}\n```\n\n### Cache original image when using a processor\n\nIf your goal is to either:\n\n1. Use different processors on the same image to obtain various versions.\n2. Apply a non-default processor to an image and later display the original.\n\nConsider using the ``KingfisherOptionsInfoItem/cacheOriginalImage`` option. This option not only caches the processed \nimage but also stores the original downloaded image in the cache.\n\n```swift\nlet p1 = MyProcessor()\nimageView.kf.setImage(with: url, options: [.processor(p1), .cacheOriginalImage])\n```\n\nBoth the image processed by `p1` and the original downloaded image are cached. Later, when processing with another \nprocessor:\n\n```swift\nlet p2 = AnotherProcessor()\nimageView.kf.setImage(with: url, options: [.processor(p2)])\n```\n\nKingfisher is clear enough to verify that the original image for the URL is cached. Instead of downloading the image \nagain, Kingfisher will reuse the original image and apply `p2` to it directly.\n\n### Downsampling the excessively high resolution images\n\nIn scenarios where you need to display large images in a table view or collection view cell, it's optimal to use \nsmaller thumbnails to decrease download times and memory usage. However, if your server doesn't provide thumbnails, \nthe ``DownsamplingImageProcessor`` comes to the rescue. It downsamples high-resolution images to a specified size\nbefore they're loaded into memory, effectively optimizing performance:\n\n```swift\nimageView.kf.setImage(\n    with: resource,\n    placeholder: placeholderImage,\n    options: [\n        .processor(DownsamplingImageProcessor(size: imageView.size)),\n        .scaleFactor(UIScreen.main.scale),\n        .cacheOriginalImage\n    ])\n```\n\n``DownsamplingImageProcessor`` is commonly used alongside ``KingfisherOptionsInfoItem/scaleFactor(_:)`` and \n``KingfisherOptionsInfoItem/cacheOriginalImage`` options. This combination ensures images are scaled appropriately for \nyour UI's pixel density while also caching the original high-resolution image to avoid future downloads, providing an \nefficient balance between image quality and resource utilization.\n"
  },
  {
    "path": "Sources/Documentation.docc/Topics/Topic_Prefetch.md",
    "content": "# Prefetching images before actually loading  \n\nPreloading images before actually required. Feeding them to the table view or collection view to improve the display speed.\n\n## Overview\n\nUse ``ImagePrefetcher`` to prefetch and cache images that are likely to be displayed later. This improves loading times \nand ensures smoother image display.\n\n### Prefetch some images\n\n```swift\nlet urls = [\n    \"https://example.com/image1.jpg\", \n    \"https://example.com/image2.jpg\"\n].map { URL(string: $0)! }\n\nlet prefetcher = ImagePrefetcher(urls: urls) {\n    skippedResources, failedResources, completedResources in\n    print(\"These resources are prefetched: \\(completedResources)\")\n}\nprefetcher.start()\n\n// Later when you need to display these images:\nimageView.kf.setImage(with: urls[0])\nanotherImageView.kf.setImage(with: urls[1])\n```\n\n### Prefetch images for table view or collection view\n\nStarting with iOS 10, Apple introduced cell prefetching behavior, which can seamlessly integrate with Kingfisher's \n``ImagePrefetcher``.\n\n```swift\noverride func viewDidLoad() {\n    super.viewDidLoad()\n    collectionView?.prefetchDataSource = self\n}\n\nextension ViewController: UICollectionViewDataSourcePrefetching {\n    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {\n        let urls = indexPaths.flatMap { URL(string: $0.urlString) }\n        ImagePrefetcher(urls: urls).start()\n    }\n}\n```\n\nSee [WWDC 16 - Session 219](https://developer.apple.com/videos/play/wwdc2016/219/) for more about changing of it in iOS 10.\n"
  },
  {
    "path": "Sources/Documentation.docc/Topics/Topic_Retry.md",
    "content": "# Retry the Image Loading\n\nManaging the retry mechanism when an error happens during loading.\n\n## Overview\n\nUse ``KingfisherOptionsInfoItem/retryStrategy(_:)`` along with a `RetryStrategy` implementation to easily set up a\nretry mechanism for image setting operations when an error occurs.\n\nThis combination allows you to define retry logic, including the number of retries and the conditions under which a\nretry should be attempted, ensuring a more resilient image loading process.\n\n\n## Built-in Retry Strategies\n\nKingfisher provides two built-in retry strategies to handle different scenarios:\n\n### DelayRetryStrategy\n\n``DelayRetryStrategy`` is a time-based retry strategy that allows you to specify the `maxRetryCount` and\nthe `retryInterval` to easily configure retry behavior. This setup enables quick implementation of a retry mechanism:\n\n```swift\nlet retry = DelayRetryStrategy(\n  maxRetryCount: 5,\n  retryInterval: .seconds(3)\n)\nimageView.kf.setImage(with: url, options: [.retryStrategy(retry)])\n```\n\nThis implements a retry mechanism that attempts to reload the target URL up to 5 times, with a fixed 3-second interval\nbetween each try.\n\n#### Other retry interval\n\nFor a more dynamic approach, you can also select `.accumulated(3)` as the retry interval results in progressively\nincreasing delays between attempts, specifically `3 -> 6 -> 9 -> 12 -> 15` seconds for each subsequent retry.\nAdditionally, for ultimate flexibility, `.custom` allows you to define a unique pattern for retry intervals, tailoring\nthe retry logic to your specific requirements.\n\n### NetworkRetryStrategy\n\n``NetworkRetryStrategy`` is a network-aware retry strategy that handles network connectivity issues.\nIt only retries when the network becomes available after a disconnection, this is suitable to handle unstable user connection.\n\n```swift\n// Basic usage - retries immediately when network becomes available\nlet networkRetry = NetworkRetryStrategy()\nimageView.kf.setImage(with: url, options: [.retryStrategy(networkRetry)])\n\n// With timeout - stops waiting after specified duration\nlet networkRetryWithTimeout = NetworkRetryStrategy(timeoutInterval: 30.0)\nimageView.kf.setImage(with: url, options: [.retryStrategy(networkRetryWithTimeout)])\n```\n\n## Custom Retry Strategies\n\nIf you need more control for the retry strategy, implement your own type that conforms to ``RetryStrategy``.\n"
  },
  {
    "path": "Sources/Documentation.docc/Tutorials/GettingStartedSwiftUI.tutorial",
    "content": "@Tutorial(time: 10) {\n    @Intro(title: \"Getting Started with Kingfisher (SwiftUI)\") {\n        Install Kingfisher and learn basic usage of the framework with SwiftUI.\n        @Image(source: getting-started-card, alt: \"Title image of the tutorial. A kingfisher bird standing on a tree.\")    \n    }\n    \n    @Section(title: \"Overview\") {\n        @ContentAndMedia {\n            This tutorial guides you through building a SwiftUI `List` that displays rounded images of kingfisher birds, \n            downloaded using the Kingfisher library. It includes:\n\n            - Setting Up `List`: Quick setup for a basic list.\n            - Using Kingfisher: Download and display bird images.\n            - Image Processing: Convert images to rounded corners for display.\n            - Cache Size Button: A feature to check cache usage.\n            \n            At the final stage of this tutorial, you will have a list like this:\n            \n            @Image(source:preview-3-swiftui.png, alt:\"The first image is loaded into the image view in cell.\")\n        }\n        @Steps { }\n    }\n    \n    @Section(title: \"Installing\") {\n        @ContentAndMedia {\n            After creating your SwiftUI app, the first step is to install Kingfisher. For this, we use Swift Package Manager. \n            \n            > There are also other way to add Kingfisher to your project, such as CocoaPods or manually. Check the related documentation for more information.\n            \n            @Image(source: create-project-swiftui.png, alt: \"\")\n            \n        }\n        \n        @Steps {\n            @Step {\n                Choose \"File\" → \"Add Package Dependencies…\". In the pop-up window, enter the URL below to the search \n                bar, and click the \"Add Package\" button. \n                \n                `https://github.com/onevcat/Kingfisher.git` \n                \n                @Image(source: add-dependency.png, alt: \"Add Kingfisher as the dependency of your project.\")\n            }\n            \n            @Step {\n                After downloading, add the `Kingfisher` library to your created project.\n                @Image(source: add-to-project.png, alt: \"\")\n            }\n            \n            @Step {\n                Select your app target in the \"project and target list\", switch to the \"Build Phases\" tab, expand the \"Link Binary With Libraries\" section, and confirm that \"Kingfisher\" is added. If not, click the \"+\" button and add it to the list.\n                @Image(source: add-library-swiftui.png, alt: \"\")\n            }\n            \n            @Step {\n                To verify the installation. Choose \"ContentView.swift\" file.\n                @Code(name: \"ContentView.swift\", file: 02-ContentView-1.swift)\n            }\n            \n            @Step {\n                Import `Kingfisher`. And try to print the `KingfisherManager.shared` in the `onAppear`. If you see \n                something like \"Kingfisher.KingfisherManager\" in the Xcode debugger console, it means Kingfisher is \n                ready in your project.\n                @Code(name: \"ContentView.swift\", file: 02-ContentView-2.swift)\n            }\n        }\n    }\n    \n    @Section(title: \"Loading image with Kingfisher\") {\n        @ContentAndMedia {\n            In this section, we will create a `List` and use Kingfisher to load some images from the network.\n        }\n        \n        @Steps {\n            @Step {\n                Setting up a `List` in SwiftUI is easy. With the `ContentView` from the SwiftUI template.\n                @Code(name: \"ContentView.swift\", file: 02-ContentView-2.swift)\n            }\n            @Step {\n                Replace the `body` of the `ContentView` with a `List` and the embedded `ForEach`.\n                @Code(name: \"ContentView.swift\", file: 02-ContentView-3.swift) {\n                    @Image(source: preview-1-swiftui.png, alt: \"\")\n                }\n            }\n            @Step {\n                To load an image from network, the easiest way is using the `KFImage` struct provided in Kingfisher. It\n                accepts a URL, loads and shows the image when its `onAppear` is called. `KFImage` has a set of similar\n                APIs to SwiftUI's `Image` type. Here we call `resizable()` to allow the image fit into the given frame.\n                \n                @Code(name: \"ContentView.swift\", file: 02-ContentView-4.swift) {\n                    @Image(source: preview-2-swiftui.png, alt: \"\")\n                }\n            }\n            @Step {\n                Kingfisher also comes with a bundle of useful processors and helper methods. For example, we can add \n                some partial round corner effect in a simple way. \n                @Code(name: \"ContentView.swift\", file: 02-ContentView-5.swift) {\n                    @Image(source:preview-3-swiftui.png, alt:\"\")\n                }\n                Besides of the `.roundCorner`, we also apply a `.serialize(as: .PNG)` to forcibly convert the\n                loaded JPG file to PNG format when storing in the disk cache. This is necessary since JPG format does\n                not contain an alpha channel, which is necessary when storing a round corner image.\n            }\n            @Step {\n                The `KFImage` has a few other modifiers too, including some life cycle handlers like \n                `.onSuccess` or `.onFailure`.\n                @Code(name: \"ContentView.swift\", file: 02-ContentView-6.swift)\n                Restart the app. If the images were loaded during your previous use of the app, you should see \n                \"Image loaded from cache: disk\" in the Xcode console.\n            }\n        }\n    }\n        \n    @Section(title: \"Manipulating the Cache\") {\n        @ContentAndMedia {\n            In this final part, we'll look at basic tasks related to image caching, like finding out the size of the \n            disk cache and clearing all the cache. Usually, Kingfisher handles cache management automatically, so you \n            don't need to think about it much. But if you need more detailed control over how caching works, this \n            section will give you helpful tips and information.\n        }\n        \n        @Steps {\n            @Step {\n                First, we need to find out how much space the image cache is using. Normally, you would check the cache \n                size or clear the cache using a button. In this example, we will add a button to the first row of the \n                list.\n                @Code(name: \"ContentView.swift\", file: 02-ContentView-7.swift)\n            }\n            @Step {\n                Add a `Button` at the top of the `List`. In its event block, call `calculateDiskStorageSize(completion:)` \n                and print the disk cache size. \n                @Code(name: \"ContentView.swift\", file: 02-ContentView-8.swift) {\n                    @Image(source:preview-4-swiftui.png, alt:\"Added a button to the top of the list.\")\n                }\n            }\n            @Step {\n                To trigger an alert in SwiftUI, we add two `@State` to the `ContentView`. In the `calculateDiskStorageSize`\n                handler, we set both states.\n                @Code(name: \"ContentView.swift\", file: 02-ContentView-9.swift)\n            }\n            @Step {\n                Add an `.alert` modifier to the `Button`. When the `showAlert` state is set, an alert with the information\n                of disk cache size is presented. \n                @Code(name: \"ContentView.swift\", file: 02-ContentView-10.swift)\n            }\n            @Step {\n                Lastly, use the `clearCache` function to remove all images from both the memory and disk caches. After \n                this, when you restart the app or trigger a full reloading for the `List`, the images will be downloaded \n                again from the internet. You'll notice \"Image loaded from cache: none\" displayed in the console, \n                indicating the images are not being loaded from the cache this time.\n                \n                @Code(name: \"ContentView.swift\", file: 02-ContentView-11.swift) {\n                    @Image(source:preview-5-swiftui.png, alt:\"An alert which shows the disk cache size used by Kingfisher, with a button to purge the cache.\")\n                }\n                \n                The cache cleaning is only for demonstration purpose. In practice, usually you do not need to call it \n                yourself. Kingfisher will manage and purge the data based on its default policy.\n            }\n        }\n    }\n    \n    @Section(title: \"Next Steps\") {\n        @ContentAndMedia {\n            \n            Congratulations! \n            \n            You have now mastered some basic uses of Kingfisher: including loading images from the web or cache using\n            the `KFImage` type, processing images before display using a modifier, and basic methods\n            for inspecting and clearing the cache. \n            \n            Kingfisher also contains a considerable number of other features, and it has been designed to be simple to\n            use while considering flexibility. As you deepen your understanding of the framework, we hope you will\n            gradually grow to like it.\n            \n            Next, we recommend that you start using Kingfisher in your projects to help you accomplish tasks. You can \n            also read the <doc:CommonTasks> and its related articles to get a better understanding. When \n            you encounter problems, come back to consult the documentation or ask the community. \n            \n            Have a nice day!\n        }\n        @Steps { }\n    }\n    \n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Tutorials/GettingStartedUIKit.tutorial",
    "content": "@Tutorial(time: 15) {\n    @Intro(title: \"Getting Started with Kingfisher (UIKit)\") {\n        Install Kingfisher and learn basic usage of the framework with UIKit.\n        @Image(source: \"getting-started-card\", alt: \"Title image of the tutorial. A kingfisher bird standing on a tree.\")\n    }\n    \n    @Section(title: \"Overview\") {\n        @ContentAndMedia {\n            This tutorial guides you through building a UITableView list that displays rounded images of kingfisher \n            \n            birds, downloaded using the Kingfisher library. It includes:\n\n            - Setting Up `UITableView`: Quick setup for a basic list.\n            - Using Kingfisher: Download and display bird images.\n            - Image Processing: Convert images to rounded corners for display.\n            - Cache Size Button: A feature to check cache usage.\n            \n            At the final stage of this tutorial, you will have a list like this:\n            \n            @Image(source:preview-4.png, alt:\"The first image is loaded into the image view in cell.\")\n        }\n        \n        @Steps { }\n    }\n    \n    @Section(title: \"Installing\") {\n        @ContentAndMedia {\n            After creating your UIKit app, the first step is to install Kingfisher. For this, we use Swift Package Manager. \n            \n            > There are also other way to add Kingfisher to your project, such as CocoaPods or manually. Check the related documentation for more information.\n            \n            @Image(source: create-project.png, alt: \"\")\n            \n        }\n        \n        @Steps {\n            @Step {\n                Choose \"File\" → \"Add Package Dependencies…\". In the pop-up window, enter the URL below to the search \n                bar, and click the \"Add Package\" button. \n                \n                `https://github.com/onevcat/Kingfisher.git` \n                \n                @Image(source: add-dependency.png, alt: \"Add Kingfisher as the dependency of your project.\")\n            }\n            \n            @Step {\n                After downloading, add the `Kingfisher` library to your created project.\n                @Image(source: add-to-project.png, alt: \"\")\n            }\n            \n            @Step {\n                Select your app target in the \"project and target list\", switch to the \"Build Phases\" tab, expand the \"Link Binary With Libraries\" section, and confirm that \"Kingfisher\" is added. If not, click the \"+\" button and add it to the list.\n                @Image(source: add-library.png, alt: \"\")\n            }\n            \n            @Step {\n                To verify the installation. Choose \"ViewController.swift\" file.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-1.swift)\n            }\n            \n            @Step {\n                Import `Kingfisher`. And try to print the `KingfisherManager.shared`. If you see something like \n                \"Kingfisher.KingfisherManager\" in the Xcode debugger console, it means Kingfisher is ready in your \n                project.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-2.swift)\n            }\n        }\n    }\n    \n    @Section(title: \"Creating the Table View\") {\n        @ContentAndMedia {\n            Creating and setting up `UITableView` is not the focus of this tutorial, as it does not involve Kingfisher. \n            However, we will later use Kingfisher to manage images in the `UIImageView` within the table cells. \n            @Image(source: preview-1.png, alt: \"\")\n        }\n        \n        @Steps {\n            @Step {\n                Create a `SampleCell` file. We will use it as the cell type of the table view.\n                @Code(name: \"SampleCell.swift\", file: 01-SampleCell-1.swift)\n            }\n            @Step {\n                Add a `sampleImageView` to the class. It is the main target image view we are going to set later.\n                @Code(name: \"SampleCell.swift\", file: 01-SampleCell-2.swift)\n            }\n            @Step {\n                Add other necessary views and layout code. (Boring, just copy it!)\n                @Code(name: \"SampleCell.swift\", file: 01-SampleCell-3.swift)\n            }\n            @Step {\n                In the \"ViewController.swift\".\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-3.swift)\n            }\n            @Step {\n                Add a `tableView` to the view controller.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-4.swift)\n            }\n            @Step {\n                Extend `ViewController` to conform the `UITableViewDataSource`. For the sake of simplicity, we will \n                only return one cell at first.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-5.swift) {\n                    @Image(source:preview-1.png, alt:\"An iOS app with a list which contains a single cell.\")\n                }\n                Run the app, now you should see a list which contains a single cell with a light grey placeholder and a\n                text label.\n                \n            }\n        }\n    }\n        \n    @Section(title: \"Loading image with Kingfisher\") {\n        @ContentAndMedia {\n            Kingfisher simplifies the task of loading images from remote URLs. It also offers a range of user-friendly \n            processors and helper methods. In this section, we will cover how to use these features to streamline common \n            tasks.\n            @Image(source: preview-4.png, alt: \"\")\n        }\n        \n        @Steps {\n            @Step {\n                The simplest way to start loading a remote image into an image view in Kingfisher, is using the `kf`\n                wrapper and its method. In the code above, we already have a `sampleImageView` in the cell.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-6-0.swift)\n            }\n            @Step {\n                To load the first image, call `kf.setImage(with:)` on the image view, with the desired URL.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-6.swift) {\n                    @Image(source:preview-2.png, alt:\"The first image is loaded into the image view in cell.\")\n                }\n                Now, running the app again, you can see the image is already loaded and set to the image view.\n            }\n            @Step {\n                To actually load the images based on the index, we can try to add more cells. We prepared 10 kingfisher\n                images, let's change the item count to 10: \n                @Code(name: \"ViewController.swift\", file: 01-ViewController-7.swift) {\n                    @Image(source:preview-3.png, alt:\"The first image is loaded into the image view in cell.\")\n                }\n                Kingfisher also downloads and caches these images. Now, even if you turn off the network of\n                your iOS device (or the simulator), and restart the app, these images can be loaded from cache and \n                still displayed.\n            }\n            @Step {\n                Kingfisher also comes with a bundle of useful processors and helper methods. For example, we can add a\n                loading indicator and some partial round corner effect easily. \n                @Code(name: \"ViewController.swift\", file: 01-ViewController-8.swift) {\n                    @Image(source:preview-4.png, alt:\"The first image is loaded into the image view in cell.\")\n                }\n                Besides of the `RoundCornerImageProcessor`, we also apply a `pngSerializer` to forcibly convert the\n                loaded JPG file to PNG format when storing in the disk cache. This is necessary since JPG format does\n                not contain an alpha channel, which is necessary when storing a round corner image.\n            }\n            @Step {\n                The `setImage(with:)` method accepts other parameters, including a completion handler too. Let us add\n                some logs before we continue to the next section.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-9.swift)\n                Now, running the app again, in the console you can see some text like \"Image loaded from cache: disk\".\n                This is because the images are already in the disk cache, and they are loaded from the disk locally. By \n                scrolling the table view up and down and triggering the cell reuse, it should print things like \"Image \n                loaded from cache: memory\", which indicates the images are already cache in memory. \n            }\n        }\n    }\n    \n    @Section(title: \"Manipulating the Cache\") {\n        @ContentAndMedia {\n            In this final part, we'll look at basic tasks related to image caching, like finding out the size of the \n            disk cache and clearing all the cache. Usually, Kingfisher handles cache management automatically, so you \n            don't need to think about it much. But if you need more detailed control over how caching works, this \n            section will give you helpful tips and information.\n        }\n        \n        @Steps {\n            @Step {\n                First, we need to find out how much space the image cache is using. Normally, you would check the cache \n                size or clear the cache using a button. But to keep things simple, we won't add any extra buttons to \n                this example.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-10.swift)\n            }\n            @Step {\n                In the `viewDidLoad` method, we use the `asyncAfter` method on the `DispatchQueue.main` queue. There we \n                start a process to calculate the current size of the disk cache. The size we get tells us how much disk \n                space Kingfisher is using for caching images.\n                \n                @Code(name: \"ViewController.swift\", file: 01-ViewController-11.swift)\n            }\n            @Step {\n                To make it clear, we can create an alert and display it to the user, with a button to clear the cache \n                manually.\n                @Code(name: \"ViewController.swift\", file: 01-ViewController-12.swift) {\n                    @Image(source:preview-5.png, alt:\"An alert which shows the disk cache size used by Kingfisher, with a button to purge the cache.\")\n                }\n            }\n            @Step {\n                Lastly, use the `clearCache` function to remove all images from both the memory and disk caches. After \n                this, when you refresh the table view's data, the images will be downloaded again from the internet. \n                You'll notice \"Image loaded from cache: none\" displayed in the console, indicating the images are not \n                being loaded from the cache this time.\n                \n                @Code(name: \"ViewController.swift\", file: 01-ViewController-13.swift)\n                \n                The cache cleaning is only for demonstration purpose. In practice, usually you do not need to call it yourself. Kingfisher will manage and purge the data based on its default policy.\n            }\n        }\n    }\n    \n    @Section(title: \"Next Steps\") {\n        @ContentAndMedia {\n            \n            Congratulations! \n            \n            You have now mastered some basic uses of Kingfisher: including loading images from the web or cache using\n            the `UIImageView` extension, processing images before display using ``ImageProcessor``, and basic methods\n            for inspecting and clearing the cache. \n            \n            Kingfisher also contains a considerable number of other features, and it has been designed to be simple to\n            use while considering flexibility. As you deepen your understanding of the framework, we hope you will\n            gradually grow to like it. \n            \n            Next, we recommend that you start using Kingfisher in your projects to help you accomplish tasks. You can \n            also read the <doc:CommonTasks> and its related articles to get a better understanding. When \n            you encounter problems, come back to consult the documentation or ask the community. \n            \n            Have a nice day!\n        }\n        @Steps { }\n    }\n}\n"
  },
  {
    "path": "Sources/Documentation.docc/Tutorials/Tutorials.tutorial",
    "content": "@Tutorials(name: \"Kingfisher Tutorials\") {\n    @Intro(title: \"Kingfisher Tutorials\") {\n        Getting started with Kingfisher by following a sample app.\n    }\n    \n    @Chapter(name: \"Getting Started with Kingfisher (UIKit)\") {\n        @Image(source: logo)\n        Installs Kingfisher and basic usage of the framework with UIKit.\n        @TutorialReference(tutorial: \"doc:GettingStartedUIKit\")\n    }\n    \n    @Chapter(name: \"Getting Started with Kingfisher (SwiftUI)\") {\n        @Image(source: logo)\n        Installs Kingfisher and basic usage of the framework with SwiftUI.\n        @TutorialReference(tutorial: \"doc:GettingStartedSwiftUI\")\n    }\n}\n"
  },
  {
    "path": "Sources/Extensions/CPListItem+Kingfisher.swift",
    "content": "\n//\n//  CPListItem+Kingfisher.swift\n//  Kingfisher\n//\n//  Created by Wayne Hartman on 2021-08-29.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(CarPlay) && !targetEnvironment(macCatalyst)\nimport CarPlay\n\n@available(iOS 14.0, *)\n@MainActor\nextension KingfisherWrapper where Base: CPListItem {\n    \n    // MARK: Setting Image\n    \n    /// Sets an image to the image view with a source.\n    ///\n    /// - Parameters:\n    ///   - source: The `Source` object contains information about the image.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.\n    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.\n    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieved and set finished.\n    /// - Returns: A task represents the image downloading.\n    ///\n    /// - Note:\n    ///\n    /// Internally, this method will use `KingfisherManager` to get the requested source\n    /// Since this method will perform UI changes, you must call it from the main thread.\n    /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.\n    ///\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? []))\n        return setImage(\n            with: source,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n    \n    /// Sets an image to the image view with a requested resource.\n    ///\n    /// - Parameters:\n    ///   - resource: The `Resource` object contains information about the image.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.\n    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.\n    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieved and set finished.\n    /// - Returns: A task represents the image downloading.\n    ///\n    /// - Note:\n    ///\n    /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache\n    /// or network. Since this method will perform UI changes, you must call it from the main thread.\n    /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.\n    ///\n    @discardableResult\n    public func setImage(\n        with resource: (any Resource)?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        return setImage(\n            with: resource?.convertToSource(),\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler)\n    }\n\n    func setImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        var mutatingSelf = self\n        return setImage(\n            with: source,\n            imageAccessor: ImagePropertyAccessor(\n                setImage: { image, _ in\n                    /**\n                     * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5\n                     * to allow `nil`. The compiler version 5.4 was introduced in this same SDK,\n                     * which allows >=14.5 SDK to set a `nil` image. This compile check allows\n                     * newer SDK users to set the image to `nil`, while still allowing older SDK\n                     * users to compile the framework.\n                     */\n                    #if compiler(>=5.4)\n                    self.base.setImage(image)\n                    #else\n                    if let image = image {\n                        self.base.setImage(image)\n                    }\n                    #endif\n                },\n                getImage: {\n                    self.base.image\n                }\n            ),\n            taskAccessor: TaskPropertyAccessor(\n                setTaskIdentifier: { mutatingSelf.taskIdentifier = $0 },\n                getTaskIdentifier: { mutatingSelf.taskIdentifier },\n                setTask: { mutatingSelf.imageTask = $0 }\n            ),\n            placeholder: placeholder,\n            parsedOptions: parsedOptions,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n    \n    // MARK: Cancelling Image\n    \n    /// Cancel the image download task bounded to the image view if it is running.\n    /// Nothing will happen if the downloading has already finished.\n    public func cancelDownloadTask() {\n        imageTask?.cancel()\n    }\n}\n\n@MainActor private var taskIdentifierKey: Void?\n@MainActor private var imageTaskKey: Void?\n\n// MARK: Properties\n@MainActor\nextension KingfisherWrapper where Base: CPListItem {\n\n    public private(set) var taskIdentifier: Source.Identifier.Value? {\n        get {\n            let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)\n            return box?.value\n        }\n        set {\n            let box = newValue.map { Box($0) }\n            setRetainedAssociatedObject(base, &taskIdentifierKey, box)\n        }\n    }\n\n    private var imageTask: DownloadTask? {\n        get { return getAssociatedObject(base, &imageTaskKey) }\n        set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Extensions/HasImageComponent+Kingfisher.swift",
    "content": "//\n//  KingfisherHasImageComponent+Kingfisher.swift\n//  Kingfisher\n//\n//  Created by JH on 2023/12/5.\n//\n//  Copyright (c) 2023 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\npublic protocol KingfisherImageSettable: KingfisherCompatible {\n    @MainActor func setImage(\n        _ image: KFCrossPlatformImage?,\n        options: KingfisherParsedOptionsInfo\n    )\n    @MainActor func getImage() -> KFCrossPlatformImage?\n}\n\npublic protocol KingfisherHasImageComponent: KingfisherImageSettable {\n    @MainActor var image: KFCrossPlatformImage? { set get }\n}\n\nextension KingfisherHasImageComponent {\n    @MainActor \n    public func setImage(_ image: KFCrossPlatformImage?, options: KingfisherParsedOptionsInfo) {\n        self.image = image\n    }\n    \n    @MainActor\n    public func getImage() -> KFCrossPlatformImage? {\n        image\n    }\n}\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\n@available(macOS 13.0, *)\nextension NSComboButton: KingfisherHasImageComponent {}\n@available(macOS 13.0, *)\nextension NSColorWell: KingfisherHasImageComponent {}\nextension NSTableViewRowAction: KingfisherHasImageComponent {}\nextension NSMenuItem: KingfisherHasImageComponent {}\nextension NSPathControlItem: KingfisherHasImageComponent {}\nextension NSToolbarItem: KingfisherHasImageComponent {}\nextension NSTabViewItem: KingfisherHasImageComponent {}\nextension NSStatusItem: KingfisherHasImageComponent {}\nextension NSCell: KingfisherHasImageComponent {}\n#endif\n\n#if canImport(UIKit) && !os(watchOS)\nimport UIKit\n@available(iOS 13.0, tvOS 13.0, *)\nextension UIAction: KingfisherHasImageComponent {}\n@available(iOS 13.0, tvOS 13.0, *)\nextension UICommand: KingfisherHasImageComponent {}\nextension UIBarItem: KingfisherHasImageComponent {}\n#endif\n\n#if canImport(WatchKit)\nimport WatchKit\nextension WKInterfaceImage: KingfisherHasImageComponent {\n    @MainActor public var image: KFCrossPlatformImage? {\n        get { nil }\n        set { setImage(newValue) }\n    }\n}\n#endif\n\n#if canImport(TVUIKit)\nimport TVUIKit\nextension TVMonogramView: KingfisherHasImageComponent {}\n#endif\n\nstruct ImagePropertyAccessor<ImageType>: Sendable {\n    let setImage: @Sendable @MainActor (ImageType?, KingfisherParsedOptionsInfo) -> Void\n    let getImage: @Sendable @MainActor () -> ImageType?\n}\n\nstruct TaskPropertyAccessor: Sendable {\n    let setTaskIdentifier: @Sendable @MainActor (Source.Identifier.Value?) -> Void\n    let getTaskIdentifier: @Sendable @MainActor () -> Source.Identifier.Value?\n    let setTask: @Sendable @MainActor (DownloadTask?) -> Void\n}\n\n@MainActor\nextension KingfisherWrapper where Base: KingfisherImageSettable {\n\n    // MARK: Setting Image\n\n    /// Sets an image to the image view with a ``Source``.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object that defines data information from the network or a data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters\n    /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:\n    ///\n    /// ```swift\n    /// // Set image from a network source.\n    /// let url = URL(string: \"https://example.com/image.png\")!\n    /// imageView.kf.setImage(with: .network(url))\n    ///\n    /// // Or set image from a data provider.\n    /// let provider = LocalFileImageDataProvider(fileURL: fileURL)\n    /// imageView.kf.setImage(with: .provider(provider))\n    /// ```\n    ///\n    /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension\n    /// methods. So the code above is equivalent to:\n    ///\n    /// ```swift\n    /// imageView.kf.setImage(with: url)\n    /// imageView.kf.setImage(with: provider)\n    /// ```\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))\n        return setImage(\n            with: source,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets an image to the image view with a ``Source``.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object that defines data information from the network or a data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters\n    /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:\n    ///\n    /// ```swift\n    /// // Set image from a network source.\n    /// let url = URL(string: \"https://example.com/image.png\")!\n    /// imageView.kf.setImage(with: .network(url))\n    ///\n    /// // Or set image from a data provider.\n    /// let provider = LocalFileImageDataProvider(fileURL: fileURL)\n    /// imageView.kf.setImage(with: .provider(provider))\n    /// ```\n    ///\n    /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension\n    /// methods. So the code above is equivalent to:\n    ///\n    /// ```swift\n    /// imageView.kf.setImage(with: url)\n    /// imageView.kf.setImage(with: provider)\n    /// ```\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: source,\n            placeholder: placeholder,\n            options: options,\n            progressBlock: nil,\n            completionHandler: completionHandler\n        )\n    }\n    \n    /// Sets an image to the image view with a requested ``Resource``.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object contains information about the resource.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters\n    /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:\n    ///\n    /// ```swift\n    /// // Set image from a URL resource.\n    /// let url = URL(string: \"https://example.com/image.png\")!\n    /// imageView.kf.setImage(with: url)\n    /// ```\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with resource: (any Resource)?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: resource?.convertToSource(),\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets an image to the image view with a requested ``Resource``.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object contains information about the resource.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters\n    /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:\n    ///\n    /// ```swift\n    /// // Set image from a URL resource.\n    /// let url = URL(string: \"https://example.com/image.png\")!\n    /// imageView.kf.setImage(with: url)\n    /// ```\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with resource: (any Resource)?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: resource,\n            placeholder: placeholder,\n            options: options,\n            progressBlock: nil,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets an image to the image view with a ``ImageDataProvider``.\n    ///\n    /// - Parameters:\n    ///   - provider: The ``ImageDataProvider`` object that defines data information from the data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with provider: (any ImageDataProvider)?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: provider.map { .provider($0) },\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets an image to the image view with a ``ImageDataProvider``.\n    ///\n    /// - Parameters:\n    ///   - provider: The ``ImageDataProvider`` object that defines data information from the data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with provider: (any ImageDataProvider)?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: provider,\n            placeholder: placeholder,\n            options: options,\n            progressBlock: nil,\n            completionHandler: completionHandler\n        )\n    }\n    \n    func setImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask? {\n        return setImage(\n            with: source, \n            imageAccessor: ImagePropertyAccessor(\n                setImage: { base.setImage($0, options: $1) },\n                getImage: { base.getImage() }\n            ),\n            taskAccessor: TaskPropertyAccessor(\n                setTaskIdentifier: {\n                    var mutatingSelf = self\n                    mutatingSelf.taskIdentifier = $0\n                },\n                getTaskIdentifier: { self.taskIdentifier },\n                setTask: { task in\n                    var mutatingSelf = self\n                    mutatingSelf.imageTask = task\n                }\n            ),\n            placeholder: placeholder,\n            parsedOptions: parsedOptions,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n}\n\n@MainActor\nextension KingfisherWrapper {\n    func setImage(\n        with source: Source?,\n        imageAccessor: ImagePropertyAccessor<KFCrossPlatformImage>,\n        taskAccessor: TaskPropertyAccessor,\n        placeholder: KFCrossPlatformImage? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        guard let source = source else {\n            imageAccessor.setImage(placeholder, parsedOptions)\n            taskAccessor.setTaskIdentifier(nil)\n            completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))\n            return nil\n        }\n\n        var options = parsedOptions\n\n        // Always set placeholder while there is no image/placeholder yet.\n#if os(watchOS)\n        let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading\n#else\n        let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading || imageAccessor.getImage() == nil\n#endif\n        if usePlaceholderDuringLoading {\n            imageAccessor.setImage(placeholder, options)\n        }\n\n        let issuedIdentifier = Source.Identifier.next()\n        taskAccessor.setTaskIdentifier(issuedIdentifier)\n\n        if let block = progressBlock {\n            options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]\n        }\n\n        let task = KingfisherManager.shared.retrieveImage(\n            with: source,\n            options: options,\n            downloadTaskUpdated: { task in\n                Task { @MainActor in taskAccessor.setTask(task) }\n            },\n            progressiveImageSetter: { imageAccessor.setImage($0, options) },\n            referenceTaskIdentifierChecker: { issuedIdentifier == taskAccessor.getTaskIdentifier() },\n            completionHandler: { result in\n                CallbackQueueMain.currentOrAsync {\n                    guard issuedIdentifier == taskAccessor.getTaskIdentifier() else {\n                        let reason: KingfisherError.ImageSettingErrorReason\n                        do {\n                            let value = try result.get()\n                            reason = .notCurrentSourceTask(result: value, error: nil, source: source)\n                        } catch {\n                            reason = .notCurrentSourceTask(result: nil, error: error, source: source)\n                        }\n                        let error = KingfisherError.imageSettingError(reason: reason)\n                        completionHandler?(.failure(error))\n                        return\n                    }\n\n                    taskAccessor.setTask(nil)\n                    taskAccessor.setTaskIdentifier(nil)\n\n                    switch result {\n                    case .success(let value):\n                        imageAccessor.setImage(value.image, options)\n                    case .failure:\n                        if let image = options.onFailureImage {\n                            imageAccessor.setImage(image, options)\n                        }\n                    }\n                    completionHandler?(result)\n                }\n            }\n        )\n        taskAccessor.setTask(task)\n        return task\n    }\n}\n\n// MARK: - Associated Object\n@MainActor private var taskIdentifierKey: Void?\n@MainActor private var imageTaskKey: Void?\n\n@MainActor\nextension KingfisherWrapper where Base: KingfisherImageSettable {\n\n    // MARK: Properties\n    public private(set) var taskIdentifier: Source.Identifier.Value? {\n        get {\n            let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)\n            return box?.value\n        }\n        set {\n            let box = newValue.map { Box($0) }\n            setRetainedAssociatedObject(base, &taskIdentifierKey, box)\n        }\n    }\n    \n    private var imageTask: DownloadTask? {\n        get { return getAssociatedObject(base, &imageTaskKey) }\n        set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}\n    }\n    \n    /// Cancels the image download task of the image view if it is running.\n    ///\n    /// Nothing will happen if the downloading has already finished.\n    public func cancelDownloadTask() {\n        imageTask?.cancel()\n    }\n}\n"
  },
  {
    "path": "Sources/Extensions/ImageView+Kingfisher.swift",
    "content": "//\n//  ImageView+Kingfisher.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n@MainActor\nextension KingfisherWrapper where Base: KFCrossPlatformImageView {\n\n    // MARK: Setting Image\n\n    /// Sets an image to the image view with a ``Source``.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object that defines data information from the network or a data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters\n    /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:\n    ///\n    /// ```swift\n    /// // Set image from a network source.\n    /// let url = URL(string: \"https://example.com/image.png\")!\n    /// imageView.kf.setImage(with: .network(url))\n    ///\n    /// // Or set image from a data provider.\n    /// let provider = LocalFileImageDataProvider(fileURL: fileURL)\n    /// imageView.kf.setImage(with: .provider(provider))\n    /// ```\n    ///\n    /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension\n    /// methods. So the code above is equivalent to:\n    ///\n    /// ```swift\n    /// imageView.kf.setImage(with: url)\n    /// imageView.kf.setImage(with: provider)\n    /// ```\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        placeholder: (any Placeholder)? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))\n        return setImage(with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler)\n    }\n\n    /// Sets an image to the image view with a ``Source``.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object that defines data information from the network or a data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters\n    /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:\n    ///\n    /// ```swift\n    /// // Set image from a network source.\n    /// let url = URL(string: \"https://example.com/image.png\")!\n    /// imageView.kf.setImage(with: .network(url))\n    ///\n    /// // Or set image from a data provider.\n    /// let provider = LocalFileImageDataProvider(fileURL: fileURL)\n    /// imageView.kf.setImage(with: .provider(provider))\n    /// ```\n    ///\n    /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension\n    /// methods. So the code above is equivalent to:\n    ///\n    /// ```swift\n    /// imageView.kf.setImage(with: url)\n    /// imageView.kf.setImage(with: provider)\n    /// ```\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        placeholder: (any Placeholder)? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: source,\n            placeholder: placeholder,\n            options: options,\n            progressBlock: nil,\n            completionHandler: completionHandler\n        )\n    }\n    \n    /// Sets an image to the image view with a requested ``Resource``.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object contains information about the resource.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters\n    /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:\n    ///\n    /// ```swift\n    /// // Set image from a URL resource.\n    /// let url = URL(string: \"https://example.com/image.png\")!\n    /// imageView.kf.setImage(with: url)\n    /// ```\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with resource: (any Resource)?,\n        placeholder: (any Placeholder)? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: resource?.convertToSource(),\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler)\n    }\n\n    /// Sets an image to the image view with a requested ``Resource``.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object contains information about the resource.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters\n    /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:\n    ///\n    /// ```swift\n    /// // Set image from a URL resource.\n    /// let url = URL(string: \"https://example.com/image.png\")!\n    /// imageView.kf.setImage(with: url)\n    /// ```\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with resource: (any Resource)?,\n        placeholder: (any Placeholder)? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: resource,\n            placeholder: placeholder,\n            options: options,\n            progressBlock: nil,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets an image to the image view with a ``ImageDataProvider``.\n    ///\n    /// - Parameters:\n    ///   - provider: The ``ImageDataProvider`` object that defines data information from the data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with provider: (any ImageDataProvider)?,\n        placeholder: (any Placeholder)? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: provider.map { .provider($0) },\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler)\n    }\n\n    /// Sets an image to the image view with a ``ImageDataProvider``.\n    ///\n    /// - Parameters:\n    ///   - provider: The ``ImageDataProvider`` object that defines data information from the data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with provider: (any ImageDataProvider)?,\n        placeholder: (any Placeholder)? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: provider,\n            placeholder: placeholder,\n            options: options,\n            progressBlock: nil,\n            completionHandler: completionHandler\n        )\n    }\n\n    func setImage(\n        with source: Source?,\n        placeholder: (any Placeholder)? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        var mutatingSelf = self\n        guard let source = source else {\n            mutatingSelf.placeholder = placeholder\n            mutatingSelf.taskIdentifier = nil\n            completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))\n            return nil\n        }\n\n        var options = parsedOptions\n\n        let isEmptyImage = base.image == nil && self.placeholder == nil\n        if !options.keepCurrentImageWhileLoading || isEmptyImage {\n            // Always set placeholder while there is no image/placeholder yet.\n            mutatingSelf.placeholder = placeholder\n        }\n\n        let maybeIndicator = indicator\n        maybeIndicator?.startAnimatingView()\n\n        let issuedIdentifier = Source.Identifier.next()\n        mutatingSelf.taskIdentifier = issuedIdentifier\n\n        if base.shouldPreloadAllAnimation() {\n            options.preloadAllAnimationData = true\n        }\n\n        if let block = progressBlock {\n            options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]\n        }\n\n        let task = KingfisherManager.shared.retrieveImage(\n            with: source,\n            options: options,\n            downloadTaskUpdated: { task in\n                Task { @MainActor in mutatingSelf.imageTask = task }\n            },\n            progressiveImageSetter: { self.base.image = $0 },\n            referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },\n            completionHandler: { result in\n                CallbackQueueMain.currentOrAsync {\n                    maybeIndicator?.stopAnimatingView()\n                    guard issuedIdentifier == self.taskIdentifier else {\n                        let reason: KingfisherError.ImageSettingErrorReason\n                        do {\n                            let value = try result.get()\n                            reason = .notCurrentSourceTask(result: value, error: nil, source: source)\n                        } catch {\n                            reason = .notCurrentSourceTask(result: nil, error: error, source: source)\n                        }\n                        let error = KingfisherError.imageSettingError(reason: reason)\n                        completionHandler?(.failure(error))\n                        return\n                    }\n\n                    mutatingSelf.imageTask = nil\n                    mutatingSelf.taskIdentifier = nil\n\n                    switch result {\n                    case .success(let value):\n                        guard self.needsTransition(options: options, cacheType: value.cacheType) else {\n                            mutatingSelf.placeholder = nil\n                            self.base.image = value.image\n                            completionHandler?(result)\n                            return\n                        }\n\n                        self.makeTransition(image: value.image, transition: options.transition) {\n                            completionHandler?(result)\n                        }\n\n                    case .failure:\n                        if let image = options.onFailureImage {\n                            mutatingSelf.placeholder = nil\n                            self.base.image = image\n                        }\n                        completionHandler?(result)\n                    }\n                }\n            }\n        )\n        mutatingSelf.imageTask = task\n        return task\n    }\n\n    // MARK: Cancelling Downloading Task\n\n    /// Cancels the image download task of the image view if it is running.\n    ///\n    /// Nothing will happen if the downloading has already finished.\n    public func cancelDownloadTask() {\n        imageTask?.cancel()\n    }\n\n    private func needsTransition(options: KingfisherParsedOptionsInfo, cacheType: CacheType) -> Bool {\n        switch options.transition {\n        case .none:\n            return false\n        #if os(macOS)\n        case .fade: // Fade is only a placeholder for SwiftUI on macOS.\n            return false\n        #else\n        default:\n            if options.forceTransition { return true }\n            if cacheType == .none { return true }\n            return false\n        #endif\n        }\n    }\n\n    private func makeTransition(image: KFCrossPlatformImage, transition: ImageTransition, done: @escaping () -> Void) {\n        #if !os(macOS)\n        // Force hiding the indicator without transition first.\n        UIView.transition(\n            with: self.base,\n            duration: 0.0,\n            options: [],\n            animations: { self.indicator?.stopAnimatingView() },\n            completion: { _ in\n                var mutatingSelf = self\n                mutatingSelf.placeholder = nil\n                UIView.transition(\n                    with: self.base,\n                    duration: transition.duration,\n                    options: [transition.animationOptions, .allowUserInteraction],\n                    animations: { transition.animations?(self.base, image) },\n                    completion: { finished in\n                        transition.completion?(finished)\n                        done()\n                    }\n                )\n            }\n        )\n        #else\n        done()\n        #endif\n    }\n}\n\n// MARK: - Associated Object\n@MainActor private var taskIdentifierKey: Void?\n@MainActor private var indicatorKey: Void?\n@MainActor private var indicatorTypeKey: Void?\n@MainActor private var placeholderKey: Void?\n@MainActor private var imageTaskKey: Void?\n\n@MainActor\nextension KingfisherWrapper where Base: KFCrossPlatformImageView {\n\n    // MARK: Properties\n    public private(set) var taskIdentifier: Source.Identifier.Value? {\n        get {\n            let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)\n            return box?.value\n        }\n        set {\n            let box = newValue.map { Box($0) }\n            setRetainedAssociatedObject(base, &taskIdentifierKey, box)\n        }\n    }\n\n    /// Specifies which indicator type is going to be used.\n    ///\n    /// The default is ``IndicatorType/none``, which means no indicator will be shown while downloading.\n    public var indicatorType: IndicatorType {\n        get {\n            return getAssociatedObject(base, &indicatorTypeKey) ?? .none\n        }\n        \n        set {\n            switch newValue {\n            case .none: indicator = nil\n            case .activity: indicator = ActivityIndicator()\n            case .image(let data): indicator = ImageIndicator(imageData: data)\n            case .custom(let anIndicator): indicator = anIndicator\n            }\n\n            setRetainedAssociatedObject(base, &indicatorTypeKey, newValue)\n        }\n    }\n    \n    /// Holds any type that conforms to the protocol ``Indicator``.\n    ///\n    /// The protocol `Indicator` has a `view` property that will be shown when loading an image.\n    /// It will be `nil` if ``KingfisherWrapper/indicatorType`` is ``IndicatorType/none``.\n    public private(set) var indicator: (any Indicator)? {\n        get {\n            let box: Box<any Indicator>? = getAssociatedObject(base, &indicatorKey)\n            return box?.value\n        }\n        \n        set {\n            // Remove previous\n            if let previousIndicator = indicator {\n                previousIndicator.view.removeFromSuperview()\n            }\n            \n            // Add new\n            if let newIndicator = newValue {\n                // Set default indicator layout\n                let view = newIndicator.view\n                \n                base.addSubview(view)\n                view.translatesAutoresizingMaskIntoConstraints = false\n                view.centerXAnchor.constraint(\n                    equalTo: base.centerXAnchor, constant: newIndicator.centerOffset.x).isActive = true\n                view.centerYAnchor.constraint(\n                    equalTo: base.centerYAnchor, constant: newIndicator.centerOffset.y).isActive = true\n\n                switch newIndicator.sizeStrategy(in: base) {\n                case .intrinsicSize:\n                    break\n                case .full:\n                    view.heightAnchor.constraint(equalTo: base.heightAnchor, constant: 0).isActive = true\n                    view.widthAnchor.constraint(equalTo: base.widthAnchor, constant: 0).isActive = true\n                case .size(let size):\n                    view.heightAnchor.constraint(equalToConstant: size.height).isActive = true\n                    view.widthAnchor.constraint(equalToConstant: size.width).isActive = true\n                }\n                \n                newIndicator.view.isHidden = true\n            }\n\n            // Save in associated object\n            // Wrap newValue with Box to workaround an issue that Swift does not recognize\n            // and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872\n            setRetainedAssociatedObject(base, &indicatorKey, newValue.map(Box.init))\n        }\n    }\n    \n    private var imageTask: DownloadTask? {\n        get { return getAssociatedObject(base, &imageTaskKey) }\n        set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}\n    }\n\n    /// Represents the ``Placeholder`` used for this image view.\n    ///\n    /// A ``Placeholder`` will be shown in the view while it is downloading an image.\n    public private(set) var placeholder: (any Placeholder)? {\n        get { return getAssociatedObject(base, &placeholderKey) }\n        set {\n            if let previousPlaceholder = placeholder {\n                previousPlaceholder.remove(from: base)\n            }\n            \n            if let newPlaceholder = newValue {\n                newPlaceholder.add(to: base)\n            } else {\n                base.image = nil\n            }\n            setRetainedAssociatedObject(base, &placeholderKey, newValue)\n        }\n    }\n}\n\nextension KFCrossPlatformImageView {\n    @objc func shouldPreloadAllAnimation() -> Bool { return true }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/Extensions/NSButton+Kingfisher.swift",
    "content": "//\n//  NSButton+Kingfisher.swift\n//  Kingfisher\n//\n//  Created by Jie Zhang on 14/04/2016.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\n\nimport AppKit\n\n@MainActor\nextension KingfisherWrapper where Base: NSButton {\n\n    // MARK: Setting Image\n\n    /// Sets an image to the button with a ``Source``.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object that defines data information from the network or a data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))\n        return setImage(\n            with: source,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets an image to the button with a ``Resource``.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object that defines data information from the network or a data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setImage(\n        with resource: (any Resource)?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setImage(\n            with: resource?.convertToSource(),\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler)\n    }\n\n    func setImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        var mutatingSelf = self\n        return setImage(\n            with: source,\n            imageAccessor: ImagePropertyAccessor(\n                setImage: { image, _ in\n                    base.image = image\n                }, getImage: {\n                    base.image\n                }),\n            taskAccessor: TaskPropertyAccessor(\n                setTaskIdentifier: { mutatingSelf.taskIdentifier = $0 },\n                getTaskIdentifier: { mutatingSelf.taskIdentifier }, \n                setTask: { mutatingSelf.imageTask = $0 }),\n            placeholder: placeholder,\n            parsedOptions: parsedOptions,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    // MARK: Cancelling Downloading Task\n\n    /// Cancels the image download task of the button if it is running.\n    /// Nothing will happen if the downloading has already finished.\n    public func cancelImageDownloadTask() {\n        imageTask?.cancel()\n    }\n\n    // MARK: Setting Alternate Image\n\n    @discardableResult\n    public func setAlternateImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))\n        return setAlternateImage(\n            with: source,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets an alternate image to the button with a ``Resource``.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object that defines data information from the network or a data provider.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI\n    ///  changes, it is your responsibility to call it from the main thread.\n    ///\n    /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread.\n    @discardableResult\n    public func setAlternateImage(\n        with resource: (any Resource)?,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        return setAlternateImage(\n            with: resource?.convertToSource(),\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler)\n    }\n\n    func setAlternateImage(\n        with source: Source?,\n        placeholder: KFCrossPlatformImage? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        var mutatingSelf = self\n        return setImage(\n            with: source,\n            imageAccessor: ImagePropertyAccessor(\n                setImage: { image, _ in\n                    base.alternateImage = image\n                }, getImage: {\n                    base.alternateImage\n                }),\n            taskAccessor: TaskPropertyAccessor(\n                setTaskIdentifier: { mutatingSelf.alternateTaskIdentifier = $0 },\n                getTaskIdentifier: { mutatingSelf.alternateTaskIdentifier },\n                setTask: { mutatingSelf.alternateImageTask = $0 }\n            ),\n            placeholder: placeholder,\n            parsedOptions: parsedOptions,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    // MARK: Cancelling Alternate Image Downloading Task\n\n    /// Cancels the image download task of the image view if it is running.\n    ///\n    /// Nothing will happen if the downloading has already finished.\n    public func cancelAlternateImageDownloadTask() {\n        alternateImageTask?.cancel()\n    }\n}\n\n\n// MARK: - Associated Object\n@MainActor private var taskIdentifierKey: Void?\n@MainActor private var imageTaskKey: Void?\n\n@MainActor private var alternateTaskIdentifierKey: Void?\n@MainActor private var alternateImageTaskKey: Void?\n\n@MainActor\nextension KingfisherWrapper where Base: NSButton {\n\n    // MARK: Properties\n    \n    public private(set) var taskIdentifier: Source.Identifier.Value? {\n        get {\n            let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)\n            return box?.value\n        }\n        set {\n            let box = newValue.map { Box($0) }\n            setRetainedAssociatedObject(base, &taskIdentifierKey, box)\n        }\n    }\n    \n    private var imageTask: DownloadTask? {\n        get { return getAssociatedObject(base, &imageTaskKey) }\n        set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}\n    }\n\n    public private(set) var alternateTaskIdentifier: Source.Identifier.Value? {\n        get {\n            let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &alternateTaskIdentifierKey)\n            return box?.value\n        }\n        set {\n            let box = newValue.map { Box($0) }\n            setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box)\n        }\n    }\n\n    private var alternateImageTask: DownloadTask? {\n        get { return getAssociatedObject(base, &alternateImageTaskKey) }\n        set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)}\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Extensions/NSTextAttachment+Kingfisher.swift",
    "content": "//\n//  NSTextAttachment+Kingfisher.swift\n//  Kingfisher\n//\n//  Created by Benjamin Briggs on 22/07/2019.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n@MainActor\nextension KingfisherWrapper where Base: NSTextAttachment {\n\n    // MARK: Setting Image\n\n    /// Sets an image to the text attachment with a source.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object that defines data information from the network or a data provider.\n    ///   - attributedView: The owner of the attributed string to which this `NSTextAttachment` is added.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `source`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the requested source. Since this method will\n    /// perform UI changes, it is your responsibility of calling it from the main thread.\n    ///\n    /// The retrieved image will be set to the `NSTextAttachment.image` property. Because it is not an image view-based\n    /// rendering, options related to the view, such as ``KingfisherOptionsInfoItem/transition(_:)``, are not supported.\n    ///\n    /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task is done. It gives the view a\n    /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an\n    /// attributed string with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter.\n    ///\n    /// Here is a typical use case:\n    ///\n    /// ```swift\n    /// let label: UILabel = // ...\n    ///\n    /// let textAttachment = NSTextAttachment()\n    /// textAttachment.kf.setImage(\n    ///     with: URL(string: \"https://onevcat.com/assets/images/avatar.jpg\")!,\n    ///     attributedView: label,\n    ///     options: [\n    ///        .processor(\n    ///            ResizingImageProcessor(referenceSize: .init(width: 30, height: 30))\n    ///            |> RoundCornerImageProcessor(cornerRadius: 15))\n    ///     ]\n    /// )\n    ///\n    /// let attributedText = NSMutableAttributedString(string: \"Hello World\")\n    /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment))\n    /// label.attributedText = attributedText\n    /// ```\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))\n        return setImage(\n            with: source,\n            attributedView: attributedView,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets an image to the text attachment with a source.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object that defines data information from the network or a data provider.\n    ///   - attributedView: The owner of the attributed string to which this `NSTextAttachment` is added.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.\n    ///   - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - progressBlock: Called when the image downloading progress is updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieval and setting are finished.\n    /// - Returns: A task that represents the image downloading.\n    ///\n    /// Internally, this method will use ``KingfisherManager`` to get the requested source. Since this method will\n    /// perform UI changes, it is your responsibility of calling it from the main thread.\n    ///\n    /// The retrieved image will be set to the `NSTextAttachment.image` property. Because it is not an image view-based\n    /// rendering, options related to the view, such as ``KingfisherOptionsInfoItem/transition(_:)``, are not supported.\n    ///\n    /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task is done. It gives the view a\n    /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an\n    /// attributed string with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter.\n    ///\n    /// Here is a typical use case:\n    ///\n    /// ```swift\n    /// let label: UILabel = // ...\n    ///\n    /// let textAttachment = NSTextAttachment()\n    /// textAttachment.kf.setImage(\n    ///     with: URL(string: \"https://onevcat.com/assets/images/avatar.jpg\")!,\n    ///     attributedView: label,\n    ///     options: [\n    ///        .processor(\n    ///            ResizingImageProcessor(referenceSize: .init(width: 30, height: 30))\n    ///            |> RoundCornerImageProcessor(cornerRadius: 15))\n    ///     ]\n    /// )\n    ///\n    /// let attributedText = NSMutableAttributedString(string: \"Hello World\")\n    /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment))\n    /// label.attributedText = attributedText\n    /// ```\n    @discardableResult\n    public func setImage(\n        with resource: (any Resource)?,\n        attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView,\n        placeholder: KFCrossPlatformImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))\n        return setImage(\n            with: resource.map { .network($0) },\n            attributedView: attributedView,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    func setImage(\n        with source: Source?,\n        attributedView: @escaping @Sendable () -> KFCrossPlatformView,\n        placeholder: KFCrossPlatformImage? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask?\n    {\n        var mutatingSelf = self\n        guard let source = source else {\n            base.image = placeholder\n            mutatingSelf.taskIdentifier = nil\n            completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))\n            return nil\n        }\n\n        var options = parsedOptions\n        if !options.keepCurrentImageWhileLoading {\n            base.image = placeholder\n        }\n\n        let issuedIdentifier = Source.Identifier.next()\n        mutatingSelf.taskIdentifier = issuedIdentifier\n\n        if let block = progressBlock {\n            options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]\n        }\n\n        let task = KingfisherManager.shared.retrieveImage(\n            with: source,\n            options: options,\n            progressiveImageSetter: { self.base.image = $0 },\n            referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },\n            completionHandler: { result in\n                CallbackQueueMain.currentOrAsync {\n                    guard issuedIdentifier == self.taskIdentifier else {\n                        let reason: KingfisherError.ImageSettingErrorReason\n                        do {\n                            let value = try result.get()\n                            reason = .notCurrentSourceTask(result: value, error: nil, source: source)\n                        } catch {\n                            reason = .notCurrentSourceTask(result: nil, error: error, source: source)\n                        }\n                        let error = KingfisherError.imageSettingError(reason: reason)\n                        completionHandler?(.failure(error))\n                        return\n                    }\n\n                    mutatingSelf.imageTask = nil\n                    mutatingSelf.taskIdentifier = nil\n\n                    switch result {\n                    case .success(let value):\n                        self.base.image = value.image\n                        let view = attributedView()\n                        #if canImport(UIKit)\n                        view.setNeedsDisplay()\n                        #else\n                        view.setNeedsDisplay(view.bounds)\n                        #endif\n                    case .failure:\n                        if let image = options.onFailureImage {\n                            self.base.image = image\n                        }\n                    }\n                    completionHandler?(result)\n                }\n        }\n        )\n\n        mutatingSelf.imageTask = task\n        return task\n    }\n\n    // MARK: Cancelling Image\n\n    /// Cancel the image download task bound to the text attachment if it is running.\n    ///\n    /// Nothing will happen if the downloading has already finished.\n    public func cancelDownloadTask() {\n        imageTask?.cancel()\n    }\n}\n\n@MainActor private var taskIdentifierKey: Void?\n@MainActor private var imageTaskKey: Void?\n\n// MARK: Properties\n@MainActor\nextension KingfisherWrapper where Base: NSTextAttachment {\n\n    public private(set) var taskIdentifier: Source.Identifier.Value? {\n        get {\n            let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)\n            return box?.value\n        }\n        set {\n            let box = newValue.map { Box($0) }\n            setRetainedAssociatedObject(base, &taskIdentifierKey, box)\n        }\n    }\n\n    private var imageTask: DownloadTask? {\n        get { return getAssociatedObject(base, &imageTaskKey) }\n        set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/Extensions/PHLivePhotoView+Kingfisher.swift",
    "content": "//\n//  PHLivePhotoView+Kingfisher.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2024/10/04.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if os(watchOS)\n// Only a placeholder.\npublic struct RetrieveLivePhotoResult: @unchecked Sendable {\n}\n#else\n@preconcurrency import PhotosUI\n\n/// A result type that contains the information of a retrieved live photo.\n///\n/// This struct is used to encapsulate the result of a live photo retrieval operation, including the loading information,\n/// the retrieved `PHLivePhoto` object, and any additional information provided by the result handler.\n///\n/// - Note: The `info` dictionary is considered sendable based on the documentation for \"Result Handler Info Dictionary Keys\".\n///         See: [Result Handler Info Dictionary Keys](https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys)\npublic struct RetrieveLivePhotoResult: @unchecked Sendable {\n    /// The loading information of the live photo.\n    public let loadingInfo: LivePhotoLoadingInfoResult\n\n    /// The retrieved live photo object which is given by the \n    /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method from\n    /// the result handler.\n    public let livePhoto: PHLivePhoto?\n    \n\n    // According to \"Result Handler Info Dictionary Keys\", we can trust the `info` in handler is sendable.\n    // https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys\n    /// The additional information provided by the result handler when retrieving the live photo.\n    public let info: [AnyHashable : Any]?\n}\n\n@MainActor private var taskIdentifierKey: Void?\n@MainActor private var targetSizeKey: Void?\n@MainActor private var contentModeKey: Void?\n\n@MainActor\nextension KingfisherWrapper where Base: PHLivePhotoView {\n    /// Gets the task identifier associated with the image view for the live photo task.\n    public private(set) var taskIdentifier: Source.Identifier.Value? {\n        get {\n            let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)\n            return box?.value\n        }\n        set {\n            let box = newValue.map { Box($0) }\n            setRetainedAssociatedObject(base, &taskIdentifierKey, box)\n        }\n    }\n    \n    /// The target size of the live photo view. It is used in the \n    /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as \n    /// the `targetSize` argument when loading the live photo. \n    /// \n    /// If not set, `.zero` will be used.\n    public var targetSize: CGSize {\n        get { getAssociatedObject(base, &targetSizeKey) ?? .zero }\n        set { setRetainedAssociatedObject(base, &targetSizeKey, newValue) }\n    }\n    \n    /// The content mode of the live photo view. It is used in the\n    /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as\n    /// the `contentMode` argument when loading the live photo.\n    /// \n    /// If not set, `.default` will be used.\n    public var contentMode: PHImageContentMode {\n        get { getAssociatedObject(base, &contentModeKey) ?? .default }\n        set { setRetainedAssociatedObject(base, &contentModeKey, newValue) }\n    }\n    \n    /// Sets a live photo to the view with an array of `URL`.\n    ///\n    /// - Parameters:\n    ///   - urls: The `URL`s defining the live photo resource. It should contains two URLs, one for the still image and\n    ///     one for the video.\n    ///   - options: An options set to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - completionHandler: Called when the image setting process finishes.\n    /// - Returns: A task represents the image downloading.\n    ///            The return value will be `nil` if the image is set with a empty source.\n    ///\n    /// - Note: Not all options in ``KingfisherOptionsInfo`` are supported in this method, for example, the live photo\n    /// does not support any custom processors. Different from the extension method for a normal image view on the\n    /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future.\n    /// \n    /// - Note: To get refined control of the resources, use the ``setImage(with:options:completionHandler:)-1n4p2``\n    /// method with a ``LivePhotoSource`` object.\n    ///\n    /// Example:\n    /// \n    /// ```swift\n    /// let urls = [\n    ///   URL(string: \"https://example.com/image.heic\")!, // imageURL\n    ///   URL(string: \"https://example.com/video.mov\")!   // videoURL\n    /// ]\n    /// let livePhotoView = PHLivePhotoView()\n    /// livePhotoView.kf.setImage(with: urls) { result in\n    ///   switch result {\n    ///   case .success(let retrieveResult):\n    ///     print(\"Live photo loaded: \\(retrieveResult.livePhoto).\")\n    ///     print(\"Cache type: \\(retrieveResult.loadingInfo.cacheType).\")\n    ///   case .failure(let error):\n    ///     print(\"Error: \\(error)\")\n    /// }\n    /// ```\n    @discardableResult\n    public func setImage(\n        with urls: [URL],\n        // placeholder: KFCrossPlatformImage? = nil, // Not supported yet\n        options: KingfisherOptionsInfo? = nil,\n        // progressBlock: DownloadProgressBlock? = nil, // Not supported yet\n        completionHandler: (@MainActor @Sendable (Result<RetrieveLivePhotoResult, KingfisherError>) -> Void)? = nil\n    ) -> Task<(), Never>? {\n        setImage(\n            with: LivePhotoSource(urls: urls),\n            options: options,\n            completionHandler: completionHandler\n        )\n    }\n    \n    /// Sets a live photo to the view with a ``LivePhotoSource``.\n    ///\n    /// - Parameters:\n    ///   - source: The ``LivePhotoSource`` object defining the live photo resource.\n    ///   - options: An options set to define image setting behaviors. See ``KingfisherOptionsInfo`` for more.\n    ///   - completionHandler: Called when the image setting process finishes.\n    /// - Returns: A task represents the image downloading.\n    ///            The return value will be `nil` if the image is set with a empty source.\n    ///\n    /// - Note: Not all options in ``KingfisherOptionsInfo`` are supported in this method, for example, the live photo\n    /// does not support any custom processors. Different from the extension method for a normal image view on the\n    /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future.\n    /// \n    /// Sample:\n    /// ```swift\n    /// let source = LivePhotoSource(urls: [\n    ///   URL(string: \"https://example.com/image.heic\")!, // imageURL\n    ///   URL(string: \"https://example.com/video.mov\")!   // videoURL\n    /// ])\n    /// let livePhotoView = PHLivePhotoView()\n    /// livePhotoView.kf.setImage(with: source) { result in \n    ///   switch result {\n    ///   case .success(let retrieveResult):\n    ///     print(\"Live photo loaded: \\(retrieveResult.livePhoto).\")\n    ///     print(\"Cache type: \\(retrieveResult.loadingInfo.cacheType).\")\n    ///   case .failure(let error):\n    ///     print(\"Error: \\(error)\")\n    /// }\n    /// ```\n    @discardableResult\n    public func setImage(\n        with source: LivePhotoSource?,\n        // placeholder: KFCrossPlatformImage? = nil, // Not supported yet\n        options: KingfisherOptionsInfo? = nil,\n        // progressBlock: DownloadProgressBlock? = nil, // Not supported yet\n        completionHandler: (@MainActor @Sendable (Result<RetrieveLivePhotoResult, KingfisherError>) -> Void)? = nil\n    ) -> Task<(), Never>? {\n        var mutatingSelf = self\n\n        // Empty source fails the loading early and clear the current task identifier.\n        guard let source = source else {\n            base.livePhoto = nil\n            mutatingSelf.taskIdentifier = nil\n            completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))\n            return nil\n        }\n        \n        let issuedIdentifier = Source.Identifier.next()\n        mutatingSelf.taskIdentifier = issuedIdentifier\n        \n        let taskIdentifierChecking = { issuedIdentifier == self.taskIdentifier }\n\n        // Copy these associated values to prevent issues from reentrance.\n        let targetSize = targetSize\n        let contentMode = contentMode\n        \n        let task = Task { @MainActor in\n            do {\n                let loadingInfo = try await KingfisherManager.shared.retrieveLivePhoto(\n                    with: source,\n                    options: options,\n                    progressBlock: nil, // progressBlock, // Not supported yet\n                    referenceTaskIdentifierChecker: taskIdentifierChecking\n                )\n                if let notCurrentTaskError = self.checkNotCurrentTask(\n                    issuedIdentifier: issuedIdentifier,\n                    result: .init(loadingInfo: loadingInfo, livePhoto: nil, info: nil),\n                    error: nil,\n                    source: source\n                ) {\n                    completionHandler?(.failure(notCurrentTaskError))\n                    return\n                }\n                \n                PHLivePhoto.request(\n                    withResourceFileURLs: loadingInfo.fileURLs,\n                    placeholderImage: nil,\n                    targetSize: targetSize,\n                    contentMode: contentMode,\n                    resultHandler: { livePhoto, info in\n                        let result = RetrieveLivePhotoResult(\n                            loadingInfo: loadingInfo,\n                            livePhoto: livePhoto,\n                            info: info\n                        )\n                        \n                        if let notCurrentTaskError = self.checkNotCurrentTask(\n                            issuedIdentifier: issuedIdentifier,\n                            result: result,\n                            error: nil,\n                            source: source\n                        ) {\n                            completionHandler?(.failure(notCurrentTaskError))\n                            return\n                        }\n                        \n                        base.livePhoto = livePhoto\n                        \n                        if let error = info[PHLivePhotoInfoErrorKey] as? NSError {\n                            let failingReason: KingfisherError.ImageSettingErrorReason =\n                                .livePhotoResultError(result: result, error: error, source: source)\n                            completionHandler?(.failure(.imageSettingError(reason: failingReason)))\n                            return\n                        }\n                        \n                        // Since we are not returning the request ID, seems no way for user to cancel it if the \n                        // `request` method is called. However, we are sure the request method will always load the \n                        // image from disk, it should not be a problem. In case we still report the error in the \n                        // completion\n                        if (info[PHLivePhotoInfoCancelledKey] as? NSNumber)?.boolValue ?? false {\n                            completionHandler?(.failure(\n                                .requestError(reason: .livePhotoTaskCancelled(source: source)))\n                            )\n                            return\n                        }\n                        \n                        // If the PHLivePhotoInfoIsDegradedKey value in your result handler’s info dictionary is true,\n                        // Photos will call your result handler again.\n                        if (info[PHLivePhotoInfoIsDegradedKey] as? NSNumber)?.boolValue == true {\n                            // This ensures `completionHandler` be only called once.\n                            return\n                        }\n                        \n                        completionHandler?(.success(result))\n                    }\n                )\n            } catch {\n                if let notCurrentTaskError = self.checkNotCurrentTask(\n                    issuedIdentifier: issuedIdentifier,\n                    result: nil,\n                    error: error,\n                    source: source\n                ) {\n                    completionHandler?(.failure(notCurrentTaskError))\n                    return\n                }\n                \n                if let kfError = error as? KingfisherError {\n                    completionHandler?(.failure(kfError))\n                } else if error is CancellationError {\n                    completionHandler?(.failure(.requestError(reason: .livePhotoTaskCancelled(source: source))))\n                } else {\n                    completionHandler?(.failure(.imageSettingError(\n                        reason: .livePhotoResultError(result: nil, error: error, source: source)))\n                    )\n                }\n            }\n        }\n        \n        return task\n    }\n    \n    private func checkNotCurrentTask(\n        issuedIdentifier: Source.Identifier.Value,\n        result: RetrieveLivePhotoResult?,\n        error: (any Error)?,\n        source: LivePhotoSource\n    ) -> KingfisherError? {\n        if issuedIdentifier == self.taskIdentifier {\n            return nil\n        }\n        return .imageSettingError(reason: .notCurrentLivePhotoSourceTask(result: result, error: error, source: source))\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Extensions/UIButton+Kingfisher.swift",
    "content": "//\n//  UIButton+Kingfisher.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/13.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n\n#if canImport(UIKit)\nimport UIKit\n\n@MainActor\nextension KingfisherWrapper where Base: UIButton {\n\n    // MARK: Setting Image\n    /// Sets an image to the button for a specified state with a source.\n    ///\n    /// - Parameters:\n    ///   - source: The `Source` object contains information about the image.\n    ///   - state: The button state to which the image should be set.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.\n    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.\n    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieved and set finished.\n    /// - Returns: A task represents the image downloading.\n    ///\n    /// - Note:\n    /// Internally, this method will use `KingfisherManager` to get the requested source, from either cache\n    /// or network. Since this method will perform UI changes, you must call it from the main thread.\n    /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.\n    ///\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        for state: UIControl.State,\n        placeholder: UIImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))\n        return setImage(\n            with: source,\n            for: state,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n    \n    /// Sets an image to the button for a specified state with a requested resource.\n    ///\n    /// - Parameters:\n    ///   - resource: The `Resource` object contains information about the resource.\n    ///   - state: The button state to which the image should be set.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.\n    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.\n    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieved and set finished.\n    /// - Returns: A task represents the image downloading.\n    ///\n    /// - Note:\n    /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache\n    /// or network. Since this method will perform UI changes, you must call it from the main thread.\n    /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.\n    ///\n    @discardableResult\n    public func setImage(\n        with resource: (any Resource)?,\n        for state: UIControl.State,\n        placeholder: UIImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        return setImage(\n            with: resource?.convertToSource(),\n            for: state,\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler)\n    }\n\n    @discardableResult\n    public func setImage(\n        with source: Source?,\n        for state: UIControl.State,\n        placeholder: UIImage? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        var mutatingSelf = self\n        return setImage(\n            with: source,\n            imageAccessor: ImagePropertyAccessor(\n                setImage: { image, _ in base.setImage(image, for: state) },\n                getImage: { base.image(for: state) }\n            ),\n            taskAccessor: TaskPropertyAccessor(\n                setTaskIdentifier: { setTaskIdentifier($0, for: state) },\n                getTaskIdentifier: { taskIdentifier(for: state) },\n                setTask: { mutatingSelf.imageTask = $0 }\n            ),\n            placeholder: placeholder,\n            parsedOptions: parsedOptions,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    // MARK: Cancelling Downloading Task\n    \n    /// Cancels the image download task of the button if it is running.\n    /// Nothing will happen if the downloading has already finished.\n    public func cancelImageDownloadTask() {\n        imageTask?.cancel()\n    }\n\n    // MARK: Setting Background Image\n\n    /// Sets a background image to the button for a specified state with a source.\n    ///\n    /// - Parameters:\n    ///   - source: The `Source` object contains information about the image.\n    ///   - state: The button state to which the image should be set.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.\n    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.\n    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieved and set finished.\n    /// - Returns: A task represents the image downloading.\n    ///\n    /// - Note:\n    /// Internally, this method will use `KingfisherManager` to get the requested source\n    /// Since this method will perform UI changes, you must call it from the main thread.\n    /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.\n    ///\n    @discardableResult\n    public func setBackgroundImage(\n        with source: Source?,\n        for state: UIControl.State,\n        placeholder: UIImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))\n        return setBackgroundImage(\n            with: source,\n            for: state,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Sets a background image to the button for a specified state with a requested resource.\n    ///\n    /// - Parameters:\n    ///   - resource: The `Resource` object contains information about the resource.\n    ///   - state: The button state to which the image should be set.\n    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.\n    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.\n    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an\n    ///                    `expectedContentLength`, this block will not be called.\n    ///   - completionHandler: Called when the image retrieved and set finished.\n    /// - Returns: A task represents the image downloading.\n    ///\n    /// - Note:\n    /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache\n    /// or network. Since this method will perform UI changes, you must call it from the main thread.\n    /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.\n    ///\n    @discardableResult\n    public func setBackgroundImage(\n        with resource: (any Resource)?,\n        for state: UIControl.State,\n        placeholder: UIImage? = nil,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        return setBackgroundImage(\n            with: resource?.convertToSource(),\n            for: state,\n            placeholder: placeholder,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler)\n    }\n\n    func setBackgroundImage(\n        with source: Source?,\n        for state: UIControl.State,\n        placeholder: UIImage? = nil,\n        parsedOptions: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@MainActor @Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?\n    {\n        var mutatingSelf = self\n        return setImage(\n            with: source,\n            imageAccessor: ImagePropertyAccessor(\n                setImage: { image, _ in\n                    base.setBackgroundImage(image, for: state)\n                },\n                getImage: {\n                    base.backgroundImage(for: state)\n                }\n            ),\n            taskAccessor: TaskPropertyAccessor(\n                setTaskIdentifier: { setBackgroundTaskIdentifier($0, for: state) },\n                getTaskIdentifier: { backgroundTaskIdentifier(for: state) },\n                setTask: { mutatingSelf.backgroundImageTask = $0 }\n            ),\n            placeholder: placeholder,\n            parsedOptions: parsedOptions,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler\n        )\n    }\n\n    // MARK: Cancelling Background Downloading Task\n    \n    /// Cancels the background image download task of the button if it is running.\n    /// Nothing will happen if the downloading has already finished.\n    public func cancelBackgroundImageDownloadTask() {\n        backgroundImageTask?.cancel()\n    }\n}\n\n// MARK: - Associated Object\n@MainActor private var taskIdentifierKey: Void?\n@MainActor private var imageTaskKey: Void?\n\n// MARK: Properties\n@MainActor\nextension KingfisherWrapper where Base: UIButton {\n\n    private typealias TaskIdentifier = Box<[UInt: Source.Identifier.Value]>\n    \n    public func taskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? {\n        return taskIdentifierInfo.value[state.rawValue]\n    }\n\n    private func setTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) {\n        taskIdentifierInfo.value[state.rawValue] = identifier\n    }\n    \n    private var taskIdentifierInfo: TaskIdentifier {\n        return  getAssociatedObject(base, &taskIdentifierKey) ?? {\n            setRetainedAssociatedObject(base, &taskIdentifierKey, $0)\n            return $0\n        } (TaskIdentifier([:]))\n    }\n    \n    private var imageTask: DownloadTask? {\n        get { return getAssociatedObject(base, &imageTaskKey) }\n        set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}\n    }\n}\n\n\n@MainActor private var backgroundTaskIdentifierKey: Void?\n@MainActor private var backgroundImageTaskKey: Void?\n\n// MARK: Background Properties\n@MainActor\nextension KingfisherWrapper where Base: UIButton {\n    \n    public func backgroundTaskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? {\n        return backgroundTaskIdentifierInfo.value[state.rawValue]\n    }\n    \n    private func setBackgroundTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) {\n        backgroundTaskIdentifierInfo.value[state.rawValue] = identifier\n    }\n    \n    private var backgroundTaskIdentifierInfo: TaskIdentifier {\n        return  getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? {\n            setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0)\n            return $0\n        } (TaskIdentifier([:]))\n    }\n    \n    private var backgroundImageTask: DownloadTask? {\n        get { return getAssociatedObject(base, &backgroundImageTaskKey) }\n        mutating set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) }\n    }\n}\n#endif\n\n#endif\n"
  },
  {
    "path": "Sources/General/ImageSource/AVAssetImageDataProvider.swift",
    "content": "//\n//  AVAssetImageDataProvider.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/08/09.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n\nimport Foundation\nimport AVKit\n\n#if canImport(MobileCoreServices)\nimport MobileCoreServices\n#else\nimport CoreServices\n#endif\n\n#if compiler(>=6)\nextension AVAssetImageGenerator: @unchecked @retroactive Sendable { }\n#else\nextension AVAssetImageGenerator: @unchecked Sendable { }\n#endif\n\n/// A data provider to provide thumbnail data from a given AVKit asset.\npublic struct AVAssetImageDataProvider: ImageDataProvider {\n\n    /// The possible error might be caused by the ``AVAssetImageDataProvider``.\n    public enum AVAssetImageDataProviderError: Error {\n        /// The data provider process is cancelled.\n        case userCancelled\n        /// The retrieved image is invalid.\n        /// - Parameter image: The image object that is not recognized as valid.\n        case invalidImage(_ image: CGImage?)\n    }\n\n    /// The asset image generator bound to `self`.\n    public let assetImageGenerator: AVAssetImageGenerator\n\n    /// The time at which the image should be generated in the asset.\n    public let time: CMTime\n\n    private var internalKey: String {\n        guard let url = (assetImageGenerator.asset as? AVURLAsset)?.url else {\n            return UUID().uuidString\n        }\n        return url.cacheKey\n    }\n\n    /// The cache key used by `self`.\n    public var cacheKey: String {\n        return \"\\(internalKey)_\\(time.seconds)\"\n    }\n\n    /// Creates an asset image data provider.\n    /// - Parameters:\n    ///   - assetImageGenerator: The asset image generator that controls data providing behaviors.\n    ///   - time: The time at which the image should be generated in the asset.\n    public init(assetImageGenerator: AVAssetImageGenerator, time: CMTime) {\n        self.assetImageGenerator = assetImageGenerator\n        self.time = time\n    }\n\n    /// Creates an asset image data provider.\n    /// - Parameters:\n    ///   - assetURL: The URL of asset for providing image data.\n    ///   - time: At which time in the asset the image should be generated.\n    ///\n    /// This method uses the `assetURL` parameter to create an `AVAssetImageGenerator` object, then calls\n    /// the ``init(assetImageGenerator:time:)`` initializer.\n    public init(assetURL: URL, time: CMTime) {\n        let asset = AVAsset(url: assetURL)\n        let generator = AVAssetImageGenerator(asset: asset)\n        generator.appliesPreferredTrackTransform = true\n        self.init(assetImageGenerator: generator, time: time)\n    }\n\n    /// Creates an asset image data provider.\n    ///\n    /// - Parameters:\n    ///   - assetURL: The URL of asset for providing image data.\n    ///   - seconds: At which time in seconds in the asset the image should be generated.\n    ///\n    /// This method uses the `assetURL` parameter to create an `AVAssetImageGenerator` object, uses the `seconds`\n    /// parameter to create a `CMTime`, then calls the ``init(assetImageGenerator:time:)`` initializer.\n    ///\n    public init(assetURL: URL, seconds: TimeInterval) {\n        let time = CMTime(seconds: seconds, preferredTimescale: 600)\n        self.init(assetURL: assetURL, time: time)\n    }\n\n    public func data(handler: @Sendable @escaping (Result<Data, any Error>) -> Void) {\n        assetImageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) {\n            (requestedTime, image, imageTime, result, error) in\n            if let error = error {\n                handler(.failure(error))\n                return\n            }\n\n            if result == .cancelled {\n                handler(.failure(AVAssetImageDataProviderError.userCancelled))\n                return\n            }\n\n            guard let cgImage = image, let data = cgImage.jpegData else {\n                handler(.failure(AVAssetImageDataProviderError.invalidImage(image)))\n                return\n            }\n\n            handler(.success(data))\n        }\n    }\n}\n\nextension CGImage {\n    var jpegData: Data? {\n        guard let mutableData = CFDataCreateMutable(nil, 0) else {\n            return nil\n        }\n#if os(visionOS)\n        guard let destination = CGImageDestinationCreateWithData(\n            mutableData, UTType.jpeg.identifier as CFString , 1, nil\n        ) else {\n            return nil\n        }\n#else\n        guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {\n            return nil\n        }\n#endif\n        \n        CGImageDestinationAddImage(destination, self, nil)\n        guard CGImageDestinationFinalize(destination) else { return nil }\n        return mutableData as Data\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/General/ImageSource/ImageDataProvider.swift",
    "content": "//\n//  ImageDataProvider.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/13.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport ImageIO\n\n/// Represents a data provider to provide image data to Kingfisher when setting with\n/// ``Source/provider(_:)`` source. Compared to ``Source/network(_:)`` member, it gives a chance\n/// to load some image data in your own way, as long as you can provide the data\n/// representation for the image.\npublic protocol ImageDataProvider: Sendable {\n    \n    /// The key used in cache.\n    var cacheKey: String { get }\n    \n    /// Provides the data which represents image. Kingfisher uses the data you pass in the\n    /// handler to process images and caches it for later use.\n    ///\n    /// - Parameter handler: The handler you should call when you prepared your data.\n    ///                      If the data is loaded successfully, call the handler with\n    ///                      a `.success` with the data associated. Otherwise, call it\n    ///                      with a `.failure` and pass the error.\n    ///\n    /// - Note: If the `handler` is called with a `.failure` with error,\n    /// a ``KingfisherError/ImageSettingErrorReason/dataProviderError(provider:error:)`` will be finally thrown out to\n    /// you as the ``KingfisherError`` from the framework.\n    func data(handler: @escaping @Sendable (Result<Data, any Error>) -> Void)\n\n    /// The content URL represents this provider, if exists.\n    var contentURL: URL? { get }\n}\n\nextension ImageDataProvider {\n    func data() async throws -> Data {\n        try await withCheckedThrowingContinuation { continuation in\n            data(handler: { continuation.resume(with: $0) })\n        }\n    }\n}\n\npublic extension ImageDataProvider {\n    var contentURL: URL? { return nil }\n    func convertToSource() -> Source {\n        .provider(self)\n    }\n}\n\n/// Represents an image data provider for loading from a local file URL on disk.\n/// Uses this type for adding a disk image to Kingfisher. Compared to loading it\n/// directly, you can get benefit of using Kingfisher's extension methods, as well\n/// as applying ``ImageProcessor``s and storing the image to ``ImageCache`` of Kingfisher.\npublic struct LocalFileImageDataProvider: ImageDataProvider {\n\n    // MARK: Public Properties\n\n    /// The file URL from which the image be loaded.\n    public let fileURL: URL\n    private let loadingQueue: ExecutionQueue\n\n    // MARK: Initializers\n\n    /// Creates an image data provider by supplying the target local file URL.\n    ///\n    /// - Parameters:\n    ///   - fileURL: The file URL from which the image be loaded.\n    ///   - cacheKey: The key is used for caching the image data. By default,\n    ///               the `absoluteString` of ``LocalFileImageDataProvider/fileURL`` is used.\n    ///   - loadingQueue: The queue where the file loading should happen. By default, the dispatch queue of\n    ///                   `.global(qos: .userInitiated)` will be used.\n    public init(\n        fileURL: URL,\n        cacheKey: String? = nil,\n        loadingQueue: ExecutionQueue = .dispatch(DispatchQueue.global(qos: .userInitiated))\n    ) {\n        self.fileURL = fileURL\n        self.cacheKey = cacheKey ?? fileURL.localFileCacheKey\n        self.loadingQueue = loadingQueue\n    }\n\n    // MARK: Protocol Conforming\n\n    /// The key used in cache.\n    public var cacheKey: String\n\n    public func data(handler: @escaping @Sendable (Result<Data, any Error>) -> Void) {\n        loadingQueue.execute {\n            handler(Result(catching: { try Data(contentsOf: fileURL) }))\n        }\n    }\n    \n    public var data: Data {\n        get async throws {\n            try await withCheckedThrowingContinuation { continuation in\n                loadingQueue.execute {\n                    do {\n                        let data = try Data(contentsOf: fileURL)\n                        continuation.resume(returning: data)\n                    } catch {\n                        continuation.resume(throwing: error)\n                    }\n                }\n            }\n        }\n    }\n\n    /// The URL of the local file on the disk.\n    public var contentURL: URL? {\n        return fileURL\n    }\n}\n\n/// Represents an image data provider for loading image from a given Base64 encoded string.\npublic struct Base64ImageDataProvider: ImageDataProvider {\n\n    // MARK: Public Properties\n    /// The encoded Base64 string for the image.\n    public let base64String: String\n\n    // MARK: Initializers\n\n    /// Creates an image data provider by supplying the Base64 encoded string.\n    ///\n    /// - Parameters:\n    ///   - base64String: The Base64 encoded string for an image.\n    ///   - cacheKey: The key is used for caching the image data. You need a different key for any different image.\n    public init(base64String: String, cacheKey: String) {\n        self.base64String = base64String\n        self.cacheKey = cacheKey\n    }\n\n    // MARK: Protocol Conforming\n\n    /// The key used in cache.\n    public var cacheKey: String\n\n    public func data(handler: (Result<Data, any Error>) -> Void) {\n        let data = Data(base64Encoded: base64String)!\n        handler(.success(data))\n    }\n}\n\n/// Represents an image data provider for a raw data object.\npublic struct RawImageDataProvider: ImageDataProvider {\n\n    // MARK: Public Properties\n\n    /// The raw data object to provide to Kingfisher image loader.\n    public let data: Data\n\n    // MARK: Initializers\n\n    /// Creates an image data provider by the given raw `data` value and a `cacheKey` be used in Kingfisher cache.\n    ///\n    /// - Parameters:\n    ///   - data: The raw data represents an image.\n    ///   - cacheKey: The key is used for caching the image data. You need a different key for any different image.\n    public init(data: Data, cacheKey: String) {\n        self.data = data\n        self.cacheKey = cacheKey\n    }\n\n    // MARK: Protocol Conforming\n    \n    /// The key used in cache.\n    public var cacheKey: String\n\n    public func data(handler: @escaping (Result<Data, any Error>) -> Void) {\n        handler(.success(data))\n    }\n}\n\n/// A data provider that creates a thumbnail from a URL using Core Graphics.\npublic struct ThumbnailImageDataProvider: ImageDataProvider {\n    \n    public enum ThumbnailImageDataProviderError: Error {\n        case invalidImageSource\n        case invalidThumbnail\n        case writeDataError\n        case finalizeDataError\n    }\n    \n    /// The URL from which to load the image\n    public let url: URL\n    \n    /// The maximum size of the thumbnail in pixels\n    public var maxPixelSize: CGFloat\n    \n    /// Whether to always create a thumbnail even if the image is smaller than maxPixelSize\n    public var alwaysCreateThumbnail: Bool\n    \n    /// The cache key for this provider\n    public var cacheKey: String\n    \n    /// Creates a new thumbnail data provider\n    /// - Parameters:\n    ///   - url: The URL from which to load the image\n    ///   - maxPixelSize: The maximum size of the thumbnail in pixels\n    ///   - alwaysCreateThumbnail: Whether to always create a thumbnail even if the image is smaller than maxPixelSize\n    public init(\n        url: URL,\n        maxPixelSize: CGFloat,\n        alwaysCreateThumbnail: Bool = true,\n        cacheKey: String? = nil\n    ) {\n        self.url = url\n        self.maxPixelSize = maxPixelSize\n        self.alwaysCreateThumbnail = alwaysCreateThumbnail\n        self.cacheKey = cacheKey ?? \"\\(url.absoluteString)_thumb_\\(maxPixelSize)_\\(alwaysCreateThumbnail)\"\n    }\n    \n    public func data(handler: @escaping @Sendable (Result<Data, any Error>) -> Void) {\n        DispatchQueue.global(qos: .userInitiated).async {\n            do {\n                guard let url = URL(string: url.absoluteString) else {\n                    throw KingfisherError.imageSettingError(reason: .emptySource)\n                        \n                }\n                \n                guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else {\n                    throw ThumbnailImageDataProviderError.invalidImageSource\n                }\n                \n                let options = [\n                    kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,\n                    kCGImageSourceCreateThumbnailFromImageAlways: alwaysCreateThumbnail,\n                    kCGImageSourceCreateThumbnailWithTransform: true\n                ]\n                \n                guard let thumbnailRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else {\n                    throw ThumbnailImageDataProviderError.invalidThumbnail\n                }\n                \n                let data = NSMutableData()\n                guard let destination = CGImageDestinationCreateWithData(\n                    data, CGImageSourceGetType(imageSource)!, 1, nil\n                ) else {\n                    throw ThumbnailImageDataProviderError.writeDataError\n                }\n                \n                CGImageDestinationAddImage(destination, thumbnailRef, nil)\n                if CGImageDestinationFinalize(destination) {\n                    handler(.success(data as Data))\n                } else {\n                    throw ThumbnailImageDataProviderError.finalizeDataError\n                }\n            } catch {\n                handler(.failure(error))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/General/ImageSource/LivePhotoSource.swift",
    "content": "//\n//  LivePhotoSource.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2024/10/01.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// A type represents a loadable resource for a Live Photo, which consists of a still image and a video.\n/// \n/// Kingfisher expects a ``LivePhotoSource`` value to load a Live Photo with its high-level APIs. \n/// A ``LivePhotoSource`` is typically a collection of two ``LivePhotoResource`` values, one for the still image and \n/// one for the video.\npublic struct LivePhotoSource: Sendable {\n    \n    /// The resources of a Live Photo.\n    public let resources: [LivePhotoResource]\n    \n    /// Creates a Live Photo source with given resources.\n    /// - Parameter resources: The downloadable resource for a Live Photo. It should contain two resources, one for the\n    /// still image and one for the video.\n    public init(resources: [any Resource]) {\n        let livePhotoResources = resources.map { LivePhotoResource(resource: $0) }\n        self.init(livePhotoResources)\n    }\n    \n    /// Creates a Live Photo source with given URLs.\n    /// - Parameter urls: The URLs of the downloadable resources for a Live Photo. It should contain two URLs, one for\n    /// the still image and one for the video.\n    public init(urls: [URL]) {\n        let resources = urls.map { KF.ImageResource(downloadURL: $0) }\n        self.init(resources: resources)\n    }\n    \n    /// Creates a Live Photo source with given resources.\n    /// - Parameter resources: The resources for a Live Photo. It should contain two resources, one for the still image\n    /// and one for the video.\n    public init(_ resources: [LivePhotoResource]) {\n        self.resources = resources\n    }\n}\n\n\n/// A resource type representing a component of a Live Photo, which consists of a still image and a video.\n///\n/// ``LivePhotoResource`` encapsulates the necessary information to download and cache a single component of a Live\n/// Photo: it is either a still image (typically in HEIF format with \"heic\" filename extension) or a video (typically in\n/// QuickTime format with \"mov\" filename extension). Multiple ``LivePhotoResource`` values (typically two, one for the\n/// image and one for the video) can form a ``LivePhotoSource``, which is expected by Kingfisher in its live photo\n/// loading high level APIs.\n///\n/// The Live Photo data can be retrieved by `PHAssetResourceManager.requestData` method and uploaded to your server.\n/// You should not modify the metadata or other information of the data, otherwise, it is possible that the\n/// `PHLivePhoto` class cannot read and recognize it anymore. For more information, please refer to Apple's\n/// documentation of Photos framework.\npublic struct LivePhotoResource: Sendable {\n    \n    /// The file type of a ``LivePhotoResource``.\n    public enum FileType: Sendable, Equatable {\n        /// File type HEIC. Usually it represents the still image in a Live Photo.\n        case heic\n        /// File type MOV. Usually it represents the video in a Live Photo.\n        case mov\n        /// Other file types with the file extension.\n        case other(String)\n        \n        var fileExtension: String {\n            switch self {\n            case .heic: return \"heic\"\n            case .mov: return \"mov\"\n            case .other(let ext): return ext\n            }\n        }\n    }\n    \n    /// The data source of a Live Photo resource.\n    /// \n    /// This is a general ``Source`` type, which can be either a network resource (as ``Source/network(_:)``) or a\n    /// provided resource as ``Source/provider(_:)``.\n    public let dataSource: Source\n\n    /// The file type of the resource.\n    public let referenceFileType: FileType\n    \n    var cacheKey: String { dataSource.cacheKey }\n    var downloadURL: URL? { dataSource.url }\n        \n    /// Creates a Live Photo resource with given download URL, cache key and file type.\n    /// - Parameters:\n    ///   - downloadURL: The URL to download the resource.\n    ///   - cacheKey: The cache key for the resource. If `nil`, Kingfisher will use the `absoluteString` of the URL as\n    ///     the cache key.\n    ///   - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL.\n    /// \n    /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them\n    /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV \n    /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about\n    /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded \n    /// data.\n    public init(downloadURL: URL, cacheKey: String? = nil, fileType: FileType? = nil) {\n        let resource = KF.ImageResource(downloadURL: downloadURL, cacheKey: cacheKey)\n        dataSource = .network(resource)\n        referenceFileType = fileType ?? resource.guessedFileType\n    }\n    \n    /// Creates a Live Photo resource with given resource and file type.\n    /// - Parameters:\n    ///   - resource: The resource to download the data.\n    ///   - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL.\n    /// \n    /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them\n    /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV \n    /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about\n    /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded \n    /// data.\n    public init(resource: any Resource, fileType: FileType? = nil) {\n        self.dataSource = .network(resource)\n        referenceFileType = fileType ?? resource.guessedFileType\n    }\n    \n    /// Creates a Live Photo resource with given data source and file type.\n    /// - Parameters:\n    ///   - source: The data source of the resource. It can be either a network resource or a provided resource.\n    ///   - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL.\n    /// \n    /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them\n    /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV \n    /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about\n    /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded \n    /// data.\n    public init(source: Source, fileType: FileType? = nil) {\n        self.dataSource = source\n        referenceFileType = fileType ?? source.url?.guessedFileType ?? .other(\"\")\n    }\n}\n\nextension LivePhotoResource.FileType {\n    func determinedFileExtension(_ data: Data) -> String? {\n        switch self {\n        case .mov: return \"mov\"\n        case .heic: return \"heic\"\n        case .other(let ext):\n            if !ext.isEmpty {\n                return ext\n            }\n            return Self.guessedFileExtension(from: data)\n        }\n    }\n    \n    static let fytpChunk: [UInt8] = [0x66, 0x74, 0x79, 0x70] // fytp (file type box)\n    static let heicChunk: [UInt8] = [0x68, 0x65, 0x69, 0x63] // heic (HEIF)\n    static let qtChunk: [UInt8] = [0x71, 0x74, 0x20, 0x20] // qt (QuickTime), .mov\n    \n    static func guessedFileExtension(from data: Data) -> String? {\n        \n        guard data.count >= 12 else { return nil }\n        \n        var buffer = [UInt8](repeating: 0, count: 12)\n        data.copyBytes(to: &buffer, count: 12)\n        \n        guard Array(buffer[4..<8]) == fytpChunk else {\n            return nil\n        }\n        \n        let fileTypeChunk = Array(buffer[8..<12])\n        if fileTypeChunk == heicChunk {\n            return \"heic\"\n        }\n        if fileTypeChunk == qtChunk {\n            return \"mov\"\n        }\n        return nil\n    }\n}\n\nextension Resource {\n    var guessedFileType: LivePhotoResource.FileType {\n        let pathExtension = downloadURL.pathExtension.lowercased()\n        switch pathExtension {\n        case \"mov\": return .mov\n        case \"heic\": return .heic\n        default: return .other(pathExtension)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/General/ImageSource/PHPickerResultImageDataProvider.swift",
    "content": "//\n//  PHPickerResultImageDataProvider.swift\n//  Kingfisher\n//\n//  Created by nuomi1 on 2024-04-17.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n#if os(iOS) || os(macOS) || os(visionOS)\n\nimport PhotosUI\n\n#if compiler(>=6)\n@available(iOS 14.0, macOS 13.0, *)\nextension PHPickerResult: @unchecked @retroactive Sendable { }\n#else\n@available(iOS 14.0, macOS 13.0, *)\nextension PHPickerResult: @unchecked Sendable { }\n#endif\n\n/// A data provider to provide image data from a given `PHPickerResult`.\n@available(iOS 14.0, macOS 13.0, *)\npublic struct PHPickerResultImageDataProvider: ImageDataProvider {\n\n    internal static func _cacheKey(\n        providedCacheKey: String?,\n        assetIdentifier: String?,\n        contentTypeIdentifier: String,\n        uuidString: () -> String\n    ) -> String {\n        if let providedCacheKey {\n            return providedCacheKey\n        }\n        let id = assetIdentifier ?? uuidString()\n        return \"\\(id)_\\(contentTypeIdentifier)\"\n    }\n\n    /// The possible error might be caused by the `PHPickerResultImageDataProvider`.\n    /// - invalidImage: The retrieved image is invalid.\n    public enum PHPickerResultImageDataProviderError: Error {\n        /// An error happens during picking up image through the item provider of `PHPickerResult`.\n        case pickerProviderError(any Error)\n        /// The retrieved image is invalid.\n        case invalidImage\n    }\n\n    /// The picker result bound to `self`.\n    public let pickerResult: PHPickerResult\n\n    /// The content type of the image.\n    public let contentType: UTType\n\n    /// The key used in cache.\n    ///\n    /// If you pass a custom key when creating the provider, it will be used.\n    /// Otherwise, if the picker result contains a stable asset identifier, it will be used as the key.\n    /// If no stable identifier is available, a random UUID will be generated and used for this provider instance.\n    public let cacheKey: String\n\n    /// Creates an image data provider from a given `PHPickerResult`.\n    /// - Parameters:\n    ///  - pickerResult: The picker result to provide image data.\n    ///  - contentType: The content type of the image. Default is `UTType.image`.\n    ///  - cacheKey: Optional cache key to use. If set, it will be used as `self.cacheKey` directly.\n    public init(pickerResult: PHPickerResult, contentType: UTType = UTType.image, cacheKey: String? = nil) {\n        self.pickerResult = pickerResult\n        self.contentType = contentType\n\n        if cacheKey == nil && pickerResult.assetIdentifier == nil {\n            assertionFailure(\"[Kingfisher] Should use `PHPhotoLibrary.shared()` to pick image.\")\n        }\n\n        self.cacheKey = Self._cacheKey(\n            providedCacheKey: cacheKey,\n            assetIdentifier: pickerResult.assetIdentifier,\n            contentTypeIdentifier: contentType.identifier,\n            uuidString: { UUID().uuidString }\n        )\n    }\n\n    public func data(handler: @escaping @Sendable (Result<Data, any Error>) -> Void) {\n        pickerResult.itemProvider.loadDataRepresentation(forTypeIdentifier: contentType.identifier) { data, error in\n            if let error {\n                handler(.failure(PHPickerResultImageDataProviderError.pickerProviderError(error)))\n                return\n            }\n\n            guard let data else {\n                handler(.failure(PHPickerResultImageDataProviderError.invalidImage))\n                return\n            }\n\n            handler(.success(data))\n        }\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/General/ImageSource/PhotosPickerItemImageDataProvider.swift",
    "content": "//\n//  PhotosPickerItemImageDataProvider.swift\n//  Kingfisher\n//\n//  Created by nuomi1 on 2026/1/7.\n//\n//  Copyright (c) 2026 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n#if os(iOS) || os(macOS) || os(visionOS)\n\nimport PhotosUI\nimport SwiftUI\n\n/// A data provider to provide image data from a given `PhotosPickerItem`.\n@available(iOS 16.0, macOS 13.0, *)\npublic struct PhotosPickerItemImageDataProvider: ImageDataProvider {\n\n    internal static func _cacheKey(\n        providedCacheKey: String?,\n        itemIdentifier: String?,\n        uuidString: () -> String\n    ) -> String {\n        if let providedCacheKey {\n            return providedCacheKey\n        }\n        if let itemIdentifier {\n            return itemIdentifier\n        }\n        return uuidString()\n    }\n\n    /// The possible error might be caused by the `PhotosPickerItemImageDataProvider`.\n    /// - invalidImage: The retrieved image is invalid.\n    public enum PhotosPickerItemImageDataProviderError: Error {\n        /// An error happens during picking up image through the item provider of `PhotosPickerItem`.\n        case pickerProviderError(any Error)\n        /// The retrieved image is invalid.\n        case invalidImage\n    }\n\n    /// The picker item bound to `self`.\n    public let pickerItem: PhotosPickerItem\n\n    /// The key used in cache.\n    ///\n    /// If you pass a custom key when creating the provider, it will be used.\n    /// Otherwise, if the picker item provides a stable identifier, it will be used.\n    /// If no stable identifier is available, a random UUID will be generated and used for this provider instance.\n    public let cacheKey: String\n\n    /// Creates an image data provider from a given `PhotosPickerItem`.\n    /// - Parameters:\n    ///  - pickerItem: The picker item to provide image data.\n    ///  - cacheKey: Optional cache key to use. If set, it will be used as `self.cacheKey` directly.\n    public init(pickerItem: PhotosPickerItem, cacheKey: String? = nil) {\n        self.pickerItem = pickerItem\n\n        if cacheKey == nil && pickerItem.itemIdentifier == nil {\n            assertionFailure(\"[Kingfisher] Should use `PHPhotoLibrary.shared()` to pick image.\")\n        }\n\n        self.cacheKey = Self._cacheKey(\n            providedCacheKey: cacheKey,\n            itemIdentifier: pickerItem.itemIdentifier,\n            uuidString: { UUID().uuidString }\n        )\n    }\n\n    public func data(handler: @escaping @Sendable (Result<Data, any Error>) -> Void) {\n        pickerItem.loadTransferable(type: Data.self, completionHandler: { result in\n            switch result {\n            case let .success(data):\n                if let data {\n                    handler(.success(data))\n                } else {\n                    handler(.failure(PhotosPickerItemImageDataProviderError.invalidImage))\n                }\n            case let .failure(error):\n                handler(.failure(PhotosPickerItemImageDataProviderError.pickerProviderError(error)))\n            }\n        })\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/General/ImageSource/Resource.swift",
    "content": "//\n//  Resource.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Represents an image resource at a certain url and a given cache key.\n/// Kingfisher will use a ``Resource`` to download a resource from network and cache it with the cache key when\n/// using ``Source/network(_:)`` as its image setting source.\npublic protocol Resource: Sendable {\n    \n    /// The key used in cache.\n    var cacheKey: String { get }\n    \n    /// The target image URL.\n    var downloadURL: URL { get }\n}\n\nextension Resource {\n\n    /// Converts `self` to a valid ``Source`` based on the ``Resource/downloadURL`` scheme. A ``Source/provider(_:)``\n    /// with ``LocalFileImageDataProvider`` associated will be returned if the URL points to a local file. Otherwise,\n    /// ``Source/network(_:)`` is returned.\n    ///\n    /// - Parameter overrideCacheKey: The key should be used to override the ``Resource/cacheKey`` when performing the\n    /// conversion. `nil` if not overridden and ``Resource/cacheKey`` of `self` is used.\n    /// - Returns: The converted source.\n    ///\n    public func convertToSource(overrideCacheKey: String? = nil) -> Source {\n        let key = overrideCacheKey ?? cacheKey\n        return downloadURL.isFileURL ?\n            .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) :\n            .network(KF.ImageResource(downloadURL: downloadURL, cacheKey: key))\n    }\n}\n\n@available(*, deprecated, message: \"This type conflicts with `GeneratedAssetSymbols.ImageResource` in Swift 5.9. Renamed to avoid issues in the future.\", renamed: \"KF.ImageResource\")\npublic typealias ImageResource = KF.ImageResource\n\n\nextension KF {\n    /// ``ImageResource`` is a simple combination of ``downloadURL`` and ``cacheKey``.\n    /// When passed to image view set methods, Kingfisher will try to download the target\n    /// image from the ``downloadURL``, and then store it with the ``cacheKey`` as the key in cache.\n    public struct ImageResource: Resource {\n\n        // MARK: - Initializers\n\n        /// Creates an image resource.\n        ///\n        /// - Parameters:\n        ///   - downloadURL: The target image URL from where the image can be downloaded.\n        ///   - cacheKey: \n        ///   The cache key. If `nil`, Kingfisher will use the `absoluteString` of ``ImageResource/downloadURL`` as\n        ///   the key. Default is `nil`.\n        ///   \n        public init(downloadURL: URL, cacheKey: String? = nil) {\n            self.downloadURL = downloadURL\n            self.cacheKey = cacheKey ?? downloadURL.cacheKey\n        }\n\n        // MARK: Protocol Conforming\n        \n        /// The key used in cache.\n        public let cacheKey: String\n\n        /// The target image URL.\n        public let downloadURL: URL\n    }\n}\n\n/// URL conforms to ``Resource`` in Kingfisher.\n/// The `absoluteString` of this URL is used as ``cacheKey``. And the URL itself will be used as `downloadURL`.\n/// If you need customize the url and/or cache key, use `ImageResource` instead.\nextension URL: Resource {\n    public var cacheKey: String { return isFileURL ? localFileCacheKey : absoluteString }\n    public var downloadURL: URL { return self }\n}\n\nextension URL {\n    static let localFileCacheKeyPrefix = \"kingfisher.local.cacheKey\"\n    \n    // The special version of cache key for a local file on disk. Every time the app is reinstalled on the disk,\n    // the system assigns a new container folder to hold the .app (and the extensions, .appex) folder. So the URL for\n    // the same image in bundle might be different.\n    //\n    // This getter only uses the fixed part in the URL (until the bundle name folder) to provide a stable cache key\n    // for the image under the same path inside the bundle.\n    //\n    // See #1825 (https://github.com/onevcat/Kingfisher/issues/1825)\n    var localFileCacheKey: String {\n        var validComponents: [String] = []\n        for part in pathComponents.reversed() {\n            validComponents.append(part)\n            if part.hasSuffix(\".app\") || part.hasSuffix(\".appex\") {\n                break\n            }\n        }\n        let fixedPath = \"\\(Self.localFileCacheKeyPrefix)/\\(validComponents.reversed().joined(separator: \"/\"))\"\n        if let q = query {\n            return \"\\(fixedPath)?\\(q)\"\n        } else {\n            return fixedPath\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/General/ImageSource/Source.swift",
    "content": "//\n//  Source.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/17.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n\n/// Represents an image source setting for Kingfisher methods.\n///\n/// A ``Source`` value indicates the way in which the target image can be retrieved and cached.\n///\n/// - `network`: The target image should be retrieved from the network remotely. The associated ``Resource``\n///            value defines detailed information like image URL and cache key.\n/// - `provider`: The target image should be provided in a data format. Normally, it can be an image\n///             from local storage or in any other encoding format (like Base64).\n///\npublic enum Source: Sendable {\n\n    /// Represents the source task identifier when setting an image to a view with extension methods.\n    public enum Identifier {\n\n        /// The underlying value type of source identifier.\n        public typealias Value = UInt\n        \n        @MainActor static private(set) var current: Value = 0\n        \n        // Not thread safe. Expected to be always called on the main thread.\n        @MainActor static func next() -> Value {\n            current += 1\n            return current\n        }\n    }\n\n    // MARK: Member Cases\n\n    /// The target image should be fetched from the network remotely. The associated `Resource`\n    /// value defines detailed information such as the image URL and cache key.\n    case network(any Resource)\n\n    /// The target image should be provided in a data format, typically as an image\n    /// from local storage or in any other encoding format, such as Base64.\n    case provider(any ImageDataProvider)\n\n    // MARK: Getting Properties\n\n    /// The cache key defined for this source value.\n    public var cacheKey: String {\n        switch self {\n        case .network(let resource): return resource.cacheKey\n        case .provider(let provider): return provider.cacheKey\n        }\n    }\n\n    /// The URL defined for this source value.\n    ///\n    /// For a ``Source/network(_:)`` source, it is the ``Resource/downloadURL`` of associated ``Resource`` instance.\n    /// For a ``Source/provider(_:)`` value, it is always `nil`.\n    public var url: URL? {\n        switch self {\n        case .network(let resource): return resource.downloadURL\n        case .provider(let provider): return provider.contentURL\n        }\n    }\n}\n\nextension Source: Hashable {\n    public static func == (lhs: Source, rhs: Source) -> Bool {\n        switch (lhs, rhs) {\n        case (.network(let r1), .network(let r2)):\n            return r1.cacheKey == r2.cacheKey && r1.downloadURL == r2.downloadURL\n        case (.provider(let p1), .provider(let p2)):\n            return p1.cacheKey == p2.cacheKey && p1.contentURL == p2.contentURL\n        case (.provider(_), .network(_)):\n            return false\n        case (.network(_), .provider(_)):\n            return false\n        }\n    }\n\n    public func hash(into hasher: inout Hasher) {\n        switch self {\n        case .network(let r):\n            hasher.combine(r.cacheKey)\n            hasher.combine(r.downloadURL)\n        case .provider(let p):\n            hasher.combine(p.cacheKey)\n            hasher.combine(p.contentURL)\n        }\n    }\n}\n\nextension Source {\n    var asResource: (any Resource)? {\n        guard case .network(let resource) = self else {\n            return nil\n        }\n        return resource\n    }\n}\n"
  },
  {
    "path": "Sources/General/KF.swift",
    "content": "//\n//  KF.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/09/21.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n#if canImport(CarPlay) && !targetEnvironment(macCatalyst)\nimport CarPlay\n#endif\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\n#endif\n\n#if canImport(WatchKit)\nimport WatchKit\n#endif\n\n#if canImport(TVUIKit)\nimport TVUIKit\n#endif\n\n/// A helper type to create image setting tasks in a builder pattern.\n///\n/// Use methods in this type to create a ``KF/Builder`` instance and configure image tasks there.\npublic enum KF {\n\n    /// Creates a builder for a given ``Source``.\n    /// - Parameter source: The ``Source`` object defines data information from network or a data provider.\n    /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its \n    /// `Builder/set(to:)` to start the image loading.\n    public static func source(_ source: Source?) -> KF.Builder {\n        Builder(source: source)\n    }\n\n    /// Creates a builder for a given ``Resource``.\n    /// - Parameter resource: The ``Resource`` object defines data information like key or URL.\n    /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its\n    /// `Builder/set(to:)` to start the image loading.\n    public static func resource(_ resource: (any Resource)?) -> KF.Builder {\n        source(resource?.convertToSource())\n    }\n\n    /// Creates a builder for a given `URL` and an optional cache key.\n    /// - Parameters:\n    ///   - url: The URL where the image should be downloaded.\n    ///   - cacheKey: The key used to store the downloaded image in cache.\n    ///               If `nil`, the `absoluteString` of `url` is used as the cache key.\n    /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its\n    /// `Builder/set(to:)` to start the image loading.\n    public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder {\n        source(url?.convertToSource(overrideCacheKey: cacheKey))\n    }\n\n    /// Creates a builder for a given ``ImageDataProvider``.\n    /// - Parameter provider: The ``ImageDataProvider`` object contains information about the data.\n    /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its\n    /// `Builder/set(to:)` to start the image loading.\n    public static func dataProvider(_ provider: (any ImageDataProvider)?) -> KF.Builder {\n        source(provider?.convertToSource())\n    }\n\n    /// Creates a builder for some given raw data and a cache key.\n    /// - Parameters:\n    ///   - data: The data object from which the image should be created.\n    ///   - cacheKey: The key used to store the downloaded image in cache.\n    /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its\n    /// `Builder/set(to:)` to start the image loading.\n    public static func data(_ data: Data?, cacheKey: String) -> KF.Builder {\n        if let data = data {\n            return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey))\n        } else {\n            return dataProvider(nil)\n        }\n    }\n}\n\n\nextension KF {\n\n    /// A builder class to configure an image retrieving task and set it to a holder view or component.\n    public class Builder: @unchecked Sendable {\n        \n        private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.KF.Builder.propertyQueue\")\n        \n        private let source: Source?\n\n        #if os(watchOS)\n        private var _placeholder: KFCrossPlatformImage?\n        private var placeholder: KFCrossPlatformImage? {\n            get { propertyQueue.sync { _placeholder } }\n            set { propertyQueue.sync { _placeholder = newValue } }\n        }\n        #else\n        private var _placeholder: (any Placeholder)?\n        private var placeholder: (any Placeholder)? {\n            get { propertyQueue.sync { _placeholder } }\n            set { propertyQueue.sync { _placeholder = newValue } }\n        }\n        #endif\n\n        private var _options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions)\n        public var options: KingfisherParsedOptionsInfo {\n            get { propertyQueue.sync { _options } }\n            set { propertyQueue.sync { _options = newValue } }\n        }\n\n        public let onFailureDelegate = Delegate<KingfisherError, Void>()\n        public let onSuccessDelegate = Delegate<RetrieveImageResult, Void>()\n        public let onProgressDelegate = Delegate<(Int64, Int64), Void>()\n\n        init(source: Source?) {\n            self.source = source\n        }\n\n        private var resultHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? {\n            {\n                switch $0 {\n                case .success(let result):\n                    self.onSuccessDelegate(result)\n                case .failure(let error):\n                    self.onFailureDelegate(error)\n                }\n            }\n        }\n\n        private var progressBlock: DownloadProgressBlock? {\n            onProgressDelegate.isSet ? { self.onProgressDelegate(($0, $1)) } : nil\n        }\n    }\n}\n\n@MainActor\nextension KF.Builder {\n    #if !os(watchOS)\n\n    /// Builds the image task request and sets it to an image view.\n    /// - Parameter imageView: The image view which loads the task and should be set with the image.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @discardableResult\n    public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? {\n        imageView.kf.setImage(\n            with: source,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n    }\n\n    /// Builds the image task request and sets it to an `NSTextAttachment` object.\n    /// - Parameters:\n    ///   - attachment: The text attachment object which loads the task and should be set with the image.\n    ///   - attributedView: The owner of the attributed string which this `NSTextAttachment` is added.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @discardableResult\n    public func set(\n        to attachment: NSTextAttachment,\n        attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView) -> DownloadTask? {\n        let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil\n        return attachment.kf.setImage(\n            with: source,\n            attributedView: attributedView,\n            placeholder: placeholderImage,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n    }\n\n    #if canImport(UIKit)\n\n    /// Builds the image task request and sets it to a button.\n    /// - Parameters:\n    ///   - button: The button which loads the task and should be set with the image.\n    ///   - state: The button state to which the image should be set.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @discardableResult\n    public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? {\n        let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil\n        return button.kf.setImage(\n            with: source,\n            for: state,\n            placeholder: placeholderImage,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n    }\n\n    /// Builds the image task request and sets it to the background image for a button.\n    /// - Parameters:\n    ///   - button: The button which loads the task and should be set with the image.\n    ///   - state: The button state to which the image should be set.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @discardableResult\n    public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? {\n        let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil\n        return button.kf.setBackgroundImage(\n            with: source,\n            for: state,\n            placeholder: placeholderImage,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n    }\n    #endif // end of canImport(UIKit)\n    \n    #if canImport(CarPlay) && !targetEnvironment(macCatalyst)\n    \n    /// Builds the image task request and sets it to the image for a list item.\n    /// - Parameters:\n    ///   - listItem: The list item which loads the task and should be set with the image.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @available(iOS 14.0, *)\n    @discardableResult\n    public func set(to listItem: CPListItem) -> DownloadTask? {\n        let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil\n        return listItem.kf.setImage(\n            with: source,\n            placeholder: placeholderImage,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n        \n    }\n    \n    #endif\n\n    #if canImport(AppKit) && !targetEnvironment(macCatalyst)\n    /// Builds the image task request and sets it to a button.\n    /// - Parameter button: The button which loads the task and should be set with the image.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @discardableResult\n    public func set(to button: NSButton) -> DownloadTask? {\n        let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil\n        return button.kf.setImage(\n            with: source,\n            placeholder: placeholderImage,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n    }\n\n    /// Builds the image task request and sets it to the alternative image for a button.\n    /// - Parameter button: The button which loads the task and should be set with the image.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @discardableResult\n    public func setAlternative(to button: NSButton) -> DownloadTask? {\n        let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil\n        return button.kf.setAlternateImage(\n            with: source,\n            placeholder: placeholderImage,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n    }\n    #endif // end of canImport(AppKit)\n    #endif // end of !os(watchOS)\n\n    #if canImport(WatchKit)\n    /// Builds the image task request and sets it to a `WKInterfaceImage` object.\n    /// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @discardableResult\n    public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? {\n        return interfaceImage.kf.setImage(\n            with: source,\n            placeholder: placeholder,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n    }\n    #endif // end of canImport(WatchKit)\n\n    #if canImport(TVUIKit)\n    /// Builds the image task request and sets it to a TV monogram view.\n    /// - Parameter monogramView: The monogram view which loads the task and should be set with the image.\n    /// - Returns: A task represents the image downloading, if initialized.\n    ///            This value is `nil` if the image is being loaded from cache.\n    @available(tvOS 12.0, *)\n    @discardableResult\n    public func set(to monogramView: TVMonogramView) -> DownloadTask? {\n        let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil\n        return monogramView.kf.setImage(\n            with: source,\n            placeholder: placeholderImage,\n            parsedOptions: options,\n            progressBlock: progressBlock,\n            completionHandler: resultHandler\n        )\n    }\n    #endif // end of canImport(TVUIKit)\n}\n\n#if !os(watchOS)\nextension KF.Builder {\n    #if os(iOS) || os(tvOS) || os(visionOS)\n\n    /// Sets a placeholder which is used while retrieving the image.\n    /// - Parameter placeholder: A placeholder to show while retrieving the image from its source.\n    /// - Returns: A ``KF/Builder`` with changes applied.\n    public func placeholder(_ placeholder: (any Placeholder)?) -> Self {\n        self.placeholder = placeholder\n        return self\n    }\n    #endif\n\n    /// Sets a placeholder image which is used while retrieving the image.\n    /// - Parameters:\n    ///   - image: An image to show while retrieving the image from its source.\n    /// - Returns: A ``KF/Builder`` with changes applied.\n    public func placeholder(_ image: KFCrossPlatformImage?) -> Self {\n        self.placeholder = image\n        return self\n    }\n}\n#endif\n\nextension KF.Builder {\n\n    #if os(iOS) || os(tvOS) || os(visionOS)\n    /// Sets the transition for the image task.\n    /// - Parameter transition: The desired transition effect when setting the image to image view.\n    /// - Returns: A ``KF/Builder`` with changes applied.\n    ///\n    /// Kingfisher will use the `transition` parameter to animate the image in if it is downloaded from web.\n    /// The transition will not happen when the image is retrieved from either memory or disk cache by default.\n    /// If you need to do the transition even when the image being retrieved from cache, also call\n    /// ``KFOptionSetter/forceRefresh(_:)`` on the returned ``KF/Builder``.\n    public func transition(_ transition: ImageTransition) -> Self {\n        options.transition = transition\n        return self\n    }\n\n    /// Sets a fade transition for the image task.\n    /// - Parameter duration: The duration of the fade transition.\n    /// - Returns: A ``KF/Builder`` with changes applied.\n    ///\n    /// Kingfisher will use the `transition` parameter to animate the image in if it is downloaded from web.\n    /// The transition will not happen when the image is retrieved from either memory or disk cache by default.\n    /// If you need to do the transition even when the image being retrieved from cache, also call\n    /// ``KFOptionSetter/forceRefresh(_:)`` on the returned ``KF/Builder``.\n    public func fade(duration: TimeInterval) -> Self {\n        options.transition = .fade(duration)\n        return self\n    }\n    #endif\n\n    /// Sets whether keeping the existing image of image view while setting another image to it.\n    /// - Parameter enabled: Whether the existing image should be kept.\n    /// - Returns: A ``KF/Builder`` with changes applied.\n    ///\n    /// By setting this option, the placeholder image parameter of image view extension method\n    /// will be ignored and the current image will be kept while loading or downloading the new image.\n    ///\n    public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self {\n        options.keepCurrentImageWhileLoading = enabled\n        return self\n    }\n\n    /// Sets whether only the first frame from an animated image file should be loaded as a single image.\n    /// - Parameter enabled: Whether the only the first frame should be loaded.\n    /// - Returns: A ``KF/Builder`` with changes applied.\n    ///\n    /// Loading an animated images may take too much memory. It will be useful when you want to display a\n    /// static preview of the first frame from an animated image.\n    ///\n    /// This option will be ignored if the target image is not animated image data.\n    ///\n    public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self {\n        options.onlyLoadFirstFrame = enabled\n        return self\n    }\n}\n\n// MARK: - Deprecated\nextension KF.Builder {\n    /// Starts the loading process of `self` immediately.\n    ///\n    /// By default, a ``KFImage`` will not load its source until the `onAppear` is called. This is a lazily loading\n    /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a\n    /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once\n    /// could help avoiding the flickering, with some performance trade-off.\n    ///\n    /// - Returns: The `Self` value with changes applied.\n    @available(*, deprecated, message: \"This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.\")\n    public func loadImmediately(_ start: Bool = true) -> Self {\n        return self\n    }\n}\n\n// MARK: - Redirect Handler\nextension KF {\n\n    /// Represents the detail information when a task redirect happens. It is wrapping necessary information for a\n    /// ``ImageDownloadRedirectHandler``. See that protocol for more information.\n    public struct RedirectPayload {\n\n        /// The related session data task when the redirect happens. It is\n        /// the current ``SessionDataTask`` which triggers this redirect.\n        public let task: SessionDataTask\n\n        /// The response received during redirection.\n        public let response: HTTPURLResponse\n\n        /// The request for redirection which can be modified.\n        public let newRequest: URLRequest\n\n        /// A closure for being called with modified request.\n        public let completionHandler: (URLRequest?) -> Void\n    }\n}\n"
  },
  {
    "path": "Sources/General/KFOptionsSetter.swift",
    "content": "//\n//  KFOptionsSetter.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/12/22.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport CoreGraphics\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// A protocol that Kingfisher can use to perform chained setting in builder pattern.\n@MainActor\npublic protocol KFOptionSetter {\n    var options: KingfisherParsedOptionsInfo { get nonmutating set }\n\n    var onFailureDelegate: Delegate<KingfisherError, Void> { get }\n    var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { get }\n    var onProgressDelegate: Delegate<(Int64, Int64), Void> { get }\n}\n\nextension KF.Builder: KFOptionSetter { }\n\nfinal actor KFDelegateObserver {\n    static let `default` = KFDelegateObserver()\n}\n\n// MARK: - Life cycles\nextension KFOptionSetter {\n    /// Sets the progress block to current builder.\n    ///\n    /// - Parameter block:\n    /// Called when the image downloading progress gets updated. If the response does not contain an\n    /// [`expectedContentLength`](https://developer.apple.com/documentation/foundation/urlresponse/1413507-expectedcontentlength)\n    /// in the received `URLResponse`, this block will not be called. If `block` is `nil`, the callback will be reset.\n    ///\n    /// - Returns: A `Self` value with changes applied.\n    ///\n    public func onProgress(_ block: DownloadProgressBlock?) -> Self {\n        onProgressDelegate.delegate(on: KFDelegateObserver.default) { (_, result) in\n            block?(result.0, result.1)\n        }\n        return self\n    }\n\n    /// Sets the done block to current builder.\n    /// - Parameter block: Called when the image task successfully completes and the image set is done. If `block`\n    ///                    is `nil`, the callback will be reset.\n    /// - Returns: A `Self` with changes applied.\n    ///\n    public func onSuccess(_ block: ((RetrieveImageResult) -> Void)?) -> Self {\n        onSuccessDelegate.delegate(on: KFDelegateObserver.default) { (_, result) in\n            block?(result)\n        }\n        return self\n    }\n\n    /// Sets the catch block to current builder.\n    /// - Parameter block: Called when an error happens during the image task. If `block`\n    ///                    is `nil`, the callback will be reset.\n    /// - Returns: A `Self` with changes applied.\n    ///\n    public func onFailure(_ block: ((KingfisherError) -> Void)?) -> Self {\n        onFailureDelegate.delegate(on: KFDelegateObserver.default) { (_, error) in\n            block?(error)\n        }\n        return self\n    }\n}\n\n// MARK: - Basic options settings.\nextension KFOptionSetter {\n\n    /// Sets the target image cache for this task.\n    ///\n    /// - Parameter cache: The target cache to be used for the task.\n    /// - Returns: A `Self` value with changes applied.\n    ///\n    /// Kingfisher will utilize the associated ``ImageCache`` object when performing related operations,\n    /// such as attempting to retrieve cached images and storing downloaded images within it.\n    ///\n    public func targetCache(_ cache: ImageCache) -> Self {\n        options.targetCache = cache\n        return self\n    }\n    \n    /// Sets the target image cache to store the original downloaded image for this task.\n    ///\n    /// - Parameter cache: The target cache is about to be used for storing the original downloaded image from the task.\n    /// - Returns: A `Self` value with changes applied.\n    ///\n    /// The ``ImageCache`` for storing and retrieving original images. If ``KingfisherOptionsInfoItem/originalCache(_:)``\n    /// is contained in the options, it will be preferred for storing and retrieving original images.\n    /// If there is no ``KingfisherOptionsInfoItem/originalCache(_:)`` in the options,\n    /// ``KingfisherOptionsInfoItem/targetCache(_:)`` will be used to store original images.\n    ///\n    /// When using ``KingfisherManager`` to download and store an image, if\n    /// ``KingfisherOptionsInfoItem/cacheOriginalImage`` is applied in the option, the original image will be stored to\n    /// the `cache` you pass as parameter in this method. At the same time, if a requested final image (with processor\n    /// applied) cannot be found in the cache defined by ``KingfisherOptionsInfoItem/targetCache(_:)``, Kingfisher\n    /// will try to search the original image to check whether it is already there. If found, it will be used and\n    /// applied with the given processor. It is an optimization for not downloading the same image for multiple times.\n    ///\n    public func originalCache(_ cache: ImageCache) -> Self {\n        options.originalCache = cache\n        return self\n    }\n\n    /// Sets the downloader to be used for the image download task.\n    ///\n    /// - Parameter downloader: The `ImageDownloader` instance to use for downloading.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// Kingfisher will utilize the specified ``ImageDownloader`` instance to download requested images.\n    ///\n    public func downloader(_ downloader: ImageDownloader) -> Self {\n        options.downloader = downloader\n        return self\n    }\n\n    /// Sets the download priority for the image task.\n    ///\n    /// - Parameter priority: The download priority of the image download task.\n    /// - Returns: A `Self` value with changes applied.\n    ///\n    /// The `priority` value will be configured as the priority of the image download task. Valid values range between \n    /// 0.0 and 1.0. You can select a value from `URLSessionTask.defaultPriority`, `URLSessionTask.lowPriority`,\n    /// or `URLSessionTask.highPriority`. If this option is not set, the default value\n    /// (`URLSessionTask.defaultPriority`) will be used.\n    ///\n    public func downloadPriority(_ priority: Float) -> Self {\n        options.downloadPriority = priority\n        return self\n    }\n\n    /// Sets whether Kingfisher should ignore the cache and attempt to initiate a download task for the image source.\n    ///\n    /// - Parameter enabled: Enable force refresh or not.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func forceRefresh(_ enabled: Bool = true) -> Self {\n        options.forceRefresh = enabled\n        return self\n    }\n\n    /// Sets whether Kingfisher should attempt to retrieve the image from the memory cache first. If the image is not \n    /// found in the memory cache, it bypasses the disk cache and initiates a download task for the image source.\n    ///\n    /// - Parameter enabled: Enable memory-only cache searching or not.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// This option is useful when you want to display a changeable image with the same URL during the same app session \n    /// while avoiding multiple downloads of the same image.\n    ///\n    public func fromMemoryCacheOrRefresh(_ enabled: Bool = true) -> Self {\n        options.fromMemoryCacheOrRefresh = enabled\n        return self\n    }\n\n    /// Sets whether the image should be cached only in memory and not on disk.\n    ///\n    /// - Parameter enabled: Enable memory-only caching for the image or not.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func cacheMemoryOnly(_ enabled: Bool = true) -> Self {\n        options.cacheMemoryOnly = enabled\n        return self\n    }\n\n    /// Sets whether Kingfisher should wait for caching operations to be completed before invoking the `onSuccess` \n    /// or `onFailure` block.\n    ///\n    /// - Parameter enabled: Enable waiting for caching operations or not.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func waitForCache(_ enabled: Bool = true) -> Self {\n        options.waitForCache = enabled\n        return self\n    }\n\n    /// Sets whether Kingfisher should exclusively attempt to retrieve the image from the cache and not from the network.\n    ///\n    /// - Parameter enabled: Enable cache-only image retrieval or not.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// If the image is not found in the cache, the image retrieval will fail with a\n    /// ``KingfisherError/CacheErrorReason/imageNotExisting(key:)`` error.\n    ///\n    public func onlyFromCache(_ enabled: Bool = true) -> Self {\n        options.onlyFromCache = enabled\n        return self\n    }\n\n    /// Sets whether the image should be decoded on a background thread before usage.\n    ///\n    /// - Parameter enabled: Enable background image decoding or not.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// When set to `true`, the downloaded image data will be decoded and undergo off-screen rendering to extract pixel \n    /// information in the background. This can enhance display speed but may consume additional time and memory for\n    /// image preparation before usage.\n    ///\n    public func backgroundDecode(_ enabled: Bool = true) -> Self {\n        options.backgroundDecode = enabled\n        return self\n    }\n\n    /// Sets the callback queue used as the target queue for dispatching callbacks when retrieving images from the \n    /// cache. If not set, Kingfisher will use the main queue for callbacks.\n    ///\n    /// - Parameter queue: The target queue on which cache retrieval callbacks will be invoked.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// - Note: This option does not impact callbacks for UI-related extension methods or ``KFImage`` result handlers. \n    /// Callbacks for those methods will always be executed on the main queue.\n    ///\n    public func callbackQueue(_ queue: CallbackQueue) -> Self {\n        options.callbackQueue = queue\n        return self\n    }\n\n    /// Sets the scale factor value used when converting retrieved data to an image.\n    ///\n    /// - Parameter factor: The scale factor value to use.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// Specify the image scale factor, which may differ from your screen's scale. This is particularly important when \n    /// working with 2x or 3x retina images. Failure to set the correct scale factor may result in Kingfisher\n    /// converting the data to an image object with a `scale` of 1.0.\n    ///\n    public func scaleFactor(_ factor: CGFloat) -> Self {\n        options.scaleFactor = factor\n        return self\n    }\n\n    /// Sets whether the original image should be cached, even when the original image has been processed by other ``ImageProcessor``s.\n    ///\n    /// - Parameter enabled: Whether to cache the original image.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// When this option is set, and an ``ImageProcessor`` is used, Kingfisher will attempt to cache both the final \n    /// processed image and the original image. This ensures that the original image can be reused when another\n    /// processor is applied to the same resource, without the need for redownloading. You can use\n    ///  ``KingfisherOptionsInfoItem/originalCache(_:)`` to specify a cache for the original images.\n    ///\n    /// - Note: The original image will be cached only in disk storage.\n    ///\n    public func cacheOriginalImage(_ enabled: Bool = true) -> Self {\n        options.cacheOriginalImage = enabled\n        return self\n    }\n\n    /// Sets writing options for an original image on its initial write to disk storage.\n    ///\n    /// - Parameter writingOptions: Options that control the data writing operation to disk storage.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// If these options are set, they will be applied to the storage operation for new files. This can be useful if \n    /// you want to implement features such as file encryption on the initial write, for example,\n    /// using `[.completeFileProtection]`.\n    ///\n    public func diskStoreWriteOptions(_ writingOptions: Data.WritingOptions) -> Self {\n        options.diskStoreWriteOptions = writingOptions\n        return self\n    }\n\n    /// Sets whether disk storage loading should occur in the same calling queue.\n    ///\n    /// - Parameter enabled: Whether disk storage loading should happen in the same calling queue.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// By default, disk storage file loading operates in its own queue with asynchronous dispatch behavior. While this \n    /// provides better non-blocking disk loading performance, it can result in flickering when reloading an image\n    /// from disk if the image view already has an image set.\n    ///\n    /// Enabling this option prevents flickering by performing all loading in the same queue (typically the UI queue if \n    /// you are using Kingfisher's extension methods to set an image). However, this may come at the cost of loading\n    /// performance.\n    ///\n    /// - Note: When using SwiftUI components (e.g., `KFImage`), this option is enabled by default to prevent \n    /// flickering during view updates. This is essential for maintaining visual consistency in SwiftUI's declarative\n    /// environment. For UIKit/AppKit usage, the default remains `false` for optimal performance.\n    ///\n    public func loadDiskFileSynchronously(_ enabled: Bool = true) -> Self {\n        options.loadDiskFileSynchronously = enabled\n        return self\n    }\n\n    /// Sets the queue on which image processing should occur.\n    ///\n    /// - Parameter queue: The queue on which image processing should take place.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// By default, Kingfisher employs a pre-defined serial queue for image processing. Use this option to modify this\n    ///  behavior. For example, specify `.mainCurrentOrAsync` to process the image on the main queue, which can prevent\n    ///  potential flickering but may lead to UI blocking if the processor requires substantial time to execute.\n    ///\n    public func processingQueue(_ queue: CallbackQueue?) -> Self {\n        options.processingQueue = queue\n        return self\n    }\n\n    /// Sets the alternative sources to be used when loading the original input `Source` fails.\n    ///\n    /// - Parameter sources: The alternative sources to be used.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// The values in the `sources` array will be employed to initiate a new image loading task if the previous task \n    /// fails due to an error. The image source loading process will terminate as soon as one of the alternative\n    /// sources is successfully loaded. If all `sources` are used but loading still fails,\n    /// a ``KingfisherError/ImageSettingErrorReason/alternativeSourcesExhausted(_:)`` error will be thrown in the\n    ///  `catch` block.\n    ///\n    /// This feature is valuable when implementing a fallback solution for setting images. \n    ///\n    /// - Note: User cancellation or calling on ``DownloadTask/cancel()`` on ``DownloadTask`` will not trigger the\n    /// loading of alternative sources.\n    ///\n    public func alternativeSources(_ sources: [Source]?) -> Self {\n        options.alternativeSources = sources\n        return self\n    }\n\n    /// Sets a retry strategy to be used when issues arise during image retrieval.\n    ///\n    /// - Parameter strategy: The provided strategy that defines how retry attempts should occur.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func retry(_ strategy: (any RetryStrategy)?) -> Self {\n        options.retryStrategy = strategy\n        return self\n    }\n\n    /// Sets a retry strategy with a maximum retry count and retry interval.\n    ///\n    /// - Parameters:\n    ///   - maxCount: The maximum number of retry attempts before the retry stops.\n    ///   - interval: The time interval between each retry attempt.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// This defines a straightforward retry strategy that retries a failing request for a specified number of times \n    /// with a designated time interval between each attempt. For example, `.retry(maxCount: 3, interval: .second(3))`\n    /// indicates a maximum of three retry attempts, with a 3-second pause between each retry if the previous attempt\n    /// fails.\n    ///\n    public func retry(maxCount: Int, interval: DelayRetryStrategy.Interval = .seconds(3)) -> Self {\n        let strategy = DelayRetryStrategy(maxRetryCount: maxCount, retryInterval: interval)\n        options.retryStrategy = strategy\n        return self\n    }\n\n    /// Sets the `Source` to be loaded when the user enables Low Data Mode and the original source fails with an\n    ///  `NSURLErrorNetworkUnavailableReason.constrained` error.\n    ///\n    /// - Parameter source: The `Source` to be loaded under low data mode.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// When this option is set, the `allowsConstrainedNetworkAccess` property of the request for the original source \n    /// will be set to `false`, and the specified ``Source`` will be used to retrieve the image in low data mode.\n    /// Typically, you can provide a low-resolution version of your image or a local image provider to display a\n    /// placeholder.\n    ///\n    /// If this option is not set or the `source` is `nil`, the device's Low Data Mode setting will be disregarded, \n    /// and the original source will be loaded following the system's default behavior in a regular manner.\n    ///\n    public func lowDataModeSource(_ source: Source?) -> Self {\n        options.lowDataModeSource = source\n        return self\n    }\n\n    /// Sets whether the image setting for an image view should include a transition even when the image is retrieved \n    /// from the cache.\n    ///\n    /// - Parameter enabled: Enable the use of a transition or not.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func forceTransition(_ enabled: Bool = true) -> Self {\n        options.forceTransition = enabled\n        return self\n    }\n\n    /// Sets the image to be used in the event of a failure during image retrieval.\n    ///\n    /// - Parameter image: The image to be used when an error occurs.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// If this option is set and an image retrieval error occurs, Kingfisher will use the provided image (or an empty \n    /// image) in place of the requested one. This is useful when you do not want to display a placeholder during the\n    ///  loading process but prefer to use a default image when requests fail.\n    ///\n    public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self {\n        options.onFailureImage = .some(image)\n        return self\n    }\n}\n\n// MARK: - Request Modifier\nextension KFOptionSetter {\n    \n    /// Sets an ``ImageDownloadRequestModifier`` to alter the image download request before it is sent.\n    ///\n    /// - Parameter modifier: The modifier to be used for changing the request before it is sent.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// This is your last opportunity to modify the image download request. You can use this for customization\n    /// purposes, such as adding an authentication token to the header, implementing basic HTTP authentication,\n    /// or URL mapping.\n    public func requestModifier(_ modifier: any AsyncImageDownloadRequestModifier) -> Self {\n        options.requestModifier = modifier\n        return self\n    }\n\n    /// Sets a block to modify the image download request before it is sent.\n    ///\n    /// - Parameter modifyBlock: The modifying block that will be called to change the request before it is sent.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// This is your last opportunity to modify the image download request. You can use this for customization purposes,\n    /// such as adding an authentication token to the header, implementing basic HTTP authentication, or URL mapping.\n    ///\n    public func requestModifier(_ modifyBlock: @escaping @Sendable (inout URLRequest) -> Void) -> Self {\n        options.requestModifier = AnyModifier { r -> URLRequest? in\n            var request = r\n            modifyBlock(&request)\n            return request\n        }\n        return self\n    }\n}\n\n// MARK: - Redirect Handler\nextension KFOptionSetter {\n    \n    /// Sets an `ImageDownloadRedirectHandler` to modify the image download request during redirection.\n    ///\n    /// - Parameter handler: The handler to be used for redirection.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// This provides an opportunity to modify the image download request during redirection. You can use this for \n    /// customization purposes, such as adding an authentication token to the header, implementing basic HTTP\n    /// authentication, or URL mapping. By default, the original redirection request will be sent without any\n    /// modification.\n    ///\n    public func redirectHandler(_ handler: any ImageDownloadRedirectHandler) -> Self {\n        options.redirectHandler = handler\n        return self\n    }\n\n    /// Sets a block to modify the image download request during redirection.\n    ///\n    /// - Parameter block: The block to be used for redirection.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// This provides an opportunity to modify the image download request during redirection. You can use this for \n    /// customization purposes, such as adding an authentication token to the header, implementing basic HTTP\n    /// authentication, or URL mapping. By default, the original redirection request will be sent without any\n    /// modification.\n    ///\n    public func redirectHandler(_ block: @escaping @Sendable (KF.RedirectPayload) -> Void) -> Self {\n        let redirectHandler = AnyRedirectHandler { (task, response, request, handler) in\n            let payload = KF.RedirectPayload(\n                task: task, response: response, newRequest: request, completionHandler: handler\n            )\n            block(payload)\n        }\n        options.redirectHandler = redirectHandler\n        return self\n    }\n}\n\n// MARK: - Processor\nextension KFOptionSetter {\n\n    /// Sets an image processor for the image task, replacing the current image processor settings.\n    ///\n    /// - Parameter processor: The processor to use for processing the image after it is downloaded.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// - Note: To append a processor to the current ones instead of replacing them all, use ``appendProcessor(_:)``.\n    ///\n    public func setProcessor(_ processor: any ImageProcessor) -> Self {\n        options.processor = processor\n        return self\n    }\n    \n    /// Enables progressive image loading with a specified `ImageProgressive` setting to process the\n    /// progressive JPEG data and display it in a progressive way.\n    /// - Parameter progressive: The progressive settings which is used while loading.\n    /// - Returns: A ``KF/Builder`` with changes applied.\n    public func progressiveJPEG(_ progressive: ImageProgressive? = .init()) -> Self {\n        options.progressiveJPEG = progressive\n        return self\n    }\n\n    /// Sets an array of image processors for the image task, replacing the current image processor settings.\n    ///\n    /// - Parameter processors: An array of processors. The processors in this array will be concatenated one by one to \n    /// form a processor pipeline.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// - Note: To append processors to the current ones instead of replacing them all, concatenate them using the\n    /// `|>` operator, and then use ``KFOptionSetter/appendProcessor(_:)``.\n    ///\n    public func setProcessors(_ processors: [any ImageProcessor]) -> Self {\n        switch processors.count {\n        case 0:\n            options.processor = DefaultImageProcessor.default\n        case 1...:\n            options.processor = processors.dropFirst().reduce(processors[0]) { $0 |> $1 }\n        default:\n            assertionFailure(\"Never happen\")\n        }\n        return self\n    }\n\n    /// Appends a processor to the current set of processors.\n    ///\n    /// - Parameter processor: The processor to append to the current processor settings.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func appendProcessor(_ processor: any ImageProcessor) -> Self {\n        options.processor = options.processor |> processor\n        return self\n    }\n\n    /// Appends a ``RoundCornerImageProcessor`` to the current set of processors.\n    ///\n    /// - Parameters:\n    ///   - radius: The radius to apply during processing. Specify a certain point value with `.point`, or a fraction \n    ///   of the target image with `.widthFraction` or `.heightFraction`. For example, with a square image where width\n    ///   and height are equal, `.widthFraction(0.5)` means using half of the length of the size to make the final\n    ///   image round.\n    ///   - targetSize: The target size for the output image. If `nil`, the image will retain its original size after \n    ///   processing.\n    ///   - corners: The target corners to round.\n    ///   - backgroundColor: The background color of the output image. If `nil`, a transparent background will be used.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func roundCorner(\n        radius: Radius,\n        targetSize: CGSize? = nil,\n        roundingCorners corners: RectCorner = .all,\n        backgroundColor: KFCrossPlatformColor? = nil\n    ) -> Self\n    {\n        let processor = RoundCornerImageProcessor(\n            radius: radius,\n            targetSize: targetSize,\n            roundingCorners: corners,\n            backgroundColor: backgroundColor\n        )\n        return appendProcessor(processor)\n    }\n\n    /// Appends a ``BlurImageProcessor`` to the current set of processors.\n    ///\n    /// - Parameter radius: The blur radius for simulating Gaussian blur.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func blur(radius: CGFloat) -> Self {\n        appendProcessor(\n            BlurImageProcessor(blurRadius: radius)\n        )\n    }\n\n    /// Appends an ``OverlayImageProcessor`` to the current set of processors.\n    ///\n    /// - Parameters:\n    ///   - color: The overlay color to be used when overlaying the input image.\n    ///   - fraction: The fraction to be used when overlaying the color onto the image.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func overlay(color: KFCrossPlatformColor, fraction: CGFloat = 0.5) -> Self {\n        appendProcessor(\n            OverlayImageProcessor(overlay: color, fraction: fraction)\n        )\n    }\n\n    /// Appends a ``TintImageProcessor`` to the current set of processors.\n    ///\n    /// - Parameter color: The tint color to be used for tinting the input image.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func tint(color: KFCrossPlatformColor) -> Self {\n        appendProcessor(\n            TintImageProcessor(tint: color)\n        )\n    }\n\n    /// Appends a ``BlackWhiteProcessor`` to the current set of processors.\n    ///\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func blackWhite() -> Self {\n        appendProcessor(\n            BlackWhiteProcessor()\n        )\n    }\n\n    /// Appends a ``CroppingImageProcessor`` to the current set of processors.\n    ///\n    /// - Parameters:\n    ///   - size: The target size for the output image.\n    ///   - anchor: The anchor point from which the output size should be calculated. The anchor point is represented \n    ///   by two values between 0.0 and 1.0, indicating a relative point in the current image. See\n    ///    ``CroppingImageProcessor/init(size:anchor:)`` for more details.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func cropping(size: CGSize, anchor: CGPoint = .init(x: 0.5, y: 0.5)) -> Self {\n        appendProcessor(\n            CroppingImageProcessor(size: size, anchor: anchor)\n        )\n    }\n\n    /// Appends a ``DownsamplingImageProcessor`` to the current set of processors.\n    ///\n    /// Compared to the ``ResizingImageProcessor``, the ``DownsamplingImageProcessor`` doesn't render the original \n    /// images and then resize them. Instead, it directly downsamples the input data to a thumbnail image, making it\n    /// more efficient than the ``ResizingImageProcessor``. It is recommended to use the ``DownsamplingImageProcessor``\n    /// whenever possible instead of the ``ResizingImageProcessor``.\n    ///\n    /// - Parameter size: The target size for the output image. It should be smaller than the size of the input image. If it is larger, the resulting image will be the same size as the input data without downsampling.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    /// - Note: Only CG-based images are supported, and animated images (e.g., GIF) are not supported.\n    ///\n    public func downsampling(size: CGSize) -> Self {\n        let processor = DownsamplingImageProcessor(size: size)\n        if options.processor == DefaultImageProcessor.default {\n            return setProcessor(processor)\n        } else {\n            return appendProcessor(processor)\n        }\n    }\n\n    /// Appends a ``ResizingImageProcessor`` to the current set of processors.\n    ///\n    /// If you need to resize a data-represented image to a smaller size, it is recommended to use the\n    /// ``DownsamplingImageProcessor`` instead, which is more efficient and uses less memory.\n    ///\n    /// - Parameters:\n    ///   - referenceSize: The reference size for the resizing operation in points.\n    ///   - mode: The target content mode for the output image. The default is `.none`.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func resizing(referenceSize: CGSize, mode: ContentMode = .none) -> Self {\n        appendProcessor(\n            ResizingImageProcessor(referenceSize: referenceSize, mode: mode)\n        )\n    }\n}\n\n// MARK: - Cache Serializer\nextension KFOptionSetter {\n\n    /// Uses a specified ``CacheSerializer`` to convert data to an image object for retrieval from the disk cache or\n    ///  vice versa for storage to the disk cache.\n    ///\n    /// - Parameter cacheSerializer: The ``CacheSerializer`` to be used.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func serialize(by cacheSerializer: any CacheSerializer) -> Self {\n        options.cacheSerializer = cacheSerializer\n        return self\n    }\n\n    /// Uses a specified format to serialize the image data to disk. It converts the image object to the given data \n    /// format.\n    ///\n    /// - Parameters:\n    ///   - format: The desired data encoding format when storing the image on disk.\n    ///   - jpegCompressionQuality: If the format is ``ImageFormat/JPEG``, it specifies the compression quality when \n    ///   converting the image to JPEG data. Otherwise, it is ignored.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func serialize(as format: ImageFormat, jpegCompressionQuality: CGFloat? = nil) -> Self {\n        let cacheSerializer: FormatIndicatedCacheSerializer\n        switch format {\n        case .JPEG:\n            cacheSerializer = .jpeg(compressionQuality: jpegCompressionQuality ?? 1.0)\n        case .PNG:\n            cacheSerializer = .png\n        case .GIF:\n            cacheSerializer = .gif\n        case .unknown:\n            cacheSerializer = .png\n        }\n        options.cacheSerializer = cacheSerializer\n        return self\n    }\n}\n\n// MARK: - Image Modifier\nextension KFOptionSetter {\n\n    /// Sets an ``ImageModifier`` for the image task. Use this to modify the fetched image object's properties if needed.\n    ///\n    /// If the image was fetched directly from the downloader, the modifier will run directly after the \n    /// ``ImageProcessor``. If the image is being fetched from a cache, the modifier will run after the \n    /// ``CacheSerializer``.\n    ///\n    /// - Parameter modifier: The ``ImageModifier`` to be used for modifying the image object.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func imageModifier(_ modifier: (any ImageModifier)?) -> Self {\n        options.imageModifier = modifier\n        return self\n    }\n\n    /// Sets a block to modify the image object. Use this to modify the fetched image object's properties if needed.\n    ///\n    /// If the image was fetched directly from the downloader, the modifier block will run directly after the \n    /// ``ImageProcessor``. If the image is being fetched from a cache, the modifier will run after the\n    /// ``CacheSerializer``.\n    ///\n    /// - Parameter block: The block used to modify the image object.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func imageModifier(_ block: @escaping @Sendable (inout KFCrossPlatformImage) throws -> Void) -> Self {\n        let modifier = AnyImageModifier { image -> KFCrossPlatformImage in\n            var image = image\n            try block(&image)\n            return image\n        }\n        options.imageModifier = modifier\n        return self\n    }\n}\n\n\n// MARK: - Cache Expiration\nextension KFOptionSetter {\n\n    /// Sets the expiration setting for the memory cache of this image task.\n    ///\n    /// By default, the underlying ``MemoryStorage/Backend`` uses the expiration in its configuration for all items. \n    /// If set, the ``MemoryStorage/Backend`` will use this value to overwrite the configuration setting for this\n    /// caching item.\n    ///\n    /// - Parameter expiration: The expiration setting used in cache storage.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func memoryCacheExpiration(_ expiration: StorageExpiration?) -> Self {\n        options.memoryCacheExpiration = expiration\n        return self\n    }\n\n    /// Sets the expiration extending setting for the memory cache. The item expiration time will be incremented by this \n    /// value after access.\n    ///\n    /// By default, the underlying ``MemoryStorage/Backend`` uses the initial cache expiration as the extending value: \n    /// ``ExpirationExtending/cacheTime``.\n    ///\n    /// To disable the extending option entirely, set `.none` to it.\n    ///\n    /// - Parameter extending: The expiration extending setting used in cache storage.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func memoryCacheAccessExtending(_ extending: ExpirationExtending) -> Self {\n        options.memoryCacheAccessExtendingExpiration = extending\n        return self\n    }\n\n    /// Sets the expiration setting for the disk cache of this image task.\n    ///\n    /// By default, the underlying ``DiskStorage/Backend`` uses the expiration in its configuration for all items. \n    /// If set, the ``DiskStorage/Backend`` will use this value to overwrite the configuration setting for this caching\n    /// item.\n    ///\n    /// - Parameter expiration: The expiration setting used in cache storage.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func diskCacheExpiration(_ expiration: StorageExpiration?) -> Self {\n        options.diskCacheExpiration = expiration\n        return self\n    }\n\n    /// Sets the expiration extending setting for the disk cache. The item expiration time will be incremented by this \n    /// value after access.\n    ///\n    /// By default, the underlying ``DiskStorage/Backend`` uses the initial cache expiration as the extending\n    ///  value: ``ExpirationExtending/cacheTime``.\n    ///\n    /// To disable the extending option entirely, set `.none` to it.\n    ///\n    /// - Parameter extending: The expiration extending setting used in cache storage.\n    /// - Returns: A `Self` value with the changes applied.\n    ///\n    public func diskCacheAccessExtending(_ extending: ExpirationExtending) -> Self {\n        options.diskCacheAccessExtendingExpiration = extending\n        return self\n    }\n}\n"
  },
  {
    "path": "Sources/General/Kingfisher.swift",
    "content": "//\n//  Kingfisher.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 16/9/14.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport ImageIO\n\n#if os(macOS)\nimport AppKit\npublic typealias KFCrossPlatformImage       = NSImage\npublic typealias KFCrossPlatformView        = NSView\npublic typealias KFCrossPlatformColor       = NSColor\npublic typealias KFCrossPlatformImageView   = NSImageView\npublic typealias KFCrossPlatformButton      = NSButton\n\n// `NSImage` is not yet Sendable. We have to assume it sendable to resolve warnings in Kingfisher.\n#if compiler(>=6)\nextension KFCrossPlatformImage: @retroactive @unchecked Sendable { }\n#else\nextension KFCrossPlatformImage: @unchecked Sendable { }\n#endif // compiler(>=6)\n#else // os(macOS)\nimport UIKit\npublic typealias KFCrossPlatformImage       = UIImage\npublic typealias KFCrossPlatformColor       = UIColor\n#if !os(watchOS)\npublic typealias KFCrossPlatformImageView   = UIImageView\npublic typealias KFCrossPlatformView        = UIView\npublic typealias KFCrossPlatformButton      = UIButton\n#if canImport(TVUIKit)\nimport TVUIKit\n#endif // canImport(TVUIKit)\n#if canImport(CarPlay) && !targetEnvironment(macCatalyst)\nimport CarPlay\n#endif // canImport(CarPlay) && !targetEnvironment(macCatalyst)\n#else // !os(watchOS)\nimport WatchKit\n#endif // !os(watchOS)\n#endif // os(macOS)\n\n/// Wrapper for Kingfisher compatible types. This type provides an extension point for\n/// convenience methods in Kingfisher.\npublic struct KingfisherWrapper<Base>: @unchecked Sendable {\n    public let base: Base\n    public init(_ base: Base) {\n        self.base = base\n    }\n}\n\n/// Represents an object type that is compatible with Kingfisher. You can use ``kf`` property to get a\n/// value in the namespace of Kingfisher.\n///\n/// In Kingfisher, most of related classes that contains an image (such as `UIImage`, `UIButton`, `NSImageView` and\n/// more) conform to this protocol, and provides the helper methods for setting an image easily. You can access the `kf`\n/// property and call its `setImage` method with a certain URL:\n///\n/// ```swift\n/// let imageView: UIImageView\n/// let url = URL(string: \"https://example.com/image.jpg\")\n/// imageView.kf.setImage(with: url)\n/// ```\n///\n/// For more about basic usage of Kingfisher, check the <doc:CommonTasks> documentation.\npublic protocol KingfisherCompatible: AnyObject { }\n\n/// Represents a value type that is compatible with Kingfisher. You can use ``kf`` property to get a\n/// value in the namespace of Kingfisher.\npublic protocol KingfisherCompatibleValue {}\n\nextension KingfisherCompatible {\n    /// Gets a namespace holder for Kingfisher compatible types.\n    public var kf: KingfisherWrapper<Self> {\n        get { return KingfisherWrapper(self) }\n        set { }\n    }\n}\n\nextension KingfisherCompatibleValue {\n    /// Gets a namespace holder for Kingfisher compatible types.\n    public var kf: KingfisherWrapper<Self> {\n        get { return KingfisherWrapper(self) }\n        set { }\n    }\n}\n\nextension KFCrossPlatformImage      : KingfisherCompatible { }\n#if !os(watchOS)\nextension KFCrossPlatformImageView  : KingfisherCompatible { }\nextension KFCrossPlatformButton     : KingfisherCompatible { }\nextension NSTextAttachment          : KingfisherCompatible { }\n#else\nextension WKInterfaceImage          : KingfisherCompatible { }\n#endif\n\n#if canImport(PhotosUI) && !os(watchOS)\nimport PhotosUI\nextension PHLivePhotoView           : KingfisherCompatible { }\n#endif\n\n\n#if os(tvOS) && canImport(TVUIKit)\n@available(tvOS 12.0, *)\nextension TVMonogramView            : KingfisherCompatible { }\n#endif\n\n#if canImport(CarPlay) && !targetEnvironment(macCatalyst)\n@available(iOS 14.0, *)\nextension CPListItem                : KingfisherCompatible { }\n#endif\n"
  },
  {
    "path": "Sources/General/KingfisherError.swift",
    "content": "//\n//  KingfisherError.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/09/26.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\nextension Never {}\n\n/// Represents all the errors that can occur in the Kingfisher framework.\n///\n/// Kingfisher-related methods always throw a ``KingfisherError`` or invoke the callback with ``KingfisherError``\n/// as its error type. To handle errors from Kingfisher, you switch over the error to get a reason catalog,\n/// then switch over the reason to understand the error details.\n///\npublic enum KingfisherError: Error {\n\n    // MARK: Error Reason Types\n\n    /// Represents the error reasons during the networking request phase.\n    public enum RequestErrorReason: Sendable {\n        \n        /// The request is empty.\n        ///\n        /// Error Code: 1001\n        case emptyRequest\n        \n        /// The URL of the request is invalid.\n        ///\n        /// - Parameter request: The request is intended to be sent, but its URL is invalid.\n        ///\n        /// Error Code: 1002\n        case invalidURL(request: URLRequest)\n\n        /// The downloading task is canceled by the user.\n        ///\n        /// - Parameters:\n        ///   - task: The session data task which is canceled.\n        ///   - token: The cancel token which is used for canceling the task.\n        ///\n        /// Error Code: 1003\n        case taskCancelled(task: SessionDataTask, token: SessionDataTask.CancelToken)\n\n        /// The live photo downloading task is canceled by the user.\n        ///\n        /// - Parameters:\n        ///   - source: The live phot source.\n        ///\n        /// Error Code: 1004\n        case livePhotoTaskCancelled(source: LivePhotoSource)\n        \n        case asyncTaskContextCancelled\n    }\n    \n    /// Represents the error reason during networking response phase.\n    public enum ResponseErrorReason: Sendable {\n        \n        /// The response is not a valid URL response.\n        ///\n        /// - Parameters:\n        ///   - response: The received invalid URL response.\n        ///               The response is expected to be an HTTP response, but it is not.\n        ///\n        /// Error Code: 2001\n        case invalidURLResponse(response: URLResponse)\n        \n        /// The response contains an invalid HTTP status code.\n        ///\n        /// - Parameters:\n        ///   - response: The received response.\n        ///\n        /// Error Code: 2002\n        ///\n        /// - Note: By default, status code 200..<400 is recognized as valid. You can override\n        ///   this behavior by conforming to the `ImageDownloaderDelegate`.\n        case invalidHTTPStatusCode(response: HTTPURLResponse)\n        \n        /// An error happens in the system URL session.\n        ///\n        /// - Parameters:\n        ///   - error: The underlying URLSession error object.\n        ///\n        /// Error Code: 2003\n        case URLSessionError(error: any Error)\n        \n        /// Data modifying fails on returning a valid data.\n        ///\n        /// - Parameters:\n        ///   - task: The failed task.\n        ///\n        ///   Error Code: 2004\n        case dataModifyingFailed(task: SessionDataTask)\n        \n        /// The task is done but no URL response found.\n        ///\n        /// - Parameters:\n        ///   - task: The failed task.\n        ///\n        /// Error Code: 2005\n        case noURLResponse(task: SessionDataTask)\n\n        /// The task is cancelled by ``ImageDownloaderDelegate`` due to the `.cancel` response disposition is\n        /// specified by the delegate method.\n        ///\n        /// - Parameters:\n        ///   - task: The cancelled task.\n        ///\n        /// Error Code: 2006\n        case cancelledByDelegate(response: URLResponse)\n    }\n    \n    /// Represents the error reason during Kingfisher caching.\n    public enum CacheErrorReason: @unchecked Sendable {\n        \n        /// Cannot create a file enumerator for a certain disk URL.\n        ///\n        /// - Parameters:\n        ///   - url: The target disk URL from which the file enumerator should be created.\n        ///\n        /// Error Code: 3001\n        case fileEnumeratorCreationFailed(url: URL)\n        \n        /// Cannot get correct file contents from a file enumerator.\n        ///\n        /// - Parameters:\n        ///   - url: The target disk URL from which the content of a file enumerator should be obtained.\n        ///\n        /// Error Code: 3002\n        case invalidFileEnumeratorContent(url: URL)\n        \n        /// The file at the target URL exists, but its URL resource is unavailable.\n        ///\n        /// - Parameters:\n        ///   - error: The underlying error thrown by the file manager.\n        ///   - key: The key used to retrieve the resource from cache.\n        ///   - url: The disk URL where the target cached file exists.\n        ///\n        /// Error Code: 3003\n        case invalidURLResource(error: any Error, key: String, url: URL)\n        \n        /// The file at the target URL exists, but the data cannot be loaded from it.\n        ///\n        /// - Parameters:\n        ///   - url: The disk URL where the target cached file exists.\n        ///   - error: The underlying error that describes why this error occurs.\n        ///\n        /// Error Code: 3004\n        case cannotLoadDataFromDisk(url: URL, error: any Error)\n        \n        /// Cannot create a folder at a given path.\n        ///\n        /// - Parameters:\n        ///   - path: The disk path where the directory creation operation fails.\n        ///   - error: The underlying error that describes why this error occurs.\n        ///\n        /// Error Code: 3005\n        case cannotCreateDirectory(path: String, error: any Error)\n        \n        /// The requested image does not exist in the cache.\n        ///\n        /// - Parameters:\n        ///   - key: The key of the requested image in the cache.\n        ///\n        /// Error Code: 3006\n        case imageNotExisting(key: String)\n        \n        /// Unable to convert an object to data for storage.\n        ///\n        /// - Parameters:\n        ///   - object: The object that needs to be converted to data.\n        ///\n        /// Error Code: 3007\n        case cannotConvertToData(object: Any, error: any Error)\n        \n        /// Unable to serialize an image to data for storage.\n        ///\n        /// - Parameters:\n        ///   - image: The input image that needs to be serialized to cache.\n        ///   - original: The original image data, if it exists.\n        ///   - serializer: The ``CacheSerializer`` used for the image serialization.\n        ///\n        /// Error Code: 3008\n        case cannotSerializeImage(image: KFCrossPlatformImage?, original: Data?, serializer: any CacheSerializer)\n\n        /// Unable to create the cache file at a specified `fileURL` under a given `key`.\n        ///\n        /// - Parameters:\n        ///   - fileURL: The URL where the cache file should be created.\n        ///   - key: The cache key used for the cache. When caching a file through ``KingfisherManager`` and Kingfisher's\n        ///          extension method, it is the resolved cache key based on your input ``Source`` and the image\n        ///          processors.\n        ///   - data: The data to be cached.\n        ///   - error: The underlying error originally thrown by Foundation when attempting to write the `data` to the disk file at\n        ///            `fileURL`.\n        ///\n        /// Error Code: 3009\n        case cannotCreateCacheFile(fileURL: URL, key: String, data: Data, error: any Error)\n\n        /// Unable to set file attributes for a cached file.\n        ///\n        /// - Parameters:\n        ///   - filePath: The path of the target cache file.\n        ///   - attributes: The file attributes to be set for the target file.\n        ///   - error: The underlying error originally thrown by the Foundation framework when attempting to set the specified\n        ///            `attributes` for the disk file at `filePath`.\n        ///\n        /// Error Code: 3010\n        case cannotSetCacheFileAttribute(filePath: String, attributes: [FileAttributeKey : Any], error: any Error)\n\n        \n        /// The disk storage for caching is not ready.\n        ///\n        /// - Parameters:\n        ///   - cacheURL: The intended URL that should be the storage folder.\n        ///\n        /// This issue typically arises due to an extreme lack of space on the disk storage. Kingfisher fails to create \n        /// the cache folder under these circumstances, rendering the disk storage unusable. In such cases, it is\n        /// recommended to prompt the user to free up storage space and restart the app to restore functionality.\n        ///\n        /// Error Code: 3011\n        case diskStorageIsNotReady(cacheURL: URL)\n        \n        /// The resource is expected on the disk, but now missing for some reason.\n        ///\n        /// This happens when the expected resource is not on the disk for some reason during loading a live photo.\n        ///\n        /// Error Code: 3012\n        case missingLivePhotoResourceOnDisk(_ resource: LivePhotoResource)\n    }\n    \n    /// Represents the error reason during image processing phase.\n    public enum ProcessorErrorReason: Sendable {\n        /// Image processing has failed, and there is no valid output image generated by the processor.\n        ///\n        /// - Parameters:\n        ///   - processor: The `ImageProcessor` responsible for processing the image or its data in `item`.\n        ///   - item: The image or its data content.\n        ///\n        /// Error Code: 4001\n        case processingFailed(processor: any ImageProcessor, item: ImageProcessItem)\n    }\n\n    /// Represents the error reason during image setting in a view related class.\n    public enum ImageSettingErrorReason: Sendable {\n        \n        /// The input resource is empty or `nil`.\n        ///\n        /// Error Code: 5001\n        case emptySource\n        \n        /// The resource task is completed, but it is not the one that was expected. This typically occurs when you set \n        /// another resource on the view without canceling the current ongoing task. The previous task will fail with the\n        /// `.notCurrentSourceTask` error when a result is obtained, regardless of whether it was successful or not for\n        /// that task.\n        ///\n        /// - Parameters:\n        ///   - result: The `RetrieveImageResult` if the source task is completed without any issues. `nil` if an error occurred.\n        ///   - error: The `Error` if there was a problem during the image setting task. `nil` if the task completed successfully.\n        ///   - source: The original source value of the task.\n        ///\n        /// Error Code: 5002\n        case notCurrentSourceTask(result: RetrieveImageResult?, error: (any Error)?, source: Source)\n\n        /// An error occurs while retrieving data from an `ImageDataProvider`.\n        ///\n        /// - Parameters:\n        ///   - provider: The ``ImageDataProvider`` that encountered the error.\n        ///   - error: The underlying error that describes why this error occurred.\n        ///\n        /// Error Code: 5003\n        case dataProviderError(provider: any ImageDataProvider, error: any Error)\n\n        /// No more alternative ``Source`` can be used in current loading process. It means that the\n        /// ``KingfisherOptionsInfoItem/alternativeSources(_:)`` are set and Kingfisher tried to recovery from the original error, but still\n        /// fails for all the given alternative sources. The associated value holds all the errors encountered during\n        /// the load process, including the original source loading error and all the alternative sources errors.\n        /// Code 5004.\n        \n        /// No more alternative `Source` can be used in the current loading process. \n        ///\n        /// - Parameters:\n        ///   - error : A ``PropagationError`` contains more information about the source and error.\n        ///\n        /// This means that the ``KingfisherOptionsInfoItem/alternativeSources(_:)`` option is set, and Kingfisher attempted to recover from the original error,\n        /// but still failed for all the provided alternative sources. The associated value holds all the errors encountered during\n        /// the loading process, including the original source loading error and all the alternative sources errors.\n        ///\n        /// Error Code: 5004\n        case alternativeSourcesExhausted([PropagationError])\n        \n        /// The resource task is completed, but it is not the one that was expected. This typically occurs when you set\n        /// another resource on the view without canceling the current ongoing task. The previous task will fail with the\n        /// `.notCurrentLivePhotoSourceTask` error when a result is obtained, regardless of whether it was successful or\n        /// not for that task.\n        ///\n        /// This error is the live photo version of the `.notCurrentSourceTask` error (error 5002).\n        ///\n        /// - Parameters:\n        ///   - result: The `RetrieveImageResult` if the source task is completed without any issues. `nil` if an error occurred.\n        ///   - error: The `Error` if there was a problem during the image setting task. `nil` if the task completed successfully.\n        ///   - source: The original source value of the task.\n        ///\n        /// Error Code: 5005\n        case notCurrentLivePhotoSourceTask(\n            result: RetrieveLivePhotoResult?, error: (any Error)?, source: LivePhotoSource\n        )\n        \n        /// The error happens during processing the live photo.\n        ///\n        /// When creating the final `PHLivePhoto` object from the downloaded image files, the internal Photos framework\n        /// method `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)`\n        /// invokes its `resultHandler`. If the `info` dictionary in `resultHandler` contains `PHLivePhotoInfoErrorKey`,\n        /// Kingfisher raises this error reason to pass the information to outside.\n        ///\n        /// If the processing fails due to any error that is not a `KingfisherError` case, Kingfisher also reports it\n        /// with this reason.\n        ///\n        /// - Parameters:\n        ///   - result: The `RetrieveLivePhotoResult` if the source task is completed and a result is already existing.\n        ///   - error: The `NSError` if `PHLivePhotoInfoErrorKey` is contained in the `resultHandler` info dictionary.\n        ///   - source: The original source value of the task.\n        ///\n        /// - Note: It is possible that both `result` and `error` are non-nil value. Check the\n        /// ``RetrieveLivePhotoResult/info`` property for the raw values that are from the Photos framework.\n        ///\n        /// Error Code: 5006\n        case livePhotoResultError(result: RetrieveLivePhotoResult?, error: (any Error)?, source: LivePhotoSource)\n    }\n\n    // MARK: Member Cases\n    \n    /// Represents the error reasons that can occur during the networking request phase.\n    case requestError(reason: RequestErrorReason)\n    /// Represents the error reason that can occur during networking response phase.\n    case responseError(reason: ResponseErrorReason)\n    /// Represents the error reason that can occur during Kingfisher caching phase.\n    case cacheError(reason: CacheErrorReason)\n    /// Represents the error reason that can occur during image processing phase.\n    case processorError(reason: ProcessorErrorReason)\n    /// Represents the error reason that can occur during image setting in a view related class.\n    case imageSettingError(reason: ImageSettingErrorReason)\n\n    // MARK: Helper Properties & Methods\n\n    /// A helper property to determine if this error is of type `RequestErrorReason.taskCancelled`.\n    public var isTaskCancelled: Bool {\n        if case .requestError(reason: .taskCancelled) = self {\n            return true\n        }\n        return false\n    }\n\n    /// Helper method to check whether this error is a ``ResponseErrorReason/invalidHTTPStatusCode(response:)``\n    /// and the associated value is a given status code.\n    ///\n    /// - Parameter code: The given status code.\n    /// - Returns: If `self` is a `ResponseErrorReason.invalidHTTPStatusCode` error\n    ///            and its status code equals to `code`, `true` is returned. Otherwise, `false`.\n    ///\n    \n    \n    /// A helper method for checking HTTP status code.\n    ///\n    /// Use this helper method to determine whether this error corresponds to a\n    /// ``ResponseErrorReason/invalidHTTPStatusCode(response:)`` with a specific status code.\n    ///\n    /// - Parameter code: The desired HTTP status code for comparison.\n    /// - Returns: `true` if the error is of type ``ResponseErrorReason/invalidHTTPStatusCode(response:)`` and its\n    /// status code matches the provided `code`, otherwise `false`.\n    public func isInvalidResponseStatusCode(_ code: Int) -> Bool {\n        if case .responseError(reason: .invalidHTTPStatusCode(let response)) = self {\n            return response.statusCode == code\n        }\n        return false\n    }\n\n    \n    /// A helper method for checking the error is type of ``ResponseErrorReason/invalidHTTPStatusCode(response:)``.\n    public var isInvalidResponseStatusCode: Bool {\n        if case .responseError(reason: .invalidHTTPStatusCode) = self {\n            return true\n        }\n        return false\n    }\n\n    /// A helper property that indicates whether this error is of type\n    /// ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)`` or not.\n    ///\n    /// This property is used to check if a new image setting task starts while the old one is still running.\n    /// In such a scenario, the identifier of the new task will overwrite the identifier of the old task.\n    ///\n    /// When the old task finishes, a ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)`` error will\n    ///  be raised to notify you that the setting process has completed with a certain result, but the image view or \n    ///  button has not been updated.\n    ///\n    /// - Returns: `true` if the error is of type ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)``,\n    ///  `false` otherwise.\n    public var isNotCurrentTask: Bool {\n        if case .imageSettingError(reason: .notCurrentSourceTask(_, _, _)) = self {\n            return true\n        }\n        return false\n    }\n\n    var isLowDataModeConstrained: Bool {\n        if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *),\n           case .responseError(reason: .URLSessionError(let sessionError)) = self,\n           let urlError = sessionError as? URLError,\n           urlError.networkUnavailableReason == .constrained\n        {\n            return true\n        }\n        return false\n    }\n\n}\n\n// MARK: - LocalizedError Conforming\nextension KingfisherError: LocalizedError {\n    \n    /// Provides a localized message describing the error that occurred.\n    ///\n    /// Use this property to obtain a human-readable description of the error for display to the user.\n    public var errorDescription: String? {\n        switch self {\n        case .requestError(let reason): return reason.errorDescription\n        case .responseError(let reason): return reason.errorDescription\n        case .cacheError(let reason): return reason.errorDescription\n        case .processorError(let reason): return reason.errorDescription\n        case .imageSettingError(let reason): return reason.errorDescription\n        }\n    }\n}\n\n\n// MARK: - CustomNSError Conforming\nextension KingfisherError: CustomNSError {\n\n    /// The error domain for ``KingfisherError``. All errors generated by Kingfisher are categorized under this domain.\n    ///\n    /// When handling errors from the Kingfisher library, you can use this domain to identify and distinguish them\n    /// from other types of errors in your application.\n    ///\n    /// - Note: The error domain is a string identifier associated with each error.\n    public static let domain = \"com.onevcat.Kingfisher.Error\"\n\n    /// Represents the error code within the specified error domain.\n    ///\n    /// Use this property to retrieve the specific error code associated with a ``KingfisherError``. The error code\n    /// provides additional context and information about the error, allowing you to handle and respond to different\n    ///  error scenarios.\n    ///\n    /// - Note: Error codes are numerical values associated with each error within a domain. Check the error code in the\n    /// API reference of each error reason for the detail.\n    ///\n    /// - Returns: The error code as an integer.\n    public var errorCode: Int {\n        switch self {\n        case .requestError(let reason): return reason.errorCode\n        case .responseError(let reason): return reason.errorCode\n        case .cacheError(let reason): return reason.errorCode\n        case .processorError(let reason): return reason.errorCode\n        case .imageSettingError(let reason): return reason.errorCode\n        }\n    }\n}\n\nextension KingfisherError.RequestErrorReason {\n    var errorDescription: String? {\n        switch self {\n        case .emptyRequest:\n            return \"The request is empty or `nil`.\"\n        case .invalidURL(let request):\n            return \"The request contains an invalid or empty URL. Request: \\(request).\"\n        case .taskCancelled(let task, let token):\n            return \"The session task was cancelled. Task: \\(task), cancel token: \\(token).\"\n        case .livePhotoTaskCancelled(let source):\n            return \"The live photo download task was cancelled. Source: \\(source)\"\n        case .asyncTaskContextCancelled:\n            return \"The async task context was cancelled. This usually happens when the task is cancelled before it starts.\"\n        }\n    }\n    \n    var errorCode: Int {\n        switch self {\n        case .emptyRequest: return 1001\n        case .invalidURL: return 1002\n        case .taskCancelled: return 1003\n        case .livePhotoTaskCancelled: return 1004\n        case .asyncTaskContextCancelled: return 1005\n        }\n    }\n}\n\nextension KingfisherError.ResponseErrorReason {\n    var errorDescription: String? {\n        switch self {\n        case .invalidURLResponse(let response):\n            return \"The URL response is invalid: \\(response)\"\n        case .invalidHTTPStatusCode(let response):\n            return \"The HTTP status code in response is invalid. Code: \\(response.statusCode), response: \\(response).\"\n        case .URLSessionError(let error):\n            return \"A URL session error happened. The underlying error: \\(error)\"\n        case .dataModifyingFailed(let task):\n            return \"The data modifying delegate returned `nil` for the downloaded data. Task: \\(task).\"\n        case .noURLResponse(let task):\n            return \"No URL response received. Task: \\(task).\"\n        case .cancelledByDelegate(let response):\n            return \"The downloading task is cancelled by the downloader delegate. Response: \\(response).\"\n\n        }\n    }\n    \n    var errorCode: Int {\n        switch self {\n        case .invalidURLResponse: return 2001\n        case .invalidHTTPStatusCode: return 2002\n        case .URLSessionError: return 2003\n        case .dataModifyingFailed: return 2004\n        case .noURLResponse: return 2005\n        case .cancelledByDelegate: return 2006\n        }\n    }\n}\n\nextension KingfisherError.CacheErrorReason {\n    var errorDescription: String? {\n        switch self {\n        case .fileEnumeratorCreationFailed(let url):\n            return \"Cannot create file enumerator for URL: \\(url).\"\n        case .invalidFileEnumeratorContent(let url):\n            return \"Cannot get contents from the file enumerator at URL: \\(url).\"\n        case .invalidURLResource(let error, let key, let url):\n            return \"Cannot get URL resource values or data for the given URL: \\(url). \" +\n                   \"Cache key: \\(key). Underlying error: \\(error)\"\n        case .cannotLoadDataFromDisk(let url, let error):\n            return \"Cannot load data from disk at URL: \\(url). Underlying error: \\(error)\"\n        case .cannotCreateDirectory(let path, let error):\n            return \"Cannot create directory at given path: Path: \\(path). Underlying error: \\(error)\"\n        case .imageNotExisting(let key):\n            return \"The image is not in cache, but you requires it should only be \" +\n                   \"from cache by enabling the `.onlyFromCache` option. Key: \\(key).\"\n        case .cannotConvertToData(let object, let error):\n            return \"Cannot convert the input object to a `Data` object when storing it to disk cache. \" +\n                   \"Object: \\(object). Underlying error: \\(error)\"\n        case .cannotSerializeImage(let image, let originalData, let serializer):\n            return \"Cannot serialize an image due to the cache serializer returning `nil`. \" +\n                   \"Image: \\(String(describing:image)), original data: \\(String(describing: originalData)), \" +\n                   \"serializer: \\(serializer).\"\n        case .cannotCreateCacheFile(let fileURL, let key, let data, let error):\n            return \"Cannot create cache file at url: \\(fileURL), key: \\(key), data length: \\(data.count). \" +\n                   \"Underlying foundation error: \\(error).\"\n        case .cannotSetCacheFileAttribute(let filePath, let attributes, let error):\n            return \"Cannot set file attribute for the cache file at path: \\(filePath), attributes: \\(attributes).\" +\n                   \"Underlying foundation error: \\(error).\"\n        case .diskStorageIsNotReady(let cacheURL):\n            return \"The disk storage is not ready to use yet at URL: '\\(cacheURL)'. \" +\n                \"This is usually caused by extremely lack of disk space. Ask users to free up some space and restart the app.\"\n        case .missingLivePhotoResourceOnDisk(let resource):\n            return \"The live photo resource '\\(resource)' is missing in the cache. Usually a re-download\" +\n            \" can fix this issue.\"\n        }\n    }\n    \n    var errorCode: Int {\n        switch self {\n        case .fileEnumeratorCreationFailed: return 3001\n        case .invalidFileEnumeratorContent: return 3002\n        case .invalidURLResource: return 3003\n        case .cannotLoadDataFromDisk: return 3004\n        case .cannotCreateDirectory: return 3005\n        case .imageNotExisting: return 3006\n        case .cannotConvertToData: return 3007\n        case .cannotSerializeImage: return 3008\n        case .cannotCreateCacheFile: return 3009\n        case .cannotSetCacheFileAttribute: return 3010\n        case .diskStorageIsNotReady: return 3011\n        case .missingLivePhotoResourceOnDisk: return 3012\n        }\n    }\n}\n\nextension KingfisherError.ProcessorErrorReason {\n    var errorDescription: String? {\n        switch self {\n        case .processingFailed(let processor, let item):\n            return \"Processing image failed. Processor: \\(processor). Processing item: \\(item).\"\n        }\n    }\n    \n    var errorCode: Int {\n        switch self {\n        case .processingFailed: return 4001\n        }\n    }\n}\n\nextension KingfisherError.ImageSettingErrorReason {\n    var errorDescription: String? {\n        switch self {\n        case .emptySource:\n            return \"The input resource is empty.\"\n        case .notCurrentSourceTask(let result, let error, let resource):\n            if let result = result {\n                return \"Retrieving resource succeeded, but this source is \" +\n                       \"not the one currently expected. Result: \\(result). Resource: \\(resource).\"\n            } else if let error = error {\n                return \"Retrieving resource failed, and this resource is \" +\n                       \"not the one currently expected. Error: \\(error). Resource: \\(resource).\"\n            } else {\n                return nil\n            }\n        case .dataProviderError(let provider, let error):\n            return \"Image data provider fails to provide data. Provider: \\(provider), error: \\(error)\"\n        case .alternativeSourcesExhausted(let errors):\n            return \"Image setting from alternative sources failed: \\(errors)\"\n        case .notCurrentLivePhotoSourceTask(let result, let error, let source):\n            if let result = result {\n                return \"Retrieving live photo resource succeeded, but this source is \" +\n                \"not the one currently expected. Result: \\(result). Resource: \\(source).\"\n            } else if let error = error {\n                return \"Retrieving live photo resource failed, and this resource is \" +\n                \"not the one currently expected. Error: \\(error). Resource: \\(source).\"\n            } else {\n                return nil\n            }\n        case .livePhotoResultError(let result, let error, let source):\n            return \"An error occurred while processing live photo. Source: \\(source). \" +\n                   \"Result: \\(String(describing: result)). Error: \\(String(describing: error))\"\n        }\n    }\n    \n    var errorCode: Int {\n        switch self {\n        case .emptySource: return 5001\n        case .notCurrentSourceTask: return 5002\n        case .dataProviderError: return 5003\n        case .alternativeSourcesExhausted: return 5004\n        case .notCurrentLivePhotoSourceTask: return 5005\n        case .livePhotoResultError: return 5006\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/General/KingfisherManager+LivePhoto.swift",
    "content": "//\n//  KingfisherManager+LivePhoto.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2024/10/01.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n@preconcurrency import Photos\n\n/// A structure that contains information about the result of loading a live photo.\npublic struct LivePhotoLoadingInfoResult: Sendable {\n    \n    /// Retrieves the live photo disk URLs from this result.\n    public let fileURLs: [URL]\n\n    /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved.\n    ///\n    /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned.\n    /// Otherwise, ``CacheType/disk`` will be returned for the live photo. ``CacheType/memory`` is not available for\n    /// live photos since it may take too much memory. All cached live photos are loaded from disk only.\n    public let cacheType: CacheType\n\n    /// The ``LivePhotoSource`` to which this result is related. This indicates where the `livePhoto` referenced by\n    /// `self` is located.\n    public let source: LivePhotoSource\n\n    /// The original ``LivePhotoSource`` from which the retrieval task begins. It may differ from the ``source`` property.\n    /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the\n    /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process.\n    public let originalSource: LivePhotoSource\n    \n    /// Retrieves the data associated with this result.\n    ///\n    /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns\n    /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache\n    /// serializer from the loading options and returns the result.\n    ///\n    /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to\n    /// use it multiple times and avoid frequent calls to this method.\n    public let data: @Sendable () -> [Data]\n}\n\nextension KingfisherManager {\n\n    /// Retrieves a live photo from the specified source.\n    ///\n    /// This method asynchronously loads a live photo from the given source, applying the specified options and\n    /// reporting progress if a progress block is provided.\n    ///\n    /// - Parameters:\n    ///   - source: The ``LivePhotoSource`` from which to retrieve the live photo.\n    ///   - options: A dictionary of options to apply to the retrieval process. If `nil`, the default options will be\n    ///   used.\n    ///   - progressBlock: An optional closure to be called periodically during the download process.\n    ///   - referenceTaskIdentifierChecker: An optional closure that returns a Boolean value indicating whether the task\n    ///   should proceed.\n    ///\n    /// - Returns: A ``LivePhotoLoadingInfoResult`` containing information about the retrieved live photo.\n    ///\n    /// - Throws: An error if the retrieval process fails.\n    ///\n    /// - Note: This method uses `LivePhotoImageProcessor` by default. Custom processors are not supported for live photos.\n    ///\n    /// - Warning: Not all options are working for this method. And currently the `progressBlock` is not working. \n    /// It will be implemented in the future.\n    public func retrieveLivePhoto(\n        with source: LivePhotoSource,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        referenceTaskIdentifierChecker: (() -> Bool)? = nil\n    ) async throws -> LivePhotoLoadingInfoResult {\n        let fullOptions = currentDefaultOptions + (options ?? .empty)\n        var checkedOptions = KingfisherParsedOptionsInfo(fullOptions)\n        \n        if checkedOptions.processor == DefaultImageProcessor.default {\n            // The default processor is a default behavior so we replace it silently.\n            checkedOptions.processor = LivePhotoImageProcessor.default\n        } else if checkedOptions.processor != LivePhotoImageProcessor.default {\n            // Warn the framework user that the processor is not supported.\n            assertionFailure(\"[Kingfisher] Using of custom processors during loading of live photo resource is not supported.\")\n            checkedOptions.processor = LivePhotoImageProcessor.default\n        }\n        \n        if let checker = referenceTaskIdentifierChecker {\n            checkedOptions.onDataReceived?.forEach {\n                $0.onShouldApply = checker\n            }\n        }\n        \n        // TODO. We ignore the retry of live photo and the progress now to suppress the complexity.\n        \n        let missingResources = missingResources(source, options: checkedOptions)\n        let resourcesResult = try await downloadAndCache(resources: missingResources, options: checkedOptions)\n        \n        let targetCache = checkedOptions.targetCache ?? cache\n        var fileURLs = [URL]()\n        for resource in source.resources {\n            let url = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: checkedOptions)\n            guard let url else {\n                // This should not happen normally if the previous `downloadAndCache` done without issue, but in case.\n                throw KingfisherError.cacheError(reason: .missingLivePhotoResourceOnDisk(resource))\n            }\n            fileURLs.append(url)\n        }\n        \n        return LivePhotoLoadingInfoResult(\n            fileURLs: fileURLs,\n            cacheType: missingResources.isEmpty ? .disk : .none,\n            source: source,\n            originalSource: source,\n            data: {\n                resourcesResult.map { $0.originalData }\n            })\n    }\n    \n    // Returns the missing resources for the given source and options. If the resource is not in the cache, it will be\n    // returned as a missing resource.\n    func missingResources(_ source: LivePhotoSource, options: KingfisherParsedOptionsInfo) -> [LivePhotoResource] {\n        let missingResources: [LivePhotoResource]\n        if options.forceRefresh {\n            missingResources = source.resources\n        } else {\n            let targetCache = options.targetCache ?? cache\n            missingResources = source.resources.reduce([], { r, resource in\n                // Check if the resource is in the cache. It includes a guess of the file extension.\n                let cachedFileURL = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: options)\n                if cachedFileURL == nil {\n                    return r + [resource]\n                } else {\n                    return r\n                }\n            })\n        }\n        return missingResources\n    }\n    \n    // Download the resources and store them to the cache.\n    // If the resource does not specify a file extension (from either the URL extension or the explicit \n    // `referenceFileType`), we infer it from the file signature.\n    func downloadAndCache(\n        resources: [LivePhotoResource],\n        options: KingfisherParsedOptionsInfo\n    ) async throws -> [LivePhotoResourceDownloadingResult] {\n        if resources.isEmpty {\n            return []\n        }\n        let downloader = options.downloader ?? downloader\n        let cache = options.targetCache ?? cache\n\n        // Download all resources concurrently.\n        return try await withThrowingTaskGroup(of: LivePhotoResourceDownloadingResult.self) { \n            group in\n            \n            for resource in resources {\n                group.addTask {\n                    \n                    let downloadedResource: LivePhotoResourceDownloadingResult\n                    \n                    switch resource.dataSource {\n                    case .network(let urlResource):\n                        downloadedResource = try await downloader.downloadLivePhotoResource(\n                            with: urlResource.downloadURL,\n                            options: options\n                        )\n                    case .provider(let provider):\n                        downloadedResource = try await LivePhotoResourceDownloadingResult(\n                            originalData: provider.data(),\n                            url: provider.contentURL\n                        )\n                    }\n                     \n                    // We need to specify the extension so the file is saved correctly. Live photo loading requires\n                    // the file extension to be correct. Otherwise, a 3302 error will be thrown.\n                    // https://developer.apple.com/documentation/photokit/phphotoserror/code/invalidresource\n                    let fileExtension = resource.referenceFileType\n                        .determinedFileExtension(downloadedResource.originalData)\n                    try await cache.storeToDisk(\n                        downloadedResource.originalData,\n                        forKey: resource.cacheKey,\n                        processorIdentifier: options.processor.identifier,\n                        forcedExtension: fileExtension,\n                        expiration: options.diskCacheExpiration\n                    )\n                    return downloadedResource\n                }\n            }\n            \n            var result: [LivePhotoResourceDownloadingResult] = []\n            for try await resource in group {\n                result.append(resource)\n            }\n            return result\n        }\n    }\n}\n\nextension ImageCache {\n    \n    func possibleCacheFileURLIfOnDisk(\n        resource: LivePhotoResource,\n        options: KingfisherParsedOptionsInfo\n    ) -> URL? {\n        possibleCacheFileURLIfOnDisk(\n            forKey: resource.cacheKey,\n            processorIdentifier: options.processor.identifier,\n            referenceFileType: resource.referenceFileType\n        )\n    }\n    \n    // Returns the possible cache file URL for the given key and processor identifier. If the file is on disk, it will\n    // return the URL. Otherwise, it will return `nil`.\n    //\n    // This method also tries to guess the file extension if it is not specified in the `referenceFileType`. \n    // `PHLivePhoto`'s `request` method requires the file extension to be correct on the disk, and we also stored the \n    // downloaded data with the correct extension (if it is not specified in the `referenceFileType`, we infer it from\n    // the file signature. See `FileType.determinedFileExtension` for more).\n    func possibleCacheFileURLIfOnDisk(\n        forKey key: String,\n        processorIdentifier identifier: String,\n        referenceFileType: LivePhotoResource.FileType\n    ) -> URL? {\n        switch referenceFileType {\n        case .heic, .mov:\n            // The extension is specified and is what necessary to load a live photo, use it.\n            return cacheFileURLIfOnDisk(\n                forKey: key, processorIdentifier: identifier, forcedExtension: referenceFileType.fileExtension\n            )\n        case .other(let ext):\n            if ext.isEmpty {\n                // The extension is not specified. Guess from the default set of values.\n                let possibleFileTypes: [LivePhotoResource.FileType] = [.heic, .mov]\n                for fileType in possibleFileTypes {\n                    let url = cacheFileURLIfOnDisk(\n                        forKey: key, processorIdentifier: identifier, forcedExtension: fileType.fileExtension\n                    )\n                    if url != nil {\n                        // Found, early return.\n                        return url\n                    }\n                }\n                return nil\n            } else {\n                // The extension is specified but maybe not valid for live photo. Trust the user and use it to find the\n                // file.\n                return cacheFileURLIfOnDisk(\n                    forKey: key, processorIdentifier: identifier, forcedExtension: ext\n                )\n            }\n        }\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/General/KingfisherManager.swift",
    "content": "//\n//  KingfisherManager.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n\nimport Foundation\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// Represents the type for a downloading progress block.\n///\n/// This block type is used to monitor the progress of data being downloaded. It takes two parameters:\n///\n/// 1. `receivedSize`: The size of the data received in the current response.\n/// 2. `expectedSize`: The total expected data length from the response's \"Content-Length\" header. If the expected \n/// length is not available, this block will not be called.\n///\n/// You can use this progress block to track the download progress and update user interfaces or perform additional \n/// actions based on the progress.\n///\n/// - Parameters:\n///   - receivedSize: The size of the data received.\n///   - expectedSize: The expected total data length from the \"Content-Length\" header.\npublic typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void)\n\n/// Represents the result of a Kingfisher image retrieval task.\n///\n/// This type encapsulates the outcome of an image retrieval operation performed by Kingfisher.\n/// It holds a successful result with the retrieved image.\npublic struct RetrieveImageResult: Sendable {\n    /// Retrieves the image object from this result.\n    public let image: KFCrossPlatformImage\n\n    /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved.\n    ///\n    /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned.\n    /// Otherwise, either ``CacheType/memory`` or ``CacheType/disk`` will be returned, allowing you to determine whether\n    /// the image was retrieved from memory or disk cache.\n    public let cacheType: CacheType\n\n    /// The ``Source`` to which this result is related. This indicates where the `image` referenced by `self` is located.\n    public let source: Source\n\n    /// The original ``Source`` from which the retrieval task begins. It may differ from the ``source`` property.\n    /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the\n    /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process.\n    public let originalSource: Source\n    \n    /// Retrieves the data associated with this result.\n    ///\n    /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns \n    /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache\n    /// serializer from the loading options and returns the result.\n    ///\n    /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to \n    /// use it multiple times and avoid frequent calls to this method.\n    public let data: @Sendable () -> Data?\n    \n    /// The network metrics collected during the download process.\n    ///\n    /// This property contains network performance metrics when the image was downloaded from the network\n    /// (`cacheType == .none`). For cached images (`cacheType == .memory` or `.disk`), this will be `nil`.\n    public let metrics: NetworkMetrics?\n    \n    /// Creates a RetrieveImageResult.\n    ///\n    /// - Parameters:\n    ///   - image: The retrieved image.\n    ///   - cacheType: The cache source type.\n    ///   - source: The source of the image.\n    ///   - originalSource: The original source that initiated the retrieval.\n    ///   - data: A closure that provides the image data.\n    ///   - metrics: The network metrics collected during download. Defaults to nil for cached images.\n    public init(\n        image: KFCrossPlatformImage,\n        cacheType: CacheType,\n        source: Source,\n        originalSource: Source,\n        data: @escaping @Sendable () -> Data?,\n        metrics: NetworkMetrics? = nil\n    ) {\n        self.image = image\n        self.cacheType = cacheType\n        self.source = source\n        self.originalSource = originalSource\n        self.data = data\n        self.metrics = metrics\n    }\n}\n\n/// A structure that stores related information about a ``KingfisherError``. It provides contextual information\n/// to facilitate the identification of the error.\npublic struct PropagationError: Sendable {\n\n    /// The ``Source`` to which current `error` is bound.\n    public let source: Source\n\n    /// The actual error happens in framework.\n    public let error: KingfisherError\n}\n\n/// The block type used for handling updates during the downloading task. \n///\n/// The `newTask` parameter represents the updated task for the image loading process. It is `nil` if the image loading\n/// doesn't involve a downloading process. When an image download is initiated, this value will contain the actual\n/// ``DownloadTask`` instance, allowing you to retain it or cancel it later if necessary.\npublic typealias DownloadTaskUpdatedBlock = (@Sendable (_ newTask: DownloadTask?) -> Void)\n\n/// The main manager class of Kingfisher. It connects the Kingfisher downloader and cache to offer a set of convenient \n/// methods for working with Kingfisher tasks.\n///\n/// You can utilize this class to retrieve an image via a specified URL from the web or cache.\npublic class KingfisherManager: @unchecked Sendable {\n\n    private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.KingfisherManagerPropertyQueue\")\n    \n    /// Represents a shared manager used across Kingfisher.\n    /// Use this instance for getting or storing images with Kingfisher.\n    public static let shared = KingfisherManager()\n\n    // Mark: Public Properties\n    \n    private var _cache: ImageCache\n    \n    /// The ``ImageCache`` utilized by this manager, which defaults to ``ImageCache/default``.\n    ///\n    /// If a cache is specified in ``KingfisherManager/defaultOptions`` or ``KingfisherOptionsInfoItem/targetCache(_:)``,\n    /// those specified values will take precedence when Kingfisher attempts to retrieve or store images in the cache.\n    public var cache: ImageCache {\n        get { propertyQueue.sync { _cache } }\n        set { propertyQueue.sync { _cache = newValue } }\n    }\n    \n    private var _downloader: ImageDownloader\n    \n    /// The ``ImageDownloader`` utilized by this manager, which defaults to ``ImageDownloader/default``.\n    ///\n    /// If a downloader is specified in ``KingfisherManager/defaultOptions`` or ``KingfisherOptionsInfoItem/downloader(_:)``,\n    /// those specified values will take precedence when Kingfisher attempts to download the image data from a remote\n    /// server.\n    public var downloader: ImageDownloader {\n        get { propertyQueue.sync { _downloader } }\n        set { propertyQueue.sync { _downloader = newValue } }\n    }\n    \n    /// The default options used by the ``KingfisherManager`` instance.\n    ///\n    /// These options are utilized in Kingfisher manager-related methods, as well as all view extension methods.\n    /// You can also pass additional options for each image task by providing an `options` parameter to Kingfisher's APIs.\n    ///\n    /// Per-image options will override the default ones if there is a conflict.\n    public var defaultOptions = KingfisherOptionsInfo.empty\n    \n    // Use `defaultOptions` to overwrite the `downloader` and `cache`.\n    var currentDefaultOptions: KingfisherOptionsInfo {\n        return [.downloader(downloader), .targetCache(cache)] + defaultOptions\n    }\n\n    private let processingQueue: CallbackQueue\n    \n    private convenience init() {\n        self.init(downloader: .default, cache: .default)\n    }\n\n    /// Creates an image setting manager with the specified downloader and cache.\n    ///\n    /// - Parameters:\n    ///   - downloader: The image downloader used for image downloads.\n    ///   - cache: The image cache that stores images in memory and on disk.\n    ///\n    public init(downloader: ImageDownloader, cache: ImageCache) {\n        _downloader = downloader\n        _cache = cache\n\n        let processQueueName = \"com.onevcat.Kingfisher.KingfisherManager.processQueue.\\(UUID().uuidString)\"\n        processingQueue = .dispatch(DispatchQueue(label: processQueueName))\n    }\n\n    // MARK: - Getting Images\n\n    /// Retrieves an image from a specified resource.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object defining data information, such as a key or URL.\n    ///   - options: Options to use when creating the image.\n    ///   - progressBlock: Called when the image download progress is updated. This block is invoked only if the response \n    ///   contains an `expectedContentLength` and always runs on the main queue.\n    ///   - downloadTaskUpdated: Called when a new image download task is created for the current image retrieval. This\n    ///   typically occurs when an alternative source is used to replace the original (failed) task. You can update your\n    ///   reference to the ``DownloadTask`` if you want to manually invoke ``DownloadTask/cancel()`` on the new task.\n    ///   - completionHandler: Called when the image retrieval and setting are completed. This completion handler is \n    ///   invoked from the `options.callbackQueue`. If not specified, the main queue is used.\n    ///\n    /// - Returns: A task representing the image download. If a download task is initiated for a ``Source/network(_:)`` resource,\n    ///            the started ``DownloadTask`` is returned; otherwise, `nil` is returned.\n    ///\n    /// - Note: This method first checks whether the requested `resource` is already in the cache. If it is cached,\n    /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads\n    /// the `resource`, stores it in the cache, and then calls the `completionHandler`.\n    ///\n    @discardableResult\n    public func retrieveImage(\n        with resource: any Resource,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?\n    {\n        return retrieveImage(\n            with: resource.convertToSource(),\n            options: options,\n            progressBlock: progressBlock,\n            downloadTaskUpdated: downloadTaskUpdated,\n            completionHandler: completionHandler\n        )\n    }\n\n    /// Retrieves an image from a specified source.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object defining data information, such as a key or URL.\n    ///   - options: Options to use when creating the image.\n    ///   - progressBlock: Called when the image download progress is updated. This block is invoked only if the response\n    ///   contains an `expectedContentLength` and always runs on the main queue.\n    ///   - downloadTaskUpdated: Called when a new image download task is created for the current image retrieval. This\n    ///   typically occurs when an alternative source is used to replace the original (failed) task. You can update your\n    ///   reference to the ``DownloadTask`` if you want to manually invoke ``DownloadTask/cancel()`` on the new task.\n    ///   - completionHandler: Called when the image retrieval and setting are completed. This completion handler is\n    ///   invoked from the `options.callbackQueue`. If not specified, the main queue is used.\n    ///\n    /// - Returns: A task representing the image download. If a download task is initiated for a ``Source/network(_:)`` resource,\n    ///            the started ``DownloadTask`` is returned; otherwise, `nil` is returned.\n    ///\n    /// - Note: This method first checks whether the requested `source` is already in the cache. If it is cached,\n    /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads\n    /// the `source`, stores it in the cache, and then calls the `completionHandler`.\n    ///\n    @discardableResult\n    public func retrieveImage(\n        with source: Source,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?\n    {\n        let options = currentDefaultOptions + (options ?? .empty)\n        let info = KingfisherParsedOptionsInfo(options)\n        return retrieveImage(\n            with: source,\n            options: info,\n            progressBlock: progressBlock,\n            downloadTaskUpdated: downloadTaskUpdated,\n            completionHandler: completionHandler)\n    }\n\n    func retrieveImage(\n        with source: Source,\n        options: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock?,\n        downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,\n        progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?\n    {\n        var info = options\n        if let block = progressBlock {\n            info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]\n        }\n        return retrieveImage(\n            with: source,\n            options: info,\n            downloadTaskUpdated: downloadTaskUpdated,\n            progressiveImageSetter: progressiveImageSetter,\n            completionHandler: completionHandler)\n    }\n\n    func retrieveImage(\n        with source: Source,\n        options: KingfisherParsedOptionsInfo,\n        downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,\n        progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,\n        referenceTaskIdentifierChecker: (() -> Bool)? = nil,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?\n    {\n        var options = options\n        let retryStrategy = options.retryStrategy\n\n        let progressiveJPEG = options.progressiveJPEG\n        if let provider = ImageProgressiveProvider(options: options, refresh: { image in\n            guard let setter = progressiveImageSetter else {\n                return\n            }\n            guard let strategy = progressiveJPEG?.onImageUpdated(image) else {\n                setter(image)\n                return\n            }\n            switch strategy {\n            case .default: setter(image)\n            case .keepCurrent: break\n            case .replace(let newImage): setter(newImage)\n            }\n        }) {\n            options.onDataReceived = (options.onDataReceived ?? []) + [provider]\n        }\n        if let checker = referenceTaskIdentifierChecker {\n            options.onDataReceived?.forEach {\n                $0.onShouldApply = checker\n            }\n        }\n        \n        let retrievingContext = RetrievingContext(options: options, originalSource: source)\n\n        @Sendable func startNewRetrieveTask(\n            with source: Source,\n            retryContext: RetryContext?,\n            downloadTaskUpdated: DownloadTaskUpdatedBlock?\n        ) {\n            let newTask = self.retrieveImage(\n                with: source,\n                context: retrievingContext,\n                downloadTaskUpdated: downloadTaskUpdated\n            ) { result in\n                handler(currentSource: source, retryContext: retryContext, result: result)\n            }\n            downloadTaskUpdated?(newTask)\n        }\n\n        @Sendable func failCurrentSource(_ source: Source, retryContext: RetryContext?, with error: KingfisherError) {\n            // Skip alternative sources if the user cancelled it.\n            guard !error.isTaskCancelled else {\n                completionHandler?(.failure(error))\n                return\n            }\n            // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly.\n            guard !error.isLowDataModeConstrained else {\n                if let source = retrievingContext.options.lowDataModeSource {\n                    retrievingContext.options.lowDataModeSource = nil\n                    startNewRetrieveTask(with: source, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated)\n                } else {\n                    // This should not happen.\n                    completionHandler?(.failure(error))\n                }\n                return\n            }\n            if let nextSource = retrievingContext.popAlternativeSource() {\n                retrievingContext.appendError(error, to: source)\n                startNewRetrieveTask(with: nextSource, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated)\n            } else {\n                // No other alternative source. Finish with error.\n                if retrievingContext.propagationErrors.isEmpty {\n                    completionHandler?(.failure(error))\n                } else {\n                    retrievingContext.appendError(error, to: source)\n                    let finalError = KingfisherError.imageSettingError(\n                        reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors)\n                    )\n                    completionHandler?(.failure(finalError))\n                }\n            }\n        }\n\n        @Sendable func handler(\n            currentSource: Source,\n            retryContext: RetryContext?,\n            result: (Result<RetrieveImageResult, KingfisherError>)\n        ) -> Void {\n            switch result {\n            case .success:\n                completionHandler?(result)\n            case .failure(let error):\n                if let retryStrategy = retryStrategy {\n                    let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error)\n                    retryStrategy.retry(context: context) { decision in\n                        switch decision {\n                        case .retry(let userInfo):\n                            context.userInfo = userInfo\n                            startNewRetrieveTask(with: source, retryContext: context, downloadTaskUpdated: downloadTaskUpdated)\n                        case .stop:\n                            failCurrentSource(currentSource, retryContext: context, with: error)\n                        }\n                    }\n                } else {\n                    failCurrentSource(currentSource, retryContext: retryContext, with: error)\n                }\n            }\n        }\n\n        return retrieveImage(\n            with: source,\n            context: retrievingContext,\n            downloadTaskUpdated: downloadTaskUpdated)\n        {\n            result in\n            handler(currentSource: source, retryContext: nil, result: result)\n        }\n\n    }\n    \n    private func retrieveImage(\n        with source: Source,\n        context: RetrievingContext<Source>,\n        downloadTaskUpdated: DownloadTaskUpdatedBlock?,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?\n    {\n        let options = context.options\n        if options.forceRefresh {\n            return loadAndCacheImage(\n                source: source,\n                context: context,\n                completionHandler: completionHandler)?.value\n            \n        } else {\n            let loadedFromCache = retrieveImageFromCache(\n                source: source,\n                context: context,\n                downloadTaskUpdated: downloadTaskUpdated,\n                completionHandler: completionHandler)\n            \n            if loadedFromCache {\n                return nil\n            }\n            \n            if options.onlyFromCache {\n                let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey))\n                completionHandler?(.failure(error))\n                return nil\n            }\n            \n            return loadAndCacheImage(\n                source: source,\n                context: context,\n                completionHandler: completionHandler)?.value\n        }\n    }\n\n    func provideImage(\n        provider: any ImageDataProvider,\n        options: KingfisherParsedOptionsInfo,\n        completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)?)\n    {\n        guard let  completionHandler = completionHandler else { return }\n        provider.data { result in\n            switch result {\n            case .success(let data):\n                (options.processingQueue ?? self.processingQueue).execute {\n                    let processor = options.processor\n                    let processingItem = ImageProcessItem.data(data)\n                    guard let image = processor.process(item: processingItem, options: options) else {\n                        options.callbackQueue.execute {\n                            let error = KingfisherError.processorError(\n                                reason: .processingFailed(processor: processor, item: processingItem))\n                            completionHandler(.failure(error))\n                        }\n                        return\n                    }\n\n                    options.callbackQueue.execute {\n                        let result = ImageLoadingResult(image: image, url: nil, originalData: data)\n                        completionHandler(.success(result))\n                    }\n                }\n            case .failure(let error):\n                options.callbackQueue.execute {\n                    let error = KingfisherError.imageSettingError(\n                        reason: .dataProviderError(provider: provider, error: error))\n                    completionHandler(.failure(error))\n                }\n\n            }\n        }\n    }\n\n    private func cacheImage(\n        source: Source,\n        options: KingfisherParsedOptionsInfo,\n        context: RetrievingContext<Source>,\n        result: Result<ImageLoadingResult, KingfisherError>,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?\n    )\n    {\n        switch result {\n        case .success(let value):\n            let needToCacheOriginalImage = options.cacheOriginalImage &&\n                                           options.processor != DefaultImageProcessor.default\n            let coordinator = CacheCallbackCoordinator(\n                shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage)\n            let result = RetrieveImageResult(\n                image: options.imageModifier?.modify(value.image) ?? value.image,\n                cacheType: .none,\n                source: source,\n                originalSource: context.originalSource,\n                data: { value.originalData },\n                metrics: value.metrics\n            )\n            // Add image to cache.\n            let targetCache = options.targetCache ?? self.cache\n            targetCache.store(\n                value.image,\n                original: value.originalData,\n                forKey: source.cacheKey,\n                options: options,\n                toDisk: !options.cacheMemoryOnly)\n            {\n                _ in\n                coordinator.apply(.cachingImage) {\n                    completionHandler?(.success(result))\n                }\n            }\n\n            // Add original image to cache if necessary.\n\n            if needToCacheOriginalImage {\n                let originalCache = options.originalCache ?? targetCache\n                originalCache.storeToDisk(\n                    value.originalData,\n                    forKey: source.cacheKey,\n                    processorIdentifier: DefaultImageProcessor.default.identifier,\n                    expiration: options.diskCacheExpiration)\n                {\n                    _ in\n                    coordinator.apply(.cachingOriginalImage) {\n                        completionHandler?(.success(result))\n                    }\n                }\n            }\n\n            coordinator.apply(.cacheInitiated) {\n                completionHandler?(.success(result))\n            }\n\n        case .failure(let error):\n            completionHandler?(.failure(error))\n        }\n    }\n\n    @discardableResult\n    func loadAndCacheImage(\n        source: Source,\n        context: RetrievingContext<Source>,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?\n    {\n        let options = context.options\n        @Sendable func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {\n            cacheImage(\n                source: source,\n                options: options,\n                context: context,\n                result: result,\n                completionHandler: completionHandler\n            )\n        }\n\n        switch source {\n        case .network(let resource):\n            let downloader = options.downloader ?? self.downloader\n            let task = downloader.downloadImage(\n                with: resource.downloadURL, options: options, completionHandler: _cacheImage\n            )\n\n\n            // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when \n            // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler. \n            // Let's fallback to a traditional style before it can be fixed in Swift.\n            //\n            // https://github.com/onevcat/Kingfisher/issues/1436\n            //\n            // return task.map(DownloadTask.WrappedTask.download)\n\n            if task.isInitialized {\n                return .download(task)\n            } else {\n                return nil\n            }\n\n        case .provider(let provider):\n            provideImage(provider: provider, options: options, completionHandler: _cacheImage)\n            return .dataProviding\n        }\n    }\n    \n    /// Retrieves an image from either memory or disk cache.\n    ///\n    /// - Parameters:\n    ///   - source: The target source from which to retrieve the image.\n    ///   - key: The key to use for caching the image.\n    ///   - url: The image request URL. This is not used when retrieving an image from the cache; it is solely used for \n    ///   compatibility with ``RetrieveImageResult`` callbacks.\n    ///   - options: Options on how to retrieve the image from the image cache.\n    ///   - completionHandler: Called when the image retrieval is complete, either with a successful\n    ///   ``RetrieveImageResult`` or an error.\n    ///\n    /// - Returns: `true` if the requested image or the original image before processing exists in the cache. Otherwise, this method returns `false`.\n    ///\n    /// - Note: Image retrieval can occur in either the memory cache or the disk cache. The\n    /// ``KingfisherOptionsInfoItem/processor(_:)`` option in `options` is considered when searching the cache. If no\n    /// processed image is found, Kingfisher attempts to determine whether an original version of the image exists. If\n    /// an original exists, Kingfisher retrieves it from the cache and processes it. Subsequently, the processed image\n    /// is stored back in the cache for future use.\n    ///\n    func retrieveImageFromCache(\n        source: Source,\n        context: RetrievingContext<Source>,\n        downloadTaskUpdated: DownloadTaskUpdatedBlock?,\n        completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> Bool\n    {\n        let options = context.options\n        // 1. Check whether the image was already in target cache. If so, just get it.\n        let targetCache = options.targetCache ?? cache\n        let key = source.cacheKey\n        let targetImageCached = targetCache.imageCachedType(\n            forKey: key, processorIdentifier: options.processor.identifier)\n        \n        let validCache = targetImageCached.cached &&\n            (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory)\n        if validCache {\n            targetCache.retrieveImage(forKey: key, options: options) { result in\n                guard let completionHandler = completionHandler else { return }\n                \n                // TODO: Optimize it when we can use async across all the project.\n                @Sendable func checkResultImageAndCallback(_ inputImage: KFCrossPlatformImage) {\n                    var image = inputImage\n                    if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, options.imageCreatingOptions != image.kf.imageCreatingOptions, let data = image.kf.animatedImageData {\n                        // Recreate animated image representation when loaded in different options.\n                        // https://github.com/onevcat/Kingfisher/issues/1923\n                        image = options.processor.process(item: .data(data), options: options) ?? .init()\n                    }\n                    if let modifier = options.imageModifier {\n                        image = modifier.modify(image)\n                    }\n                    let value = result.map {\n                        RetrieveImageResult(\n                            image: image,\n                            cacheType: $0.cacheType,\n                            source: source,\n                            originalSource: context.originalSource,\n                            data: { [image] in options.cacheSerializer.data(with: image, original: nil) }\n                        )\n                    }\n                    completionHandler(value)\n                }\n                \n                result.match { cacheResult in\n                    options.callbackQueue.execute {\n                        guard let image = cacheResult.image else {\n                            completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))))\n                            return\n                        }\n                        \n                        if options.cacheSerializer.originalDataUsed {\n                            let processor = options.processor\n                            (options.processingQueue ?? self.processingQueue).execute {\n                                let item = ImageProcessItem.image(image)\n                                guard let processedImage = processor.process(item: item, options: options) else {\n                                    let error = KingfisherError.processorError(\n                                        reason: .processingFailed(processor: processor, item: item))\n                                    options.callbackQueue.execute { completionHandler(.failure(error)) }\n                                    return\n                                }\n                                options.callbackQueue.execute {\n                                    checkResultImageAndCallback(processedImage)\n                                }\n                            }\n                        } else {\n                            checkResultImageAndCallback(image)\n                        }\n                    }\n                } onFailure: { error in\n                    options.callbackQueue.execute {\n                        completionHandler(.failure(error))\n                    }\n                }\n            }\n            return true\n        }\n\n        // 2. Check whether the original image exists. If so, get it, process it, save to storage and return.\n        let originalCache = options.originalCache ?? targetCache\n        // No need to store the same file in the same cache again.\n        if originalCache === targetCache && options.processor == DefaultImageProcessor.default {\n            return false\n        }\n\n        // Check whether the unprocessed image existing or not.\n        let originalImageCacheType = originalCache.imageCachedType(\n            forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier)\n        let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh\n        \n        let canUseOriginalImageCache =\n            (canAcceptDiskCache && originalImageCacheType.cached) ||\n            (!canAcceptDiskCache && originalImageCacheType == .memory)\n        \n        if canUseOriginalImageCache {\n            // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove\n            // any processor from options first.\n            var optionsWithoutProcessor = options\n            optionsWithoutProcessor.processor = DefaultImageProcessor.default\n            originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in\n\n                result.match(\n                    onSuccess: { cacheResult in\n                        guard let image = cacheResult.image else {\n                            // The original cache type check is not a strong guarantee. When it happens, treat it as a cache miss.\n                            // In this case, fall back to download or provider loading.\n                            if options.onlyFromCache {\n                                let error = KingfisherError.cacheError(reason: .imageNotExisting(key: key))\n                                options.callbackQueue.execute { completionHandler?(.failure(error)) }\n                            } else {\n                                let task = self.loadAndCacheImage(\n                                    source: source,\n                                    context: context,\n                                    completionHandler: completionHandler\n                                )\n                                downloadTaskUpdated?(task?.value)\n                            }\n                            return\n                        }\n\n                        let processor = options.processor\n                        (options.processingQueue ?? self.processingQueue).execute {\n                            let item = ImageProcessItem.image(image)\n                            guard let processedImage = processor.process(item: item, options: options) else {\n                                let error = KingfisherError.processorError(\n                                    reason: .processingFailed(processor: processor, item: item))\n                                options.callbackQueue.execute { completionHandler?(.failure(error)) }\n                                return\n                            }\n\n                            var cacheOptions = options\n                            cacheOptions.callbackQueue = .untouch\n\n                            let coordinator = CacheCallbackCoordinator(\n                                shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false)\n\n                            let image = options.imageModifier?.modify(processedImage) ?? processedImage\n                            let result = RetrieveImageResult(\n                                image: image,\n                                cacheType: .none,\n                                source: source,\n                                originalSource: context.originalSource,\n                                data: { options.cacheSerializer.data(with: processedImage, original: nil) }\n                            )\n\n                            targetCache.store(\n                                processedImage,\n                                forKey: key,\n                                options: cacheOptions,\n                                toDisk: !options.cacheMemoryOnly)\n                            {\n                                _ in\n                                coordinator.apply(.cachingImage) {\n                                    options.callbackQueue.execute { completionHandler?(.success(result)) }\n                                }\n                            }\n\n                            coordinator.apply(.cacheInitiated) {\n                                options.callbackQueue.execute { completionHandler?(.success(result)) }\n                            }\n                        }\n                    },\n                    onFailure: { error in\n                        // This should not happen actually, since we already confirmed `originalImageCached` is `true`.\n                        // Just in case...\n                        if let completionHandler = completionHandler {\n                            options.callbackQueue.execute { completionHandler(.failure(error)) }\n                        }\n                    }\n                )\n            }\n            return true\n        }\n\n        return false\n    }\n}\n\n// Concurrency\nextension KingfisherManager {\n    \n    /// Retrieves an image from a specified resource.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object defining data information, such as a key or URL.\n    ///   - options: Options to use when creating the image.\n    ///   - progressBlock: Called when the image download progress is updated. This block is invoked only if the response\n    ///   contains an `expectedContentLength` and always runs on the main queue.\n    ///\n    /// - Returns: The ``RetrieveImageResult`` containing the retrieved image object and cache type.\n    /// - Throws: A ``KingfisherError`` if any issue occurred during the image retrieving progress.\n    ///\n    /// - Note: This method first checks whether the requested `resource` is already in the cache. If it is cached,\n    /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads\n    /// the `resource`, stores it in the cache, and then calls the `completionHandler`.\n    ///\n    public func retrieveImage(\n        with resource: any Resource,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil\n    ) async throws -> RetrieveImageResult\n    {\n        try await retrieveImage(\n            with: resource.convertToSource(),\n            options: options,\n            progressBlock: progressBlock\n        )\n    }\n    \n    /// Retrieves an image from a specified source.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object defining data information, such as a key or URL.\n    ///   - options: Options to use when creating the image.\n    ///   - progressBlock: Called when the image download progress is updated. This block is invoked only if the response\n    ///   contains an `expectedContentLength` and always runs on the main queue.\n    ///\n    /// - Returns: The ``RetrieveImageResult`` containing the retrieved image object and cache type.\n    /// - Throws: A ``KingfisherError`` if any issue occurred during the image retrieving progress.\n    ///\n    /// - Note: This method first checks whether the requested `source` is already in the cache. If it is cached,\n    /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads\n    /// the `source`, stores it in the cache, and then calls the `completionHandler`.\n    ///\n    public func retrieveImage(\n        with source: Source,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil\n    ) async throws -> RetrieveImageResult\n    {\n        let options = currentDefaultOptions + (options ?? .empty)\n        let info = KingfisherParsedOptionsInfo(options)\n        return try await retrieveImage(\n            with: source,\n            options: info,\n            progressBlock: progressBlock\n        )\n    }\n    \n    func retrieveImage(\n        with source: Source,\n        options: KingfisherParsedOptionsInfo,\n        progressBlock: DownloadProgressBlock? = nil\n    ) async throws -> RetrieveImageResult\n    {\n        var info = options\n        if let block = progressBlock {\n            info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]\n        }\n        return try await retrieveImage(\n            with: source,\n            options: info,\n            progressiveImageSetter: nil\n        )\n    }\n    \n    func retrieveImage(\n        with source: Source,\n        options: KingfisherParsedOptionsInfo,\n        progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,\n        referenceTaskIdentifierChecker: (() -> Bool)? = nil\n    ) async throws -> RetrieveImageResult\n    {\n        // Early cancellation check\n        if Task.isCancelled {\n            throw CancellationError()\n        }\n        \n        let task = CancellationDownloadTask()\n        return try await withTaskCancellationHandler {\n            try await withCheckedThrowingContinuation { continuation in\n                // Use an actor to ensure continuation is only resumed once in a Swift 6 compatible way\n                actor ContinuationState {\n                    var isResumed = false\n                    \n                    func tryResume() -> Bool {\n                        if !isResumed {\n                            isResumed = true\n                            return true\n                        }\n                        return false\n                    }\n                }\n                \n                let state = ContinuationState()\n                \n                @Sendable func safeResume(with result: Result<RetrieveImageResult, KingfisherError>) {\n                    Task {\n                        if await state.tryResume() {\n                            continuation.resume(with: result)\n                        }\n                    }\n                }\n                \n                let downloadTask = retrieveImage(\n                    with: source,\n                    options: options,\n                    downloadTaskUpdated: { newTask in\n                        Task {\n                            await task.setTask(newTask)\n                        }\n                    },\n                    progressiveImageSetter: progressiveImageSetter,\n                    referenceTaskIdentifierChecker: referenceTaskIdentifierChecker,\n                    completionHandler: { result in\n                        safeResume(with: result)\n                    }\n                )\n                \n                // Check for cancellation that may have occurred during setup\n                if Task.isCancelled {\n                    downloadTask?.cancel()\n                    let error: KingfisherError\n                    if let sessionTask = downloadTask?.sessionTask, let cancelToken = downloadTask?.cancelToken {\n                        error = .requestError(reason: .taskCancelled(task: sessionTask, token: cancelToken))\n                    } else {\n                        error = .requestError(reason: .asyncTaskContextCancelled)\n                    }\n                    safeResume(with: .failure(error))\n                } else {\n                    Task {\n                        await task.setTask(downloadTask)\n                    }\n                }\n            }\n        } onCancel: {\n            Task {\n                await task.task?.cancel()\n            }\n        }\n    }\n}\n\nclass RetrievingContext<SourceType>: @unchecked Sendable {\n\n    private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.RetrievingContextPropertyQueue\")\n    \n    private var _options: KingfisherParsedOptionsInfo\n    var options: KingfisherParsedOptionsInfo {\n        get { propertyQueue.sync { _options } }\n        set { propertyQueue.sync { _options = newValue } }\n    }\n\n    let originalSource: SourceType\n    var propagationErrors: [PropagationError] = []\n\n    init(options: KingfisherParsedOptionsInfo, originalSource: SourceType) {\n        self.originalSource = originalSource\n        _options = options\n    }\n\n    func popAlternativeSource() -> Source? {\n        var localOptions = options\n        guard var alternativeSources = localOptions.alternativeSources, !alternativeSources.isEmpty else {\n            return nil\n        }\n        let nextSource = alternativeSources.removeFirst()\n        \n        localOptions.alternativeSources = alternativeSources\n        options = localOptions\n        \n        return nextSource\n    }\n\n    @discardableResult\n    func appendError(_ error: KingfisherError, to source: Source) -> [PropagationError] {\n        let item = PropagationError(source: source, error: error)\n        propagationErrors.append(item)\n        return propagationErrors\n    }\n}\n\nclass CacheCallbackCoordinator: @unchecked Sendable {\n\n    enum State {\n        case idle\n        case imageCached\n        case originalImageCached\n        case done\n    }\n\n    enum Action {\n        case cacheInitiated\n        case cachingImage\n        case cachingOriginalImage\n    }\n\n    private let shouldWaitForCache: Bool\n    private let shouldCacheOriginal: Bool\n    private let stateQueue: DispatchQueue\n    private var threadSafeState: State = .idle\n\n    private(set) var state: State {\n        set { stateQueue.sync { threadSafeState = newValue } }\n        get { stateQueue.sync { threadSafeState } }\n    }\n\n    init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) {\n        self.shouldWaitForCache = shouldWaitForCache\n        self.shouldCacheOriginal = shouldCacheOriginal\n        let stateQueueName = \"com.onevcat.Kingfisher.CacheCallbackCoordinator.stateQueue.\\(UUID().uuidString)\"\n        self.stateQueue = DispatchQueue(label: stateQueueName)\n    }\n\n    func apply(_ action: Action, trigger: () -> Void) {\n        switch (state, action) {\n        case (.done, _):\n            break\n\n        // From .idle\n        case (.idle, .cacheInitiated):\n            if !shouldWaitForCache {\n                state = .done\n                trigger()\n            }\n        case (.idle, .cachingImage):\n            if shouldCacheOriginal {\n                state = .imageCached\n            } else {\n                state = .done\n                trigger()\n            }\n        case (.idle, .cachingOriginalImage):\n            state = .originalImageCached\n\n        // From .imageCached\n        case (.imageCached, .cachingOriginalImage):\n            state = .done\n            trigger()\n\n        // From .originalImageCached\n        case (.originalImageCached, .cachingImage):\n            state = .done\n            trigger()\n\n        default:\n            assertionFailure(\"This case should not happen in CacheCallbackCoordinator: \\(state) - \\(action)\")\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/General/KingfisherOptionsInfo.swift",
    "content": "//\n//  KingfisherOptionsInfo.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/23.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n    \n\n/// `KingfisherOptionsInfo` is a typealias for `[KingfisherOptionsInfoItem]`.\n/// You can utilize the enum of option items with values to control certain behaviors of Kingfisher.\npublic typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]\n\nextension Array where Element == KingfisherOptionsInfoItem {\n    static let empty: KingfisherOptionsInfo = []\n}\n\n/// Represents the available option items that can be used in ``KingfisherOptionsInfo``.\npublic enum KingfisherOptionsInfoItem: Sendable {\n    \n    /// Kingfisher will utilize the associated ``ImageCache`` object when performing related operations, such as\n    /// attempting to retrieve cached images and storing downloaded images in it.\n    case targetCache(ImageCache)\n    \n    /// The ``ImageCache`` used for storing and retrieving original images.\n    ///\n    /// If ``originalCache(_:)`` is specified in the options, it will be given preference for storing and retrieving\n    /// original images. If there is no ``originalCache(_:)`` option, ``targetCache(_:)`` will be used to\n    /// store original images as well.\n    ///\n    /// When using ``KingfisherManager`` to download and store an image, if ``cacheOriginalImage`` is applied in the\n    /// options, the original image will be stored in the associated ``ImageCache`` of this option. \n    ///\n    /// Simultaneously, if a requested final image (with a processor applied) cannot be found in the ``targetCache(_:)``,\n    /// Kingfisher will attempt to search for the original image to see if it already exists. If found, it will be\n    /// utilized and processed with the given processor. This optimization prevents downloading the same image multiple\n    /// times.\n    case originalCache(ImageCache)\n    \n    /// Kingfisher will utilize the associated ``ImageDownloader`` object to download the requested images.\n    case downloader(ImageDownloader)\n\n    /// This enum defines the transition effect to be applied when setting an image to an image view.\n    ///\n    /// Kingfisher uses the ``ImageTransition`` specified by this enum to animate the image in if it's downloaded from\n    /// the web. \n    ///\n    /// By default, the transition does not occur when the image is retrieved from either memory or disk cache. To\n    /// force the transition even when the image is retrieved from the cache, also set\n    /// ``KingfisherOptionsInfoItem/forceTransition``.\n    ///\n    /// - Important: This option is designed for UIKit/AppKit transitions. For SwiftUI applications, use the\n    /// ``KFImageProtocol/loadTransition(_:animation:)`` method instead, which provides native SwiftUI transition support.\n    case transition(ImageTransition)\n    \n    /// The associated `Float` value to be set as the priority of the image download task.\n    ///\n    /// This value should fall within the range of 0.0 to 1.0. If this option is not set, the default value\n    ///  (`URLSessionTask.defaultPriority`) will be used.\n    case downloadPriority(Float)\n    \n    /// When set, Kingfisher will disregard the cache and attempt to initiate a download task for the image source.\n    case forceRefresh\n\n    /// Sets whether Kingfisher should try to load from memory cache first, and then perform a refresh from network.\n    ///\n    /// When set, Kingfisher will attempt to retrieve the image from memory cache first. If the image is not found in \n    /// the memory cache, it will skip the disk cache and download the image again from the network. This is useful\n    /// when you want to display a changeable image with the same URL within the same app session, while avoiding\n    /// multiple downloads.\n    case fromMemoryCacheOrRefresh\n    \n    /// When set, applying a transition to set the image in an image view will occur even when the image is retrieved \n    /// from the cache. Refer to the ``transition(_:)`` option for more details.\n    case forceTransition\n    \n    /// When set, Kingfisher will cache the value only in memory and not on disk.\n    case cacheMemoryOnly\n    \n    /// When set, Kingfisher will wait for the caching operation to be completed before invoking the completion block.\n    case waitForCache\n    \n    /// When set, Kingfisher will attempt to retrieve the image solely from the cache and not from the network.\n    ///\n    /// If the image is not found in the cache, the image retrieval will fail with a\n    /// ``KingfisherError/CacheErrorReason/imageNotExisting(key:)`` error.\n    case onlyFromCache\n    \n    /// Decode the image on a background thread before usage.\n    ///\n    /// This process involves decoding the downloaded image data and performing off-screen rendering to extract pixel\n    ///  information in the background. While this can accelerate display performance, it may require additional time\n    ///  to prepare the image for use.\n    case backgroundDecode\n\n    /// The associated value will be used as the target queue of dispatch callbacks when retrieving images from\n    /// cache. If not set, Kingfisher will use `.mainCurrentOrAsync` for callbacks.\n    ///\n    /// - Note: This option does not affect the callbacks for UI related extension methods. You will always get the\n    /// callbacks called from main queue.\n    \n    /// The associated value will serve as the target queue for dispatch callbacks when retrieving images from the cache.\n    ///\n    /// If not set, Kingfisher will use ``CallbackQueue/mainCurrentOrAsync`` for callbacks.\n    ///\n    /// - Note: This option does not impact the callbacks for UI-related extension methods. Those callbacks will always \n    /// occur on the main queue.\n    case callbackQueue(CallbackQueue)\n    \n    /// The associated value will be used as the scale factor when converting retrieved image data to an image object.\n    ///\n    /// Specify the image scale rather than your screen scale. You should set the correct scale when dealing with 2x or \n    /// 3x retina images. Otherwise, Kingfisher will convert the data to an image object with a scale of 1.0.\n    case scaleFactor(CGFloat)\n\n    /// Determines whether all the animated image data should be preloaded.\n    ///\n    /// The default value is `false`, which means only the following frames will be loaded on demand. If set to `true`, \n    /// all the animated image data will be loaded and decoded into memory.\n    ///\n    /// This option is primarily used for internal backward compatibility. It should not be set directly. Instead, you \n    /// should choose the appropriate image view class to control the GIF data loading. Kingfisher offers two classes\n    /// for displaying GIF images: ``AnimatedImageView``, which does not preload all data, consumes less memory, but uses\n    /// more CPU during display; and a regular image view (`UIImageView` or `NSImageView`), which loads all data at\n    /// once, consumes more memory, but decodes image frames only once.\n    case preloadAllAnimationData\n    \n    /// The contained ``ImageDownloadRequestModifier`` will be used to alter the request before it is sent.\n    ///\n    /// This is the final opportunity to modify the image download request. You can customize the request for various \n    /// purposes, such as adding an authentication token to the header, performing basic HTTP authentication, or URL\n    /// mapping.\n    ///\n    /// By default, the original request is sent without any modifications.\n    case requestModifier(any AsyncImageDownloadRequestModifier)\n\n    /// The contained ``ImageDownloadRedirectHandler`` will be used to alter the request during redirection.\n    ///\n    /// This provides an opportunity to customize the image download request during redirection. You can modify the \n    /// request for various purposes, such as adding an authentication token to the header, performing basic HTTP\n    /// authentication, or URL mapping.\n    ///\n    /// By default, the original redirection request is sent without any modifications.\n    case redirectHandler(any ImageDownloadRedirectHandler)\n\n    /// The processor used in the image retrieval task.\n    ///\n    /// After downloading is complete, a processor will convert the downloaded data into an image and/or apply various \n    /// filters or transformations to it.\n    ///\n    /// If a cache is linked to the downloader (which occurs when you use ``KingfisherManager`` or any of the view\n    /// extension methods), the converted image will also be stored in the cache. If not set, the\n    /// ``DefaultImageProcessor/default`` will be used.\n    case processor(any ImageProcessor)\n\n    /// Offers a ``CacheSerializer`` to convert data into an image object for retrieval from disk cache, or vice versa\n    /// for storage in the disk cache.\n    ///\n    /// If not set, the ``DefaultCacheSerializer/default`` will be used.\n    case cacheSerializer(any CacheSerializer)\n\n    /// An ``ImageModifier`` for making adjustments to an image right before it is used.\n    ///\n    /// If the image was directly fetched from the downloader, the modifier will be applied immediately after the \n    /// ``ImageProcessor``. If the image is retrieved from a cache, the modifier will be applied after the\n    /// ``CacheSerializer``.\n    ///\n    /// Use the ``ImageModifier`` when you need to set properties that do not persist when caching the image with a \n    /// specific image type. Examples include setting the `renderingMode` or `alignmentInsets` of a `UIImage`.\n    case imageModifier(any ImageModifier)\n\n    /// Keep the existing image of image view while setting another image to it.\n    /// By setting this option, the placeholder image parameter of image view extension method\n    /// will be ignored and the current image will be kept while loading or downloading the new image.\n    case keepCurrentImageWhileLoading\n    \n    /// When set, Kingfisher will load only the first frame from an animated image file as a single image.\n    ///\n    /// Loading animated images can consume a significant amount of memory. This option is useful when you want to \n    /// display a static preview of the first frame from an animated image. It will be ignored if the target image is\n    /// not animated image data.\n    case onlyLoadFirstFrame\n    \n    /// When set and an non-default ``ImageProcessor`` is used, Kingfisher will attempt to cache both the final result\n    /// and the original image.\n    ///\n    /// Kingfisher will have the opportunity to use the original image when another processor is applied to the same \n    /// resource, instead of downloading it anew. You can use ``KingfisherOptionsInfoItem/originalCache(_:)`` to\n    /// specify a cache for the original images if necessary.\n    ///\n    /// The original image will only be cached to disk storage.\n    case cacheOriginalImage\n    \n    /// When set and an image retrieval error occurs, Kingfisher will replace the requested image with the provided \n    /// image (or an empty image).\n    ///\n    /// This is useful when you prefer not to display a placeholder during loading but want to use a default image when\n    /// requests fail.\n    case onFailureImage(KFCrossPlatformImage?)\n    \n    /// When set and used in methods of ``ImagePrefetcher``, the prefetching operation will aggressively load the images \n    /// into memory storage.\n    ///\n    /// By default, this option is not included in the options. This means that if the requested image is already in \n    /// the disk cache, Kingfisher will not attempt to load it into memory.\n    case alsoPrefetchToMemory\n    \n    /// When set, disk storage loading will occur in the same calling queue.\n    ///\n    /// By default, disk storage file loading operates on its own queue with asynchronous dispatch behavior. While this \n    /// provides improved non-blocking disk loading performance, it can lead to flickering when you reload an image from\n    /// disk if the image view already has an image set.\n    ///\n    /// Setting this option will eliminate that flickering by keeping all loading in the same queue (typically the UI \n    /// queue if you are using Kingfisher's extension methods to set an image). However, this comes with a tradeoff in\n    /// loading performance.\n    case loadDiskFileSynchronously\n\n    /// Options for controlling the data writing process to disk storage.\n    ///\n    /// When set, these options will be passed to the store operation for new files.\n    case diskStoreWriteOptions(Data.WritingOptions)\n\n    /// When set, use the associated ``StorageExpiration`` value for the memory cache to determine the expiration date.\n    ///\n    /// By default, the underlying ``MemoryStorage/Backend`` uses the expiration defined in its ``MemoryStorage/Config``\n    /// for all items. If this option is set, the ``MemoryStorage/Backend`` will utilize the associated value to \n    /// override the configuration setting for this caching item.\n    case memoryCacheExpiration(StorageExpiration)\n    \n    /// When set, use the associated ``ExpirationExtending`` value for the memory cache to determine the extending policy\n    /// when setting the next expiration date.\n    ///\n    /// The item's expiration date will be extended after access to keep the \"most recently accessed\" items alive for a \n    /// longer duration in the cache.\n    ///\n    /// By default, the underlying ``MemoryStorage/Backend`` uses the initial cache expiration as the extending value,\n    /// which is ``ExpirationExtending/cacheTime``.\n    ///\n    /// - Note: To disable expiration extending entirely, use ``ExpirationExtending/none``.\n    case memoryCacheAccessExtendingExpiration(ExpirationExtending)\n    \n    /// When set, use the associated ``StorageExpiration`` value for the disk cache to determine the expiration date.\n    ///\n    /// By default, the underlying ``DiskStorage/Backend`` uses the expiration defined in its ``DiskStorage/Config``\n    /// for all items. If this option is set, the ``DiskStorage/Backend`` will utilize the associated value to override\n    /// the configuration setting for this caching item.\n    case diskCacheExpiration(StorageExpiration)\n\n    /// When set, use the associated ``ExpirationExtending`` value for the disk cache to determine the extending policy\n    /// when setting the next expiration date.\n    ///\n    /// The item's expiration date will be extended after access to keep the \"most recently accessed\" items alive for a\n    /// longer duration in the cache.\n    ///\n    /// By default, the underlying ``DiskStorage/Backend`` uses the initial cache expiration as the extending value,\n    /// which is ``ExpirationExtending/cacheTime``.\n    ///\n    /// - Note: To disable expiration extending entirely, use ``ExpirationExtending/none``.\n    case diskCacheAccessExtendingExpiration(ExpirationExtending)\n    \n    /// Determines the queue on which image processing should occur.\n    ///\n    /// By default, Kingfisher uses an internal pre-defined serial queue to process images. Use this option to modify\n    /// this behavior.\n    ///\n    /// For instance, you can specify ``CallbackQueue/mainCurrentOrAsync`` to process the image on the main queue,\n    /// preventing potential flickering (but with the risk of blocking the UI, especially if the processor is\n    /// time-consuming).\n    ///\n    /// If you need more control over scheduling (such as limiting concurrency, changing priority, or using a LIFO\n    /// strategy), you can provide an operation queue by using ``CallbackQueue/operationQueue(_:)``.\n    ///\n    /// ```swift\n    /// let queue = OperationQueue()\n    /// // Configure `queue` as needed.\n    /// options = [.processingQueue(.operationQueue(queue))]\n    /// ```\n    ///\n    /// - Note: The execution order depends on the provided queue.\n    case processingQueue(CallbackQueue)\n    \n    /// Enables progressive image loading.\n    ///\n    /// Kingfisher will use the associated ``ImageProgressive`` value to process progressive JPEG data and display \n    /// it progressively, if the image supports it.\n    case progressiveJPEG(ImageProgressive)\n\n    /// Sets a set of alternative sources when the original input `Source` fails to load.\n    ///\n    ///  The `Source`s in the associated\n    /// array will be used to start a new image loading task if the previous task fails due to an error. The image\n    /// source loading process will stop as soon as a source is loaded successfully. If all `[Source]`s are used but\n    /// the loading is still failing, an `imageSettingError` with `alternativeSourcesExhausted` as its reason will be\n    /// thrown out.\n    ///\n    /// This option is useful if you want to implement a fallback solution for setting image.\n    ///\n    /// User cancellation will not trigger the alternative source loading.\n    ///\n    \n    /// Specifies a set of alternative sources to use when the original input ``Source`` fails to load.\n    ///\n    /// The ``Source``s in the associated array will be used to start a new image loading task if the previous task\n    /// fails due to an error. The image source loading process will halt as soon as a source is loaded successfully.\n    /// If all ``Source``s are used, but loading still fails, a\n    /// ``KingfisherError/ImageSettingErrorReason/alternativeSourcesExhausted(_:)``will be used as the error in the\n    /// result.\n    ///\n    /// This option is useful for implementing a fallback solution for image setting.\n    ///\n    /// - Note: User cancellation will not trigger the loading of alternative sources.\n    case alternativeSources([Source])\n\n    /// Provides a retry strategy to use when something goes wrong during the image retrieval process from\n    /// ``KingfisherManager``.\n    ///\n    /// You can define a strategy by creating a type that conforms to the ``RetryStrategy`` protocol. When Kingfisher\n    /// encounters a loading failure, it follows the defined retry strategy and retries until a ``RetryDecision/stop``\n    /// is received.\n    ///\n    /// - Note: All extension methods of Kingfisher (the `kf` extensions on `UIImageView` or `UIButton`, for example)\n    /// retrieve images through ``KingfisherManager``, so the retry strategy also applies when using them. However,\n    /// this option does not apply when passed to an ``ImageDownloader`` or an ``ImageCache`` directly.\n    case retryStrategy(any RetryStrategy)\n\n    /// Specifies the `Source` to load when the user enables Low Data Mode and the original source fails due to the data\n    /// constraint.\n    ///\n    /// When the user enables Low Data Mode in the system settings, and the original source fails with an\n    /// `NSURLErrorNetworkUnavailableReason.constrained` error, Kingfisher uses this source instead to load an image\n    ///  for Low Data Mode.\n    ///\n    /// When this option is set, the `allowsConstrainedNetworkAccess` property of the request for the original source \n    /// will be set to `false`, and the ``Source`` in the associated value will be used to retrieve the image for Low\n    /// Data Mode. Typically, you can provide a low-resolution version of your image or a local image provider to\n    /// display a placeholder to save data usage.\n    ///\n    /// If not set or if the associated optional ``Source`` value is `nil`, the device's Low Data Mode will be ignored,\n    /// and the original source will be loaded following the system default behavior.\n    case lowDataMode(Source?)\n    \n    case forcedCacheFileExtension(String?)\n}\n\n// MARK: - KingfisherParsedOptionsInfo\n\n// Improve performance by parsing the input `KingfisherOptionsInfo` (self) first.\n// So we can prevent the iterating over the options array again and again.\n\n/// Represents the parsed options info used throughout Kingfisher methods.\n///\n/// Each property in this type corresponds to a case member in ``KingfisherOptionsInfoItem``. When a\n///  ``KingfisherOptionsInfo`` is sent to Kingfisher-related methods, it will be parsed and converted to a\n///  ``KingfisherParsedOptionsInfo`` first before passing through the internal methods.\npublic struct KingfisherParsedOptionsInfo: Sendable {\n\n    public var targetCache: ImageCache? = nil\n    public var originalCache: ImageCache? = nil\n    public var downloader: ImageDownloader? = nil\n    public var transition: ImageTransition = .none\n    public var downloadPriority: Float = URLSessionTask.defaultPriority\n    public var forceRefresh = false\n    public var fromMemoryCacheOrRefresh = false\n    public var forceTransition = false\n    public var cacheMemoryOnly = false\n    public var waitForCache = false\n    public var onlyFromCache = false\n    public var backgroundDecode = false\n    public var preloadAllAnimationData = false\n    public var callbackQueue: CallbackQueue = .mainCurrentOrAsync\n    public var scaleFactor: CGFloat = 1.0\n    public var requestModifier: (any AsyncImageDownloadRequestModifier)? = nil\n    public var redirectHandler: (any ImageDownloadRedirectHandler)? = nil\n    public var processor: any ImageProcessor = DefaultImageProcessor.default\n    public var imageModifier: (any ImageModifier)? = nil\n    public var cacheSerializer: any CacheSerializer = DefaultCacheSerializer.default\n    public var keepCurrentImageWhileLoading = false\n    public var onlyLoadFirstFrame = false\n    public var cacheOriginalImage = false\n    public var onFailureImage: Optional<KFCrossPlatformImage?> = .none\n    public var alsoPrefetchToMemory = false\n    public var loadDiskFileSynchronously = false\n    public var diskStoreWriteOptions: Data.WritingOptions = []\n    public var memoryCacheExpiration: StorageExpiration? = nil\n    public var memoryCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime\n    public var diskCacheExpiration: StorageExpiration? = nil\n    public var diskCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime\n    public var processingQueue: CallbackQueue? = nil\n    public var progressiveJPEG: ImageProgressive? = nil\n    public var alternativeSources: [Source]? = nil\n    public var retryStrategy: (any RetryStrategy)? = nil\n    public var lowDataModeSource: Source? = nil\n    public var forcedExtension: String? = nil\n\n    var onDataReceived: [any DataReceivingSideEffect]? = nil\n    \n    public init(_ info: KingfisherOptionsInfo?) {\n        guard let info = info else { return }\n        for option in info {\n            switch option {\n            case .targetCache(let value): targetCache = value\n            case .originalCache(let value): originalCache = value\n            case .downloader(let value): downloader = value\n            case .transition(let value): transition = value\n            case .downloadPriority(let value): downloadPriority = value\n            case .forceRefresh: forceRefresh = true\n            case .fromMemoryCacheOrRefresh: fromMemoryCacheOrRefresh = true\n            case .forceTransition: forceTransition = true\n            case .cacheMemoryOnly: cacheMemoryOnly = true\n            case .waitForCache: waitForCache = true\n            case .onlyFromCache: onlyFromCache = true\n            case .backgroundDecode: backgroundDecode = true\n            case .preloadAllAnimationData: preloadAllAnimationData = true\n            case .callbackQueue(let value): callbackQueue = value\n            case .scaleFactor(let value): scaleFactor = value\n            case .requestModifier(let value): requestModifier = value\n            case .redirectHandler(let value): redirectHandler = value\n            case .processor(let value): processor = value\n            case .imageModifier(let value): imageModifier = value\n            case .cacheSerializer(let value): cacheSerializer = value\n            case .keepCurrentImageWhileLoading: keepCurrentImageWhileLoading = true\n            case .onlyLoadFirstFrame: onlyLoadFirstFrame = true\n            case .cacheOriginalImage: cacheOriginalImage = true\n            case .onFailureImage(let value): onFailureImage = .some(value)\n            case .alsoPrefetchToMemory: alsoPrefetchToMemory = true\n            case .loadDiskFileSynchronously: loadDiskFileSynchronously = true\n            case .diskStoreWriteOptions(let options): diskStoreWriteOptions = options\n            case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration\n            case .memoryCacheAccessExtendingExpiration(let expirationExtending): memoryCacheAccessExtendingExpiration = expirationExtending\n            case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration\n            case .diskCacheAccessExtendingExpiration(let expirationExtending): diskCacheAccessExtendingExpiration = expirationExtending\n            case .processingQueue(let queue): processingQueue = queue\n            case .progressiveJPEG(let value): progressiveJPEG = value\n            case .alternativeSources(let sources): alternativeSources = sources\n            case .retryStrategy(let strategy): retryStrategy = strategy\n            case .lowDataMode(let source): lowDataModeSource = source\n            case .forcedCacheFileExtension(let ext): forcedExtension = ext\n            }\n        }\n\n        if originalCache == nil {\n            originalCache = targetCache\n        }\n    }\n}\n\nextension KingfisherParsedOptionsInfo {\n    var imageCreatingOptions: ImageCreatingOptions {\n        return ImageCreatingOptions(\n            scale: scaleFactor,\n            duration: 0.0,\n            preloadAll: preloadAllAnimationData,\n            onlyFirstFrame: onlyLoadFirstFrame)\n    }\n}\n\nprotocol DataReceivingSideEffect: AnyObject, Sendable {\n    var onShouldApply: () -> Bool { get set }\n    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data)\n}\n\nclass ImageLoadingProgressSideEffect: DataReceivingSideEffect, @unchecked Sendable {\n\n    private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.ImageLoadingProgressSideEffectPropertyQueue\")\n    \n    private var _onShouldApply: () -> Bool = { return true }\n    \n    var onShouldApply: () -> Bool {\n        get { propertyQueue.sync { _onShouldApply } }\n        set { propertyQueue.sync { _onShouldApply = newValue } }\n    }\n    \n    let block: DownloadProgressBlock\n\n    init(_ block: @escaping DownloadProgressBlock) {\n        self.block = block\n    }\n\n    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {\n        DispatchQueue.main.async {\n            guard self.onShouldApply() else { return }\n            guard let expectedContentLength = task.task.response?.expectedContentLength,\n                      expectedContentLength != -1 else\n            {\n                return\n            }\n\n            let dataLength = Int64(task.mutableData.count)\n            self.block(dataLength, expectedContentLength)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Image/Filter.swift",
    "content": "//\n//  Filter.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2016/08/31.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\nimport CoreImage\n\n// Reuses the same CI Context for all CI drawings.\nstruct SendableBox<T>: @unchecked Sendable {\n    let value: T\n}\n\nprivate let ciContext = SendableBox(value: CIContext(options: nil))\n\n/// Represents the type of transformer method, which will be used to provide a ``Filter``.\npublic typealias Transformer = (CIImage) -> CIImage?\n\n/// Represents an ``ImageProcessor`` based on a ``Filter``, for images of `CIImage`.\n///\n/// You can use any ``Filter``, or in other words, a ``Transformer`` to convert a `CIImage` to another, to create a\n/// ``ImageProcessor`` type easily.\npublic protocol CIImageProcessor: ImageProcessor {\n    var filter: Filter { get }\n}\n\nextension CIImageProcessor {\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.apply(filter)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// A wrapper struct for a `Transformer` of CIImage filters. \n///\n/// A ``Filter`` value can be used to create an ``ImageProcessor`` for `CIImage`s.\npublic struct Filter {\n    \n    let transform: Transformer\n    \n    /// Creates a ``Filter`` from a given ``Transformer``.\n    ///\n    /// - Parameter transform: The value defines how a `CIImage` can be converted to another one.\n    public init(transform: @escaping Transformer) {\n        self.transform = transform\n    }\n    \n    /// Tint filter that applies a tint color to images.\n    public static let tint: @Sendable (KFCrossPlatformColor) -> Filter = {\n        color in\n        Filter {\n            input in\n            \n            let colorFilter = CIFilter(name: \"CIConstantColorGenerator\")!\n            colorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey)\n            \n            let filter = CIFilter(name: \"CISourceOverCompositing\")!\n            \n            let colorImage = colorFilter.outputImage\n            filter.setValue(colorImage, forKey: kCIInputImageKey)\n            filter.setValue(input, forKey: kCIInputBackgroundImageKey)\n            \n            return filter.outputImage?.cropped(to: input.extent)\n        }\n    }\n    \n    /// Represents color control elements.\n    ///\n    /// It contains necessary variables which can be applied as a filter to `CIImage.applyingFilter` feature as\n    /// \"CIColorControls\".\n    public struct ColorElement {\n        public let brightness: CGFloat\n        public let contrast: CGFloat\n        public let saturation: CGFloat\n        public let inputEV: CGFloat\n        \n        /// Creates a ``ColorElement`` value with given parameters.\n        /// - Parameters:\n        ///   - brightness: The brightness change applied to the image.\n        ///   - contrast: The contrast change applied to the image.\n        ///   - saturation: The saturation change applied to the image.\n        ///   - inputEV: The EV (F-stops brighter or darker) change applied to the image.\n        public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) {\n            self.brightness = brightness\n            self.contrast = contrast\n            self.saturation = saturation\n            self.inputEV = inputEV\n        }\n    }\n    \n    /// Color control filter that applies color control changes to images.\n    public static let colorControl: @Sendable (ColorElement) -> Filter = { arg -> Filter in\n        return Filter { input in\n            let paramsColor = [kCIInputBrightnessKey: arg.brightness,\n                                 kCIInputContrastKey: arg.contrast,\n                               kCIInputSaturationKey: arg.saturation]\n            let blackAndWhite = input.applyingFilter(\"CIColorControls\", parameters: paramsColor)\n            let paramsExposure = [kCIInputEVKey: arg.inputEV]\n            return blackAndWhite.applyingFilter(\"CIExposureAdjust\", parameters: paramsExposure)\n        }\n    }\n}\n\nextension KingfisherWrapper where Base: KFCrossPlatformImage {\n\n    /// Applies a `Filter` containing a `CIImage` transformer to `self`.\n    ///\n    /// - Parameters:\n    ///     - filter: The filter used to transform `self`.\n    /// - Returns: A transformed image by the input `Filter`.\n    ///\n    /// > Important: Only CG-based images are supported. If an error occurs during transformation,\n    /// ``KingfisherWrapper/base`` will be returned.\n    public func apply(_ filter: Filter) -> KFCrossPlatformImage {\n        \n        guard let cgImage = cgImage else {\n            assertionFailure(\"[Kingfisher] Tint image only works for CG-based image.\")\n            return base\n        }\n        \n        let inputImage = CIImage(cgImage: cgImage)\n        guard let outputImage = filter.transform(inputImage) else {\n            return base\n        }\n\n        guard let result = ciContext.value.createCGImage(outputImage, from: outputImage.extent) else {\n            assertionFailure(\"[Kingfisher] Can not make an tint image within context.\")\n            return base\n        }\n        \n        #if os(macOS)\n            return fixedForRetinaPixel(cgImage: result, to: size)\n        #else\n            return KFCrossPlatformImage(cgImage: result, scale: base.scale, orientation: base.imageOrientation)\n        #endif\n    }\n\n}\n\n#endif\n"
  },
  {
    "path": "Sources/Image/GIFAnimatedImage.swift",
    "content": "//\n//  AnimatedImage.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/09/26.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport ImageIO\n\n/// Represents a set of image creation options used in Kingfisher.\npublic struct ImageCreatingOptions: Equatable {\n\n    /// The target scale of the image that needs to be created.\n    public var scale: CGFloat\n\n    /// The expected animation duration if an animated image is being created.\n    public var duration: TimeInterval\n\n    /// For an animated image, indicates whether or not all frames should be loaded before displaying.\n    public var preloadAll: Bool\n\n    /// For an animated image, indicates whether only the first image should be\n    /// loaded as a static image. It is useful for previewing an animated image.\n    public var onlyFirstFrame: Bool\n    \n    /// Creates an `ImageCreatingOptions` object.\n    ///\n    /// - Parameters:\n    ///     - scale: The target scale of the image that needs to be created. Default is `1.0`.\n    ///     - duration: The expected animation duration if an animated image is being created.\n    ///                 A value less than or equal to `0.0` means the animated image duration will\n    ///                 be determined by the frame data. Default is `0.0`.\n    ///     - preloadAll: For an animated image, whether or not all frames should be loaded before displaying.\n    ///                   Default is `false`.\n    ///     - onlyFirstFrame: For an animated image, whether only the first image should be\n    ///                       loaded as a static image. It is useful for previewing an animated image.\n    ///                       Default is `false`.\n    public init(\n        scale: CGFloat = 1.0,\n        duration: TimeInterval = 0.0,\n        preloadAll: Bool = false,\n        onlyFirstFrame: Bool = false\n    )\n    {\n        self.scale = scale\n        self.duration = duration\n        self.preloadAll = preloadAll\n        self.onlyFirstFrame = onlyFirstFrame\n    }\n}\n\n/// Represents the decoding for a GIF image. This class extracts frames from an ``ImageFrameSource``, and then\n/// holds the images for later use.\npublic class GIFAnimatedImage {\n    let images: [KFCrossPlatformImage]\n    let duration: TimeInterval\n    \n    init?(from frameSource: any ImageFrameSource, options: ImageCreatingOptions) {\n        let frameCount = frameSource.frameCount\n        var images = [KFCrossPlatformImage]()\n        var gifDuration = 0.0\n        \n        for i in 0 ..< frameCount {\n            guard let imageRef = frameSource.frame(at: i) else {\n                return nil\n            }\n            \n            if frameCount == 1 {\n                gifDuration = .infinity\n            } else {\n                // Get current animated GIF frame duration\n                gifDuration += frameSource.duration(at: i)\n            }\n            images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil))\n            if options.onlyFirstFrame { break }\n        }\n        self.images = images\n        self.duration = gifDuration\n    }\n    \n    convenience init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) {\n        let frameSource = CGImageFrameSource(data: nil, imageSource: imageSource, options: info)\n        self.init(from: frameSource, options: options)\n    }\n    \n    /// Calculates the frame duration for a GIF frame out of the `kCGImagePropertyGIFDictionary` dictionary.\n    public static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval {\n        let defaultFrameDuration = 0.1\n        guard let gifInfo = gifInfo else { return defaultFrameDuration }\n        \n        let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber\n        let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber\n        let duration = unclampedDelayTime ?? delayTime\n        \n        guard let frameDuration = duration else { return defaultFrameDuration }\n        return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration\n    }\n\n    /// Calculates the frame duration at a specific index for a GIF from an `CGImageSource`.\n    /// \n    /// - Parameters:\n    ///   - imageSource: The image source where the animated image information should be extracted from.\n    ///   - index: The index of the target frame in the image.\n    /// - Returns: The time duration of the frame at given index in the image.\n    public static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval {\n        guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil)\n            as? [String: Any] else { return 0.0 }\n\n        let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any]\n        return getFrameDuration(from: gifInfo)\n    }\n}\n\n/// Represents a frame source for an animated image.\npublic protocol ImageFrameSource {\n    \n    /// Source data associated with this frame source.\n    var data: Data? { get }\n    \n    /// Count of the total frames in this frame source.\n    var frameCount: Int { get }\n    \n    /// Retrieves the frame at a specific index. \n    ///\n    /// The resulting image is expected to be no larger than `maxSize`. If the index is invalid,\n    /// implementors should return `nil`.\n    func frame(at index: Int, maxSize: CGSize?) -> CGImage?\n    \n    /// Retrieves the duration at a specific index. If the index is invalid, implementors should return `0.0`.\n    func duration(at index: Int) -> TimeInterval\n    \n    /// Creates a copy of the current `ImageFrameSource` instance.\n    ///\n    /// - Returns: A new instance of the same type as `self` with identical properties.\n    ///            If not overridden by conforming types, this default implementation\n    ///            simply returns `self`, which may not create an actual copy if the type is a reference type.\n    func copy() -> Self\n}\n\npublic extension ImageFrameSource {\n    \n    /// Retrieves the frame at a specific index. If the index is invalid, implementors should return `nil`.\n    func frame(at index: Int) -> CGImage? {\n        return frame(at: index, maxSize: nil)\n    }\n    \n    func copy() -> Self {\n        return self\n    }\n}\n\nstruct CGImageFrameSource: ImageFrameSource {\n    let data: Data?\n    let imageSource: CGImageSource\n    let options: [String: Any]?\n    \n    var frameCount: Int {\n        return CGImageSourceGetCount(imageSource)\n    }\n\n    func frame(at index: Int, maxSize: CGSize?) -> CGImage? {\n        var options = self.options as? [CFString: Any]\n        if let maxSize = maxSize, maxSize != .zero {\n            options = (options ?? [:]).merging([\n                kCGImageSourceCreateThumbnailFromImageIfAbsent: true,\n                kCGImageSourceCreateThumbnailWithTransform: true,\n                kCGImageSourceShouldCacheImmediately: true,\n                kCGImageSourceThumbnailMaxPixelSize: max(maxSize.width, maxSize.height)\n            ], uniquingKeysWith: { $1 })\n        }\n        return CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?)\n    }\n\n    func duration(at index: Int) -> TimeInterval {\n        return GIFAnimatedImage.getFrameDuration(from: imageSource, at: index)\n    }\n    \n    func copy() -> Self {\n        guard let data = data, let source = CGImageSourceCreateWithData(data as CFData, options as CFDictionary?) else {\n            return self\n        }\n        return CGImageFrameSource(data: data, imageSource: source, options: options)\n    }\n}\n\n"
  },
  {
    "path": "Sources/Image/GraphicsContext.swift",
    "content": "//\n//  GraphicsContext.swift\n//  Kingfisher\n//\n//  Created by taras on 19/04/2021.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if os(macOS) || os(watchOS)\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\n#endif\n#if canImport(UIKit)\nimport UIKit\n#endif\n\nenum GraphicsContext {\n    static func begin(size: CGSize, scale: CGFloat) {\n        #if os(macOS)\n        NSGraphicsContext.saveGraphicsState()\n        #elseif os(watchOS)\n        UIGraphicsBeginImageContextWithOptions(size, false, scale)\n        #else\n        assertionFailure(\"This method is deprecated on the current platform and should not be used.\")\n        #endif\n    }\n    \n    static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> CGContext? {\n        #if os(macOS)\n        let descriptor = BitmapContextDescriptor(size: size, cgImage: cgImage)\n        guard let context = descriptor.makeContext() else {\n            assertionFailure(\"[Kingfisher] Image context cannot be created.\")\n            return nil\n        }\n        let graphicsContext = NSGraphicsContext(cgContext: context, flipped: false)\n        graphicsContext.imageInterpolation = .high\n        NSGraphicsContext.current = graphicsContext\n        return graphicsContext.cgContext\n        #elseif os(watchOS)\n        guard let context = UIGraphicsGetCurrentContext() else {\n            return nil\n        }\n        if inverting { // If drawing a CGImage, we need to make context flipped.\n            context.scaleBy(x: 1.0, y: -1.0)\n            context.translateBy(x: 0, y: -size.height)\n        }\n        return context\n        #else\n        assertionFailure(\"This method is deprecated on the current platform and should not be used.\")\n        return nil\n        #endif\n    }\n    \n    static func end() {\n        #if os(macOS)\n        NSGraphicsContext.restoreGraphicsState()\n        #elseif os(watchOS)\n        UIGraphicsEndImageContext()\n        #else\n        assertionFailure(\"This method is deprecated on the current platform and should not be used.\")\n        #endif\n    }\n}\n\n#endif\n\n#if os(macOS)\nprivate struct BitmapContextDescriptor {\n    let width: Int\n    let height: Int\n    let bitsPerComponent: Int\n    let bytesPerRow: Int\n    let colorSpace: CGColorSpace\n    let bitmapInfo: CGBitmapInfo\n    \n    init(size: CGSize, cgImage: CGImage?) {\n        width = max(Int(size.width.rounded(.down)), 1)\n        height = max(Int(size.height.rounded(.down)), 1)\n        colorSpace = BitmapContextDescriptor.resolveColorSpace(from: cgImage)\n        bitsPerComponent = BitmapContextDescriptor.supportedBitsPerComponent(from: cgImage)\n        let componentCount = colorSpace.numberOfComponents\n        let hasAlpha = BitmapContextDescriptor.containsAlpha(from: cgImage)\n        bitmapInfo = BitmapContextDescriptor.bitmapInfo(componentCount: componentCount, hasAlpha: hasAlpha)\n        let channelsPerPixel = BitmapContextDescriptor.channelsPerPixel(componentCount: componentCount, hasAlpha: hasAlpha)\n        let bitsPerPixel = channelsPerPixel * bitsPerComponent\n        bytesPerRow = BitmapContextDescriptor.alignedBytesPerRow(bitsPerPixel: bitsPerPixel, width: width)\n    }\n    \n    func makeContext() -> CGContext? {\n        CGContext(\n            data: nil,\n            width: width,\n            height: height,\n            bitsPerComponent: bitsPerComponent,\n            bytesPerRow: bytesPerRow,\n            space: colorSpace,\n            bitmapInfo: bitmapInfo.rawValue\n        )\n    }\n    \n    private static func supportedBitsPerComponent(from cgImage: CGImage?) -> Int {\n        guard let bits = cgImage?.bitsPerComponent, bits > 0 else { return 8 }\n        if bits <= 8 { return 8 }\n        return 16\n    }\n    \n    private static func resolveColorSpace(from cgImage: CGImage?) -> CGColorSpace {\n        guard let cgColorSpace = cgImage?.colorSpace else {\n            return CGColorSpaceCreateDeviceRGB()\n        }\n        let components = cgColorSpace.numberOfComponents\n        if components == 1 || components == 3 {\n            return cgColorSpace\n        }\n        return CGColorSpaceCreateDeviceRGB()\n    }\n    \n    private static func containsAlpha(from cgImage: CGImage?) -> Bool {\n        guard let alphaInfo = cgImage?.alphaInfo else { return true }\n        switch alphaInfo {\n        case .none, .noneSkipFirst, .noneSkipLast:\n            return false\n        default:\n            return true\n        }\n    }\n    \n    private static func bitmapInfo(componentCount: Int, hasAlpha: Bool) -> CGBitmapInfo {\n        let alphaInfo: CGImageAlphaInfo\n        if componentCount == 1 {\n            alphaInfo = hasAlpha ? .premultipliedLast : .none\n        } else {\n            alphaInfo = hasAlpha ? .premultipliedLast : .noneSkipLast\n        }\n        return CGBitmapInfo(rawValue: alphaInfo.rawValue)\n    }\n    \n    private static func channelsPerPixel(componentCount: Int, hasAlpha: Bool) -> Int {\n        if componentCount == 1 {\n            return hasAlpha ? 2 : 1\n        }\n        return hasAlpha ? componentCount + 1 : componentCount + 1\n    }\n    \n    private static func alignedBytesPerRow(bitsPerPixel: Int, width: Int) -> Int {\n        let rawBytes = (bitsPerPixel * width + 7) / 8\n        return (rawBytes + 0x3F) & ~0x3F\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Image/Image.swift",
    "content": "//\n//  Image.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 16/1/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n\n#if os(macOS)\nimport AppKit\n#else // os(macOS)\nimport UIKit\nimport MobileCoreServices\n#endif // os(macOS)\n\n#if !os(watchOS)\nimport CoreImage\n#endif\n\nimport CoreGraphics\nimport ImageIO\n\n#if canImport(UniformTypeIdentifiers)\nimport UniformTypeIdentifiers\n#endif\n\n#if compiler(>=5.10)\nnonisolated(unsafe) private let animatedImageDataKey = malloc(1)!\nnonisolated(unsafe) private let imageFrameCountKey = malloc(1)!\nnonisolated(unsafe) private let imageSourceKey = malloc(1)!\nnonisolated(unsafe) private let imageCreatingOptionsKey = malloc(1)!\n#if os(macOS)\nnonisolated(unsafe) private let imagesKey = malloc(1)!\nnonisolated(unsafe) private let durationKey = malloc(1)!\n#endif // os(macOS)\n#else // compiler(>=5.10)\nprivate let animatedImageDataKey = malloc(1)!\nprivate let imageFrameCountKey = malloc(1)!\nprivate let imageSourceKey = malloc(1)!\nprivate let imageCreatingOptionsKey = malloc(1)!\n#if os(macOS)\nprivate let imagesKey = malloc(1)!\nprivate let durationKey = malloc(1)!\n#endif // os(macOS)\n#endif // compiler(>=5.10)\n\n// MARK: - Image Properties\nextension KingfisherWrapper where Base: KFCrossPlatformImage {\n    private(set) var animatedImageData: Data? {\n        get { return getAssociatedObject(base, animatedImageDataKey) }\n        set { setRetainedAssociatedObject(base, animatedImageDataKey, newValue) }\n    }\n    \n    private(set) var imageCreatingOptions: ImageCreatingOptions? {\n        get { return getAssociatedObject(base, imageCreatingOptionsKey) }\n        set { setRetainedAssociatedObject(base, imageCreatingOptionsKey, newValue) }\n    }\n    \n    public var imageFrameCount: Int? {\n        get { return getAssociatedObject(base, imageFrameCountKey) }\n        set { setRetainedAssociatedObject(base, imageFrameCountKey, newValue) }\n    }\n    \n    #if os(macOS)\n    var cgImage: CGImage? {\n        return base.cgImage(forProposedRect: nil, context: nil, hints: nil)\n    }\n    \n    var scale: CGFloat {\n        return 1.0\n    }\n    \n    private(set) var images: [KFCrossPlatformImage]? {\n        get { return getAssociatedObject(base, imagesKey) }\n        set { setRetainedAssociatedObject(base, imagesKey, newValue) }\n    }\n    \n    private(set) var duration: TimeInterval {\n        get { return getAssociatedObject(base, durationKey) ?? 0.0 }\n        set { setRetainedAssociatedObject(base, durationKey, newValue) }\n    }\n    \n    var size: CGSize {\n        // Prefer to use pixel size of the image\n        let pixelSize = base.representations.reduce(.zero) { size, rep in\n            CGSize(\n                width: max(size.width, CGFloat(rep.pixelsWide)),\n                height: max(size.height, CGFloat(rep.pixelsHigh))\n            )\n        }\n        // If the pixel size is zero (SVG or PDF, for example), use the size of the image.\n        return pixelSize == .zero ? base.representations.reduce(.zero) { size, rep in\n            CGSize(\n                width: max(size.width, CGFloat(rep.size.width)),\n                height: max(size.height, CGFloat(rep.size.height))\n            )\n        } : pixelSize\n    }\n    #else\n    var cgImage: CGImage? { return base.cgImage }\n    var scale: CGFloat { return base.scale }\n    var images: [KFCrossPlatformImage]? { return base.images }\n    var duration: TimeInterval { return base.duration }\n    var size: CGSize { return base.size }\n    \n    /// The source reference for the current image.\n    public var imageSource: CGImageSource? {\n        get {\n            guard let frameSource = frameSource as? CGImageFrameSource else { return nil }\n            return frameSource.imageSource\n        }\n    }\n    #endif\n    \n    /// The custom frame source for the current image.\n    public private(set) var frameSource: (any ImageFrameSource)? {\n        get { return getAssociatedObject(base, imageSourceKey) }\n        set { setRetainedAssociatedObject(base, imageSourceKey, newValue) }\n    }\n\n    /// Copies Kingfisher internal image states from `base` to a `target` image.\n    ///\n    /// This includes the embedded animated image data and related metadata that are used by Kingfisher for caching and\n    /// animated image rendering. It is useful when a custom processor creates and returns a new image instance from\n    /// an animated image in `.image` branch.\n    ///\n    /// - Important: This method does not make the `target` image animated by itself. It only propagates Kingfisher's\n    ///   internal metadata so the cache can preserve the original animated bytes when possible.\n    ///\n    /// - Parameter target: The target image to which the internal states will be copied.\n    public func copyKingfisherState(to target: KFCrossPlatformImage) {\n        target.kf.animatedImageData = animatedImageData\n        target.kf.imageFrameCount = imageFrameCount\n        target.kf.frameSource = frameSource\n        target.kf.imageCreatingOptions = imageCreatingOptions\n        #if os(macOS)\n        target.kf.images = images\n        target.kf.duration = duration\n        #endif\n    }\n\n    // Bitmap memory cost with bytes.\n    var cost: Int {\n        let pixel = Int(size.width * size.height * scale * scale)\n        guard let cgImage = cgImage else {\n            return pixel * 4\n        }\n        let bytesPerPixel = cgImage.bitsPerPixel / 8\n        guard let imageCount = images?.count else {\n            return pixel * bytesPerPixel\n        }\n        return pixel * bytesPerPixel * imageCount\n    }\n}\n\n// MARK: - Image Conversion\nextension KingfisherWrapper where Base: KFCrossPlatformImage {\n    #if os(macOS)\n    static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage {\n        return KFCrossPlatformImage(cgImage: cgImage, size: .zero)\n    }\n    \n    /// The normalized image. On macOS, this getter returns the image itself without performing any additional operations.\n    public var normalized: KFCrossPlatformImage { return base }\n    #else\n\n    /// Create an image from a given `CGImage` with specified scale and orientation, tailored for `refImage`. This\n    /// method signature is designed for compatibility with macOS versions.\n    ///\n    /// - Parameters:\n    ///   - cgImage: The `CGImage` which is used to create the `UIImage` object.\n    ///   - scale: The scale.\n    ///   - refImage: The ref image which is used to determine the image orientation.\n    /// - Returns: The created image object.\n    static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage {\n        return KFCrossPlatformImage(cgImage: cgImage, scale: scale, orientation: refImage?.imageOrientation ?? .up)\n    }\n    \n    /// The normalized image for the current `base` image.\n    ///\n    /// This method attempts to redraw the image, taking orientation and scale into account.\n    public var normalized: KFCrossPlatformImage {\n        // prevent animated image (GIF) lose it's images\n        guard images == nil else { return base.copy() as! KFCrossPlatformImage }\n        // No need to do anything if already up\n        guard base.imageOrientation != .up else { return base.copy() as! KFCrossPlatformImage }\n\n        return draw(to: size, inverting: true, refImage: KFCrossPlatformImage()) {\n            fixOrientation(in: $0)\n            return true\n        }\n    }\n\n    func fixOrientation(in context: CGContext) {\n        guard let cgImage else { return }\n\n        var transform = CGAffineTransform.identity\n        let orientation = base.imageOrientation\n\n        switch orientation {\n        case .down, .downMirrored:\n            transform = transform.translatedBy(x: size.width, y: size.height)\n            transform = transform.rotated(by: .pi)\n        case .left, .leftMirrored:\n            transform = transform.translatedBy(x: size.width, y: 0)\n            transform = transform.rotated(by: .pi / 2.0)\n        case .right, .rightMirrored:\n            transform = transform.translatedBy(x: 0, y: size.height)\n            transform = transform.rotated(by: .pi / -2.0)\n        case .up, .upMirrored:\n            break\n        @unknown default:\n            break\n        }\n\n        // Flip image one more time if needed for mirrored images. This is to prevent the flipped image.\n        switch orientation {\n        case .upMirrored, .downMirrored:\n            transform = transform.translatedBy(x: size.width, y: 0)\n            transform = transform.scaledBy(x: -1, y: 1)\n        case .leftMirrored, .rightMirrored:\n            transform = transform.translatedBy(x: size.height, y: 0)\n            transform = transform.scaledBy(x: -1, y: 1)\n        case .up, .down, .left, .right:\n            break\n        @unknown default:\n            break\n        }\n\n        context.concatenate(transform)\n        switch orientation {\n        case .left, .leftMirrored, .right, .rightMirrored:\n            context.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.height, height: size.width))\n        default:\n            context.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))\n        }\n    }\n    #endif\n}\n\n// MARK: - Image Representation\nextension KingfisherWrapper where Base: KFCrossPlatformImage {\n    /// Returns a data object that contains the specified image in PNG format.\n    ///\n    /// - Returns: PNG data of image.\n    public func pngRepresentation() -> Data? {\n        #if os(macOS)\n            guard let cgImage = cgImage else {\n                return nil\n            }\n            let rep = NSBitmapImageRep(cgImage: cgImage)\n            return rep.representation(using: .png, properties: [:])\n        #else\n            return base.pngData()\n        #endif\n    }\n\n    /// Returns a data object that contains the specified image in JPEG format.\n    ///\n    /// - Parameter compressionQuality: The compression quality when converting image to JPEG data.\n    /// - Returns: JPEG data of image.\n    public func jpegRepresentation(compressionQuality: CGFloat) -> Data? {\n        #if os(macOS)\n            guard let cgImage = cgImage else {\n                return nil\n            }\n            let rep = NSBitmapImageRep(cgImage: cgImage)\n            return rep.representation(using:.jpeg, properties: [.compressionFactor: compressionQuality])\n        #else\n            return base.jpegData(compressionQuality: compressionQuality)\n        #endif\n    }\n\n    /// Returns GIF representation of `base` image.\n    ///\n    /// - Returns: Original GIF data of image.\n    public func gifRepresentation() -> Data? {\n        return animatedImageData\n    }\n\n    /// Returns a data representation for the `base` image with the specified `format`.\n    ///\n    /// - Parameters:\n    ///   - format: The desired format for the output data. If set to `unknown`, the `base` image will be\n    ///             converted to PNG representation.\n    ///   - compressionQuality: The compression quality when converting the image to a lossy format data.\n    ///\n    /// - Returns: The resulting data representation.\n    public func data(format: ImageFormat, compressionQuality: CGFloat = 1.0) -> Data? {\n        return autoreleasepool { () -> Data? in\n            let data: Data?\n            switch format {\n            case .PNG: data = pngRepresentation()\n            case .JPEG: data = jpegRepresentation(compressionQuality: compressionQuality)\n            case .GIF: data = gifRepresentation()\n            case .unknown: data = normalized.kf.pngRepresentation()\n            }\n            \n            return data\n        }\n    }\n}\n\n// MARK: - Creating Images\nextension KingfisherWrapper where Base: KFCrossPlatformImage {\n    \n    /// Creates an animated image from provided data and options.\n    ///\n    /// - Parameters:\n    ///   - data: The data containing the animated image.\n    ///   - options: Options to be used when creating the animated image.\n    /// - Returns: An `Image` object representing the animated image. It's structured as an array of image frames, \n    /// each with a specific duration. Returns `nil` if any issues occur during animated image creation.\n    ///\n    /// - Note: Currently, only GIF data is supported.\n    public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {\n        #if os(visionOS)\n        let info: [String: Any] = [\n            kCGImageSourceShouldCache as String: true,\n            kCGImageSourceTypeIdentifierHint as String: UTType.gif.identifier\n        ]\n        #else\n        let info: [String: Any] = [\n            kCGImageSourceShouldCache as String: true,\n            kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF\n        ]\n        #endif\n        \n        guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {\n            return nil\n        }\n        let frameSource = CGImageFrameSource(data: data, imageSource: imageSource, options: info)\n        #if os(macOS)\n        let baseImage = KFCrossPlatformImage(data: data)\n        #else\n        let baseImage = KFCrossPlatformImage(data: data, scale: options.scale)\n        #endif\n        return animatedImage(source: frameSource, options: options, baseImage: baseImage)\n    }\n    \n    /// Creates an animated image from a given frame source.\n    ///\n    /// - Parameters:\n    ///   - source: The frame source from which to create the animated image.\n    ///   - options: Options to be used during animated image creation.\n    ///   - baseImage: An optional image object to serve as the key frame of the animated image. If `nil`, the first\n    ///                frame of the `source` will be used.\n    /// - Returns: An `Image` object representing the animated image. It consists of an array of image frames, each with a\n    ///            specific duration. Returns `nil` if any issues arise during animated image creation.\n    public static func animatedImage(source: any ImageFrameSource, options: ImageCreatingOptions, baseImage: KFCrossPlatformImage? = nil) -> KFCrossPlatformImage? {\n        #if os(macOS)\n        guard let animatedImage = GIFAnimatedImage(from: source, options: options) else {\n            return nil\n        }\n        var image: KFCrossPlatformImage?\n        if options.onlyFirstFrame {\n            image = animatedImage.images.first\n        } else {\n            if let baseImage = baseImage {\n                image = baseImage\n            } else {\n                image = animatedImage.images.first\n            }\n            var kf = image?.kf\n            kf?.images = animatedImage.images\n            kf?.duration = animatedImage.duration\n        }\n        image?.kf.animatedImageData = source.data\n        image?.kf.imageFrameCount = source.frameCount\n        image?.kf.frameSource = source\n        image?.kf.imageCreatingOptions = options\n        return image\n        #else\n        \n        var image: KFCrossPlatformImage?\n        if options.preloadAll || options.onlyFirstFrame {\n            // Use `images` image if you want to preload all animated data\n            guard let animatedImage = GIFAnimatedImage(from: source, options: options) else {\n                return nil\n            }\n            if options.onlyFirstFrame {\n                image = animatedImage.images.first\n            } else {\n                let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration\n                image = .animatedImage(with: animatedImage.images, duration: duration)\n            }\n            image?.kf.animatedImageData = source.data\n        } else {\n            if let baseImage = baseImage {\n                image = baseImage\n            } else {\n                guard let firstFrame = source.frame(at: 0) else {\n                    return nil\n                }\n                image = KFCrossPlatformImage(cgImage: firstFrame, scale: options.scale, orientation: .up)\n            }\n            var kf = image?.kf\n            kf?.frameSource = source\n            kf?.animatedImageData = source.data\n        }\n        \n        image?.kf.imageFrameCount = source.frameCount\n        image?.kf.imageCreatingOptions = options\n        return image\n        #endif\n    }\n\n    /// Creates an image from provided data and options. Supported formats include `.JPEG`, `.PNG`, or `.GIF`. For \n    /// other image formats, the system's image initializer will be used. If no image object can be created from the\n    /// given `data`, `nil` will be returned.\n    ///\n    /// - Parameters:\n    ///   - data: The data representing the image.\n    ///   - options: Options to be used when creating the image.\n    /// - Returns: An `Image` object representing the image if successfully created. If the `data` is invalid or \n    /// unsupported, `nil` will be returned.\n    public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {\n        var image: KFCrossPlatformImage?\n        switch data.kf.imageFormat {\n        case .JPEG:\n            image = KFCrossPlatformImage(data: data, scale: options.scale)\n        case .PNG:\n            image = KFCrossPlatformImage(data: data, scale: options.scale)\n        case .GIF:\n            image = KingfisherWrapper.animatedImage(data: data, options: options)\n        case .unknown:\n            image = KFCrossPlatformImage(data: data, scale: options.scale)\n        }\n        return image\n    }\n\n    /// Creates a downsampled image from the given data to a specified size and scale.\n    ///\n    /// - Parameters:\n    ///   - data: The image data containing a JPEG or PNG image.\n    ///   - pointSize: The target size in points to which the image should be downsampled.\n    ///   - scale: The scale of the resulting image.\n    /// - Returns: A downsampled `Image` object adhering to the specified conditions.\n    ///\n    /// Unlike image `resize` methods, downsampling does not render the original input image in pixel format.\n    /// Instead, it downsamples directly from the image data, making it more memory-efficient and friendly. Whenever\n    /// possible, consider using downsampling.\n    ///\n    /// > Important: The `pointSize` should be smaller than the size of the input image. If it is larger than the original image\n    /// > size, the resulting image will have the same dimensions as the input without downsampling.\n    public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? {\n        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary\n        guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else {\n            return nil\n        }\n        \n        let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale\n        let downsampleOptions: [CFString : Any] = [\n            kCGImageSourceCreateThumbnailFromImageAlways: true,\n            kCGImageSourceShouldCacheImmediately: true,\n            kCGImageSourceCreateThumbnailWithTransform: true,\n            kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels\n        ]\n        guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions as CFDictionary) else {\n            return nil\n        }\n        return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil)\n    }\n}\n"
  },
  {
    "path": "Sources/Image/ImageDrawing.swift",
    "content": "//\n//  ImageDrawing.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/09/28.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Accelerate\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\n#endif\n#if canImport(UIKit)\nimport UIKit\n#endif\n\nextension KingfisherWrapper where Base: KFCrossPlatformImage {\n    // MARK: - Image Transforming\n    \n    // MARK: Blend Mode\n    \n#if !os(macOS)\n    /// Create an image from the `base` image and apply a blend mode.\n    ///\n    /// - Parameters:\n    ///   - blendMode: The blend mode to be applied to the image.\n    ///   - alpha: The alpha value to be used for the image.\n    ///   - backgroundColor: The background color for the output image.\n    /// - Returns: An image with the specified blend mode applied.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    public func image(withBlendMode blendMode: CGBlendMode,\n                      alpha: CGFloat = 1.0,\n                      backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage\n    {\n        guard let _ = cgImage else {\n            assertionFailure(\"[Kingfisher] Blend mode image only works for CG-based image.\")\n            return base\n        }\n        \n        let rect = CGRect(origin: .zero, size: size)\n        return draw(to: rect.size, inverting: false) { _ in\n            if let backgroundColor = backgroundColor {\n                backgroundColor.setFill()\n                UIRectFill(rect)\n            }\n            \n            base.draw(in: rect, blendMode: blendMode, alpha: alpha)\n            return false\n        }\n    }\n#endif\n    \n#if os(macOS)\n    // MARK: Compositing\n    \n    /// Create an image from the `base` image and apply a compositing operation.\n    ///\n    /// - Parameters:\n    ///   - compositingOperation: The compositing operation to be applied to the image.\n    ///   - alpha: The alpha value to be used for the image.\n    ///   - backgroundColor: The background color for the output image.\n    /// - Returns: An image with the specified compositing operation applied.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    public func image(withCompositingOperation compositingOperation: NSCompositingOperation,\n                      alpha: CGFloat = 1.0,\n                      backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage\n    {\n        guard let _ = cgImage else {\n            assertionFailure(\"[Kingfisher] Compositing Operation image only works for CG-based image.\")\n            return base\n        }\n        \n        let rect = CGRect(origin: .zero, size: size)\n        return draw(to: rect.size, inverting: false) { _ in\n            if let backgroundColor = backgroundColor {\n                backgroundColor.setFill()\n                rect.fill()\n            }\n            base.draw(in: rect, from: .zero, operation: compositingOperation, fraction: alpha)\n            return false\n        }\n    }\n#endif\n    \n    // MARK: Round Corner\n    \n    /// Create a rounded corner image from the `base` image.\n    ///\n    /// - Parameters:\n    ///   - radius: The radius for rounding the corners of the image.\n    ///   - size: The target size of the resulting image.\n    ///   - corners: The corners to which rounding will be applied.\n    ///   - backgroundColor: The background color for the output image.\n    /// - Returns: An image with rounded corners based on `self`.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    public func image(\n        withRadius radius: Radius,\n        fit size: CGSize,\n        roundingCorners corners: RectCorner = .all,\n        backgroundColor: KFCrossPlatformColor? = nil\n    ) -> KFCrossPlatformImage\n    {\n\n        guard let _ = cgImage else {\n            assertionFailure(\"[Kingfisher] Round corner image only works for CG-based image.\")\n            return base\n        }\n        \n        let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)\n        return draw(to: size, inverting: false) { _ in\n            #if os(macOS)\n            if let backgroundColor = backgroundColor {\n                let rectPath = NSBezierPath(rect: rect)\n                backgroundColor.setFill()\n                rectPath.fill()\n            }\n            \n            let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners)\n            path.addClip()\n            base.draw(in: rect)\n            #else\n            guard let context = UIGraphicsGetCurrentContext() else {\n                assertionFailure(\"[Kingfisher] Failed to create CG context for image.\")\n                return false\n            }\n            \n            if let backgroundColor = backgroundColor {\n                let rectPath = UIBezierPath(rect: rect)\n                backgroundColor.setFill()\n                rectPath.fill()\n            }\n            \n            let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners)\n            context.addPath(path.cgPath)\n            context.clip()\n            base.draw(in: rect)\n            #endif\n            return false\n        }\n    }\n    \n    /// Create a round corner image from the `base` image.\n    ///\n    /// - Parameters:\n    ///   - radius: The radius for rounding the corners of the image.\n    ///   - size: The target size of the resulting image.\n    ///   - corners: The corners to which rounding will be applied.\n    ///   - backgroundColor: The background color for the output image.\n    /// - Returns: An image with rounded corners based on `self`.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    public func image(\n        withRoundRadius radius: CGFloat,\n        fit size: CGSize,\n        roundingCorners corners: RectCorner = .all,\n        backgroundColor: KFCrossPlatformColor? = nil\n    ) -> KFCrossPlatformImage\n    {\n        image(withRadius: .point(radius), fit: size, roundingCorners: corners, backgroundColor: backgroundColor)\n    }\n    \n    #if os(macOS)\n    func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> NSBezierPath {\n        let cornerRadius = radius.compute(with: rect.size)\n        let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: cornerRadius - offsetBase / 2)\n        path.windingRule = .evenOdd\n        return path\n    }\n    #else\n    func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> UIBezierPath {\n        let cornerRadius = radius.compute(with: rect.size)\n        return UIBezierPath(\n            roundedRect: rect,\n            byRoundingCorners: corners.uiRectCorner,\n            cornerRadii: CGSize(\n                width: cornerRadius - offsetBase / 2,\n                height: cornerRadius - offsetBase / 2\n            )\n        )\n    }\n    #endif\n    \n    #if os(iOS) || os(tvOS) || os(visionOS)\n    func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> KFCrossPlatformImage {\n        switch contentMode {\n        case .scaleAspectFit:\n            return resize(to: size, for: .aspectFit)\n        case .scaleAspectFill:\n            return resize(to: size, for: .aspectFill)\n        default:\n            return resize(to: size)\n        }\n    }\n    #endif\n    \n    // MARK: Resizing\n    \n    /// Resize the `base` image to a new size.\n    ///\n    /// - Parameter size: The target size in points.\n    /// - Returns: An image with the new size.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    ///\n    /// > Tip: This method resizes the `base` image to a specified size by drawing it into that size. If you require a\n    /// smaller thumbnail of the image, consider using ``downsampledImage(data:to:scale:)`` instead, as it offers\n    /// improved efficiency.\n    public func resize(to size: CGSize) -> KFCrossPlatformImage {\n        guard let _ = cgImage else {\n            assertionFailure(\"[Kingfisher] Resize only works for CG-based image.\")\n            return base\n        }\n        \n        let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)\n        return draw(to: size, inverting: false) { _ in\n            #if os(macOS)\n            base.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0)\n            #else\n            base.draw(in: rect)\n            #endif\n            return false\n        }\n    }\n    \n    /// Resize the `base` image to a new size while respecting the specified content mode.\n    ///\n    /// - Parameters:\n    ///   - targetSize: The target size in points.\n    ///   - contentMode: The desired content mode for the output image.\n    /// - Returns: An image with the new size.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    ///\n    /// > Tip: This method resizes the `base` image to a specified size by drawing it into that size. If you require a\n    /// smaller thumbnail of the image, consider using ``downsampledImage(data:to:scale:)`` instead, as it offers\n    /// improved efficiency.\n    public func resize(to targetSize: CGSize, for contentMode: ContentMode) -> KFCrossPlatformImage {\n        let newSize = size.kf.resize(to: targetSize, for: contentMode)\n        return resize(to: newSize)\n    }\n\n    // MARK: Cropping\n    \n    /// Crop the `base` image to a new size with a specified anchor point.\n    ///\n    /// - Parameters:\n    ///   - size: The target size.\n    ///   - anchor: The anchor point from which the size should be calculated.\n    /// - Returns: An image with the new size.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    public func crop(to size: CGSize, anchorOn anchor: CGPoint) -> KFCrossPlatformImage {\n        guard let cgImage = cgImage else {\n            assertionFailure(\"[Kingfisher] Crop only works for CG-based image.\")\n            return base\n        }\n        \n        let rect = self.size.kf.constrainedRect(for: size, anchor: anchor)\n        guard let image = cgImage.cropping(to: rect.scaled(scale)) else {\n            assertionFailure(\"[Kingfisher] Cropping image failed.\")\n            return base\n        }\n        \n        return KingfisherWrapper.image(cgImage: image, scale: scale, refImage: base)\n    }\n    \n    // MARK: Blur\n    \n    /// Create an image with a blur effect based on the `base` image.\n    ///\n    /// - Parameter radius: The blur radius to be used when creating the blur effect.\n    /// - Returns: An image with the blur effect applied.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    public func blurred(withRadius radius: CGFloat) -> KFCrossPlatformImage {\n        guard let cgImage = cgImage else {\n            assertionFailure(\"[Kingfisher] Blur only works for CG-based image.\")\n            return base\n        }\n        \n        // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement\n        // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)\n        // if d is odd, use three box-blurs of size 'd', centered on the output pixel.\n        let s = max(radius, 2.0)\n        \n        // We will do blur on a resized image (*0.5), so the blur radius could be half as well.\n        // Fix the slow compiling time for Swift 3.\n        // See https://github.com/onevcat/Kingfisher/issues/611\n        let pi2 = 2 * CGFloat.pi\n        let sqrtPi2 = sqrt(pi2)\n        var targetRadius = floor(s * 3.0 * sqrtPi2 / 4.0 + 0.5)\n        \n        if targetRadius.isEven { targetRadius += 1 }\n\n        // Determine necessary iteration count by blur radius.\n        let iterations: Int\n        if radius < 0.5 {\n            iterations = 1\n        } else if radius < 1.5 {\n            iterations = 2\n        } else {\n            iterations = 3\n        }\n        \n        func createEffectBuffer(_ context: CGContext) -> vImage_Buffer {\n            let data = context.data\n            let width = vImagePixelCount(context.width)\n            let height = vImagePixelCount(context.height)\n            let rowBytes = context.bytesPerRow\n            \n            return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes)\n        }\n        \n        guard let inputContext = CGContext.fresh(cgImage: cgImage) else {\n            return base\n        }\n        inputContext.draw(\n            cgImage,\n            in: CGRect(\n                x: 0,\n                y: 0,\n                width: size.width * scale,\n                height: size.height * scale\n            )\n        )\n        var inBuffer = createEffectBuffer(inputContext)\n\n        guard let outContext = CGContext.fresh(cgImage: cgImage) else {\n            return base\n        }\n        var outBuffer = createEffectBuffer(outContext)\n\n        for _ in 0 ..< iterations {\n            let flag = vImage_Flags(kvImageEdgeExtend)\n            vImageBoxConvolve_ARGB8888(\n                &inBuffer, &outBuffer, nil, 0, 0, UInt32(targetRadius), UInt32(targetRadius), nil, flag)\n            // Next inBuffer should be the outButter of current iteration\n            (inBuffer, outBuffer) = (outBuffer, inBuffer)\n        }\n        \n        #if os(macOS)\n        let result = outContext.makeImage().flatMap {\n            fixedForRetinaPixel(cgImage: $0, to: size)\n        }\n        #else\n        let result = outContext.makeImage().flatMap {\n            KFCrossPlatformImage(cgImage: $0, scale: base.scale, orientation: base.imageOrientation)\n        }\n        #endif\n        guard let blurredImage = result else {\n            assertionFailure(\"[Kingfisher] Can not make an blurred image within this context.\")\n            return base\n        }\n        return blurredImage\n    }\n    \n    public func addingBorder(_ border: Border) -> KFCrossPlatformImage\n    {\n        guard let _ = cgImage else {\n            assertionFailure(\"[Kingfisher] Blend mode image only works for CG-based image.\")\n            return base\n        }\n        \n        let rect = CGRect(origin: .zero, size: size)\n        return draw(to: rect.size, inverting: false) { context in\n            \n            #if os(macOS)\n            base.draw(in: rect)\n            #else\n            base.draw(in: rect, blendMode: .normal, alpha: 1.0)\n            #endif\n            \n            \n            let strokeRect =  rect.insetBy(dx: border.lineWidth / 2, dy: border.lineWidth / 2)\n            context.setStrokeColor(border.color.cgColor)\n            context.setAlpha(border.color.rgba.a)\n            \n            let line = pathForRoundCorner(\n                rect: strokeRect,\n                radius: border.radius,\n                corners: border.roundingCorners,\n                offsetBase: border.lineWidth\n            )\n            line.lineCapStyle = .square\n            line.lineWidth = border.lineWidth\n            line.stroke()\n            \n            return false\n        }\n    }\n    \n    // MARK: Overlay\n    \n    /// Create an image from the `base` image with a color overlay layer.\n    ///\n    /// - Parameters:\n    ///   - color: The color to be used for the overlay.\n    ///   - fraction: The fraction of the input color to apply, ranging from 0.0 (solid color) to 1.0 (transparent overlay).\n    /// - Returns: An image with a color overlay applied.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    public func overlaying(with color: KFCrossPlatformColor, fraction: CGFloat) -> KFCrossPlatformImage {\n        \n        guard let _ = cgImage else {\n            assertionFailure(\"[Kingfisher] Overlaying only works for CG-based image.\")\n            return base\n        }\n        \n        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)\n        return draw(to: rect.size, inverting: false) { context in\n            #if os(macOS)\n            base.draw(in: rect)\n            if fraction > 0 {\n                color.withAlphaComponent(1 - fraction).set()\n                rect.fill(using: .sourceAtop)\n            }\n            #else\n            color.set()\n            UIRectFill(rect)\n            base.draw(in: rect, blendMode: .destinationIn, alpha: 1.0)\n            \n            if fraction > 0 {\n                base.draw(in: rect, blendMode: .sourceAtop, alpha: fraction)\n            }\n            #endif\n            return false\n        }\n    }\n    \n    // MARK: Tint\n    \n    /// Create an image from the `base` image with a color tint.\n    ///\n    /// - Parameter color: The color to be used for tinting the `base` image.\n    /// - Returns: An image with a color tint applied.\n    ///\n    /// > Important: This method does not work on watchOS, where the original image is returned.\n    public func tinted(with color: KFCrossPlatformColor) -> KFCrossPlatformImage {\n#if os(watchOS)\n        return base\n#else\n        return apply(.tint(color))\n#endif\n    }\n    \n    // MARK: Color Control\n    \n    /// Create an image from `self` with color control adjustments.\n    ///\n    /// - Parameters:\n    ///   - brightness: The degree of brightness adjustment to apply to the image.\n    ///   - contrast: The degree of contrast adjustment to apply to the image.\n    ///   - saturation: The degree of saturation adjustment to apply to the image.\n    ///   - inputEV: The exposure value (EV) adjustment to apply to the image.\n    /// - Returns: An image with color control adjustments applied.\n    ///\n    /// > Important: This method does not work on watchOS, where the original image is returned.\n    public func adjusted(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) -> KFCrossPlatformImage {\n#if os(watchOS)\n        return base\n#else\n        let colorElement = Filter.ColorElement(\n            brightness: brightness,\n            contrast: contrast,\n            saturation: saturation,\n            inputEV: inputEV\n        )\n        return apply(.colorControl(colorElement))\n#endif\n    }\n    \n    /// Return an image with the specified scale.\n    ///\n    /// - Parameter scale: The target scale factor for the new image.\n    /// - Returns: The image with the target scale. If the base image is already at the target scale, the `base` image \n    /// will be returned.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image, the `base` image itself is returned.\n    public func scaled(to scale: CGFloat) -> KFCrossPlatformImage {\n        guard scale != self.scale else {\n            return base\n        }\n        guard let cgImage = cgImage else {\n            assertionFailure(\"[Kingfisher] Scaling only works for CG-based image.\")\n            return base\n        }\n        return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base)\n    }\n}\n\n// MARK: - Decoding Image\nextension KingfisherWrapper where Base: KFCrossPlatformImage {\n    \n    /// Returns the decoded image of the `base` image. \n    ///\n    /// On iOS 15 or later, this is identical to the `UIImage.preparingForDisplay` method.\n    ///\n    /// In previous versions, this method draws the image in a plain context and returns the data from it. Using this\n    ///  method can improve drawing performance when an image is created from data but hasn't been displayed for the\n    ///  first time.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image or animated image, the `base` image itself is returned.\n    public var decoded: KFCrossPlatformImage { return decoded(scale: scale) }\n    \n    /// Returns the decoded image of the `base` image at a given `scale`.\n    ///\n    /// On iOS 15 or later, this is identical to the `UIImage.preparingForDisplay` method.\n    ///\n    /// In previous versions, this method draws the image in a plain context and returns the data from it. Using this\n    ///  method can improve drawing performance when an image is created from data but hasn't been displayed for the\n    ///  first time.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image or animated image, the `base` image itself is returned.\n    public func decoded(scale: CGFloat) -> KFCrossPlatformImage {\n\n        // Prevent animated image (GIF) losing it's images\n        #if os(iOS) || os(visionOS)\n        if frameSource != nil { return base }\n        #else\n        if images != nil { return base }\n        #endif\n        \n        // For older system versions, revert to the drawing for decoding.\n        guard let imageRef = cgImage else {\n            assertionFailure(\"[Kingfisher] Decoding only works for CG-based image.\")\n            return base\n        }\n        \n        #if !os(watchOS) && !os(macOS)\n        // In newer system versions, use `preparingForDisplay`.\n        if #available(iOS 15.0, tvOS 15.0, visionOS 1.0, *) {\n            if base.scale == scale, let image = base.preparingForDisplay() {\n                return image\n            }\n            let scaledImage = KFCrossPlatformImage(cgImage: imageRef, scale: scale, orientation: base.imageOrientation)\n            if let image = scaledImage.preparingForDisplay() {\n                return image\n            }\n        }\n        #endif\n\n        let size = CGSize(width: CGFloat(imageRef.width) / scale, height: CGFloat(imageRef.height) / scale)\n        return draw(to: size, inverting: true, scale: scale) { context in\n            context.draw(imageRef, in: CGRect(origin: .zero, size: size))\n            return true\n        }\n    }\n\n    /// Returns the decoded image of the `base` image on a given `context`.\n    ///\n    /// This method draws the image in the given context and returns the data from it. Using this\n    /// method can improve drawing performance when an image is created from data but hasn't been displayed for the\n    /// first time.\n    ///\n    /// > This method is only applicable to CG-based images. The current image scale is preserved.\n    /// > For any non-CG-based image or animated image, the `base` image itself is returned.\n    public func decoded(on context: CGContext) -> KFCrossPlatformImage {\n        // Prevent animated image (GIF) losing it's images\n        if frameSource != nil { return base }\n\n        guard let refImage = cgImage,\n              let decodedRefImage = refImage.decoded(on: context, scale: scale) else\n        {\n            assertionFailure(\"[Kingfisher] Decoding only works for CG-based image.\")\n            return base\n        }\n        return KingfisherWrapper.image(cgImage: decodedRefImage, scale: scale, refImage: base)\n    }\n}\n\nextension CGImage {\n    func decoded(on context: CGContext, scale: CGFloat) -> CGImage? {\n        let size = CGSize(width: CGFloat(self.width) / scale, height: CGFloat(self.height) / scale)\n        context.draw(self, in: CGRect(origin: .zero, size: size))\n        guard let decodedImageRef = context.makeImage() else {\n            return nil\n        }\n        return decodedImageRef\n    }\n    \n    static func create(ref: CGImage) -> CGImage? {\n        guard let space = ref.colorSpace, let provider = ref.dataProvider else {\n            return nil\n        }\n        return CGImage(\n            width: ref.width,\n            height: ref.height,\n            bitsPerComponent: ref.bitsPerComponent,\n            bitsPerPixel: ref.bitsPerPixel,\n            bytesPerRow: ref.bytesPerRow,\n            space: space,\n            bitmapInfo: ref.bitmapInfo,\n            provider: provider,\n            decode: ref.decode,\n            shouldInterpolate: ref.shouldInterpolate,\n            intent: ref.renderingIntent\n        )\n    }\n}\n\nextension KingfisherWrapper where Base: KFCrossPlatformImage {\n    func draw(\n        to size: CGSize,\n        inverting: Bool,\n        scale: CGFloat? = nil,\n        refImage: KFCrossPlatformImage? = nil,\n        draw: (CGContext) -> Bool // Whether use the refImage (`true`) or ignore image orientation (`false`)\n    ) -> KFCrossPlatformImage\n    {\n        #if os(macOS) || os(watchOS)\n        let targetScale = scale ?? self.scale\n        GraphicsContext.begin(size: size, scale: targetScale)\n        guard let context = GraphicsContext.current(size: size, scale: targetScale, inverting: inverting, cgImage: cgImage) else {\n            assertionFailure(\"[Kingfisher] Failed to create CG context for blurring image.\")\n            return base\n        }\n        defer { GraphicsContext.end() }\n        let useRefImage = draw(context)\n        guard let cgImage = context.makeImage() else {\n            return base\n        }\n        let ref = useRefImage ? (refImage ?? base) : nil\n        return KingfisherWrapper.image(cgImage: cgImage, scale: targetScale, refImage: ref)\n        #else\n        \n        let format = UIGraphicsImageRendererFormat.preferred()\n        format.scale = scale ?? self.scale\n        let renderer = UIGraphicsImageRenderer(size: size, format: format)\n        \n        var useRefImage: Bool = false\n        let image = renderer.image { rendererContext in\n            \n            let context = rendererContext.cgContext\n            if inverting { // If drawing a CGImage, we need to make context flipped.\n                context.scaleBy(x: 1.0, y: -1.0)\n                context.translateBy(x: 0, y: -size.height)\n            }\n            \n            useRefImage = draw(context)\n        }\n        if useRefImage {\n            guard let cgImage = image.cgImage else {\n                return base\n            }\n            let ref = refImage ?? base\n            return KingfisherWrapper.image(cgImage: cgImage, scale: format.scale, refImage: ref)\n        } else {\n            return image\n        }\n        #endif\n    }\n    \n    #if os(macOS)\n    func fixedForRetinaPixel(cgImage: CGImage, to size: CGSize) -> KFCrossPlatformImage {\n        \n        let image = KFCrossPlatformImage(cgImage: cgImage, size: base.size)\n        let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)\n        \n        return draw(to: self.size, inverting: false) { context in\n            image.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0)\n            return false\n        }\n    }\n    #endif\n}\n\nextension CGContext {\n    fileprivate static func fresh(cgImage: CGImage) -> CGContext? {\n        CGContext(\n            data: nil,\n            width: cgImage.width,\n            height: cgImage.height,\n            bitsPerComponent: 8,\n            bytesPerRow: 4 * cgImage.width,\n            space: CGColorSpaceCreateDeviceRGB(),\n            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/Image/ImageFormat.swift",
    "content": "//\n//  ImageFormat.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/09/28.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Represents the image format.\npublic enum ImageFormat: Sendable {\n    /// The format cannot be recognized or not supported yet.\n    case unknown\n    /// PNG image format.\n    case PNG\n    /// JPEG image format.\n    case JPEG\n    /// GIF image format.\n    case GIF\n    \n    struct HeaderData {\n        static let PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]\n        static let JPEG_SOI: [UInt8] = [0xFF, 0xD8]\n        static let JPEG_IF: [UInt8] = [0xFF]\n        static let GIF: [UInt8] = [0x47, 0x49, 0x46]\n    }\n    \n    /// JPEG marker of each sequence of segments.\n    ///\n    /// See also [here](https://www.digicamsoft.com/itu/itu-t81-36.html).\n    public enum JPEGMarker {\n        case SOF0           //baseline\n        case SOF2           //progressive\n        case DHT            //Huffman Table\n        case DQT            //Quantization Table\n        case DRI            //Restart Interval\n        case SOS            //Start Of Scan\n        case RSTn(UInt8)    //Restart\n        case APPn           //Application-specific\n        case COM            //Comment\n        case EOI            //End Of Image\n        \n        var bytes: [UInt8] {\n            switch self {\n            case .SOF0:         return [0xFF, 0xC0]\n            case .SOF2:         return [0xFF, 0xC2]\n            case .DHT:          return [0xFF, 0xC4]\n            case .DQT:          return [0xFF, 0xDB]\n            case .DRI:          return [0xFF, 0xDD]\n            case .SOS:          return [0xFF, 0xDA]\n            case .RSTn(let n):  return [0xFF, 0xD0 + n]\n            case .APPn:         return [0xFF, 0xE0]\n            case .COM:          return [0xFF, 0xFE]\n            case .EOI:          return [0xFF, 0xD9]\n            }\n        }\n    }\n}\n\n\nextension Data: KingfisherCompatibleValue {}\n\n// MARK: - Misc Helpers\nextension KingfisherWrapper where Base == Data {\n    /// Gets the image format corresponding to the data.\n    public var imageFormat: ImageFormat {\n        guard base.count > 8 else { return .unknown }\n        \n        var buffer = [UInt8](repeating: 0, count: 8)\n        base.copyBytes(to: &buffer, count: 8)\n        \n        if buffer == ImageFormat.HeaderData.PNG {\n            return .PNG\n            \n        } else if buffer[0] == ImageFormat.HeaderData.JPEG_SOI[0],\n            buffer[1] == ImageFormat.HeaderData.JPEG_SOI[1],\n            buffer[2] == ImageFormat.HeaderData.JPEG_IF[0]\n        {\n            return .JPEG\n            \n        } else if buffer[0] == ImageFormat.HeaderData.GIF[0],\n            buffer[1] == ImageFormat.HeaderData.GIF[1],\n            buffer[2] == ImageFormat.HeaderData.GIF[2]\n        {\n            return .GIF\n        }\n        \n        return .unknown\n    }\n    \n    public func contains(jpeg marker: ImageFormat.JPEGMarker) -> Bool {\n        guard imageFormat == .JPEG else {\n            return false\n        }\n        \n        let bytes = [UInt8](base)\n        let markerBytes = marker.bytes\n        for (index, item) in bytes.enumerated() where bytes.count > index + 1 {\n            guard\n                item == markerBytes.first,\n                bytes[index + 1] == markerBytes[1] else {\n                continue\n            }\n            return true\n        }\n        return false\n    }\n}\n"
  },
  {
    "path": "Sources/Image/ImageProcessor.swift",
    "content": "//\n//  ImageProcessor.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2016/08/26.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport CoreGraphics\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// Represents an item which could be processed by an `ImageProcessor`.\npublic enum ImageProcessItem: Sendable {\n    \n    /// Input image. The processor should provide a method to apply\n    /// processing to this `image` and return the resulting image.\n    case image(KFCrossPlatformImage)\n    \n    /// Input data. The processor should provide a method to apply\n    /// processing to this `data` and return the resulting image.\n    case data(Data)\n}\n\n/// An `ImageProcessor` is used to convert downloaded data into an image.\npublic protocol ImageProcessor: Sendable {\n    \n    /// Identifier for the processor.\n    ///\n    /// This identifier is used to distinguish the processor when caching and retrieving an image. Ensure that\n    /// processors with the same properties or functionality share the same identifier so that processed images can be\n    /// retrieved with the correct key.\n    ///\n    /// > Important: Avoid using an empty string for a custom processor, as it is already reserved by the\n    /// > `DefaultImageProcessor`. It is recommended to use a reverse domain name notation string for your identifier.\n    var identifier: String { get }\n\n    /// Process the input `ImageProcessItem` using this processor.\n    ///\n    /// - Parameters:\n    ///   - item: The input item to be processed by `self`.\n    ///   - options: The parsed options for processing the item.\n    /// - Returns: The processed image.\n    ///\n    /// You should return `nil` if processing fails when converting an input item to an image. If the processing\n    /// caller receives `nil`, an error will be reported, and the processing flow will stop. If processing flow is not\n    /// critical for your use case, and the input item is already an image (`.image` case), you can also choose to\n    /// return the input image itself to continue the processing pipeline.\n    ///\n    /// > Important: Most processors only support CG-based images. The watchOS is not supported for processors\n    /// > containing a filter, and the input image will be returned directly on watchOS.\n    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?\n}\n\nextension ImageProcessor {\n    \n    /// Append an `ImageProcessor` to another. The identifier of the new `ImageProcessor` will \n    /// be `\"\\(self.identifier)|>\\(another.identifier)\"`.\n    ///\n    /// - Parameter another: An `ImageProcessor` to be appended to `self`.\n    /// - Returns: The new `ImageProcessor` that will process the image in the order of the two processors concatenated.\n    public func append(another: any ImageProcessor) -> any ImageProcessor {\n        let newIdentifier = identifier.appending(\"|>\\(another.identifier)\")\n        return GeneralProcessor(identifier: newIdentifier) {\n            item, options in\n            if let image = self.process(item: item, options: options) {\n                return another.process(item: .image(image), options: options)\n            } else {\n                return nil\n            }\n        }\n    }\n}\n\nfunc ==(left: any ImageProcessor, right: any ImageProcessor) -> Bool {\n    return left.identifier == right.identifier\n}\n\nfunc !=(left: any ImageProcessor, right: any ImageProcessor) -> Bool {\n    return !(left == right)\n}\n\ntypealias ProcessorImp = (@Sendable (ImageProcessItem, KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?)\nstruct GeneralProcessor: ImageProcessor {\n    let identifier: String\n    let p: ProcessorImp\n    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        return p(item, options)\n    }\n}\n\n/// The default processor. It converts the input data into a valid image.\n///\n/// Supported image formats include .PNG, .JPEG, and .GIF. If an image item is provided as the\n/// ``ImageProcessItem/image(_:)`` case, ``DefaultImageProcessor`` will leave it unchanged and return the associated\n/// image.\npublic struct DefaultImageProcessor: ImageProcessor {\n    \n    /// A default instance of ``DefaultImageProcessor`` can be used across the framework.\n    public static let `default` = DefaultImageProcessor()\n    \n    public let identifier = \"\"\n    \n    /// Create a ``DefaultImageProcessor``.\n    ///\n    /// Use ``DefaultImageProcessor/default`` to obtain an instance if you have no specific reason to create your own\n    /// ``DefaultImageProcessor``.\n    public init() {}\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.scaled(to: options.scaleFactor)\n        case .data(let data):\n            return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)\n        }\n    }\n}\n\n/// Represents the rect corner setting when processing a round corner image.\npublic struct RectCorner: OptionSet, Sendable {\n    \n    /// Raw value for the corner radius.\n    public let rawValue: Int\n    \n    /// Represents the top left corner.\n    public static let topLeft = RectCorner(rawValue: 1 << 0)\n    \n    /// Represents the top right corner.\n    public static let topRight = RectCorner(rawValue: 1 << 1)\n    \n    /// Represents the bottom left corner.\n    public static let bottomLeft = RectCorner(rawValue: 1 << 2)\n    \n    /// Represents the bottom right corner.\n    public static let bottomRight = RectCorner(rawValue: 1 << 3)\n    \n    /// Represents all corners.\n    public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight]\n    \n    /// Create a `RectCorner` option set with a specified value.\n    ///\n    /// - Parameter rawValue: The value representing a specific corner option.\n    public init(rawValue: Int) {\n        self.rawValue = rawValue\n    }\n    \n    var cornerIdentifier: String {\n        if self == .all {\n            return \"\"\n        }\n        return \"_corner(\\(rawValue))\"\n    }\n}\n\n#if !os(macOS)\n/// Processor for applying a blend mode to images. \n///\n/// Supported for CG-based images only.\npublic struct BlendImageProcessor: ImageProcessor {\n\n    public let identifier: String\n\n    /// The blend mode used to blend the input image.\n    public let blendMode: CGBlendMode\n\n    /// The alpha value used when blending the image.\n    public let alpha: CGFloat\n\n    /// The background color of the output image.\n    ///\n    /// If `nil`, the background will remain transparent.\n    public let backgroundColor: KFCrossPlatformColor?\n\n    /// Create a `BlendImageProcessor`.\n    ///\n    /// - Parameters:\n    ///   - blendMode: The blend mode to be used for blending the input image.\n    ///   - alpha: The alpha value to be used when blending the image, ranging from 0.0 (completely transparent) to \n    ///   1.0 (completely solid). Default is 1.0.\n    ///   - backgroundColor: The background color to apply to the output image. Default is `nil`.\n    public init(blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) {\n        self.blendMode = blendMode\n        self.alpha = alpha\n        self.backgroundColor = backgroundColor\n        var identifier = \"com.onevcat.Kingfisher.BlendImageProcessor(\\(blendMode.rawValue),\\(alpha))\"\n        if let color = backgroundColor {\n            identifier.append(\"_\\(color.rgbaDescription)\")\n        }\n        self.identifier = identifier\n    }\n\n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.image(withBlendMode: blendMode, alpha: alpha, backgroundColor: backgroundColor)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n#endif\n\n#if os(macOS)\n/// Processor for applying a compositing operation to images.\n///\n/// Supported for CG-based images on macOS.\npublic struct CompositingImageProcessor: ImageProcessor {\n\n    public let identifier: String\n\n    /// The compositing operation applied to the input image.\n    public let compositingOperation: NSCompositingOperation\n\n    /// The alpha value used when compositing the image.\n    public let alpha: CGFloat\n\n    /// The background color of the output image. If `nil`, the background will remain transparent.\n    public let backgroundColor: KFCrossPlatformColor?\n\n    /// Create a `CompositingImageProcessor`.\n    ///\n    /// - Parameters:\n    ///   - compositingOperation: The compositing operation to be applied to the input image.\n    ///   - alpha: The alpha value to be used when compositing the image, ranging from 0.0 (completely transparent) to \n    ///   1.0 (completely solid). Default is 1.0.\n    ///   - backgroundColor: The background color to apply to the output image. Default is `nil`.\n    public init(compositingOperation: NSCompositingOperation,\n                alpha: CGFloat = 1.0,\n                backgroundColor: KFCrossPlatformColor? = nil)\n    {\n        self.compositingOperation = compositingOperation\n        self.alpha = alpha\n        self.backgroundColor = backgroundColor\n        var identifier = \"com.onevcat.Kingfisher.CompositingImageProcessor(\\(compositingOperation.rawValue),\\(alpha))\"\n        if let color = backgroundColor {\n            identifier.append(\"_\\(color.rgbaDescription)\")\n        }\n        self.identifier = identifier\n    }\n\n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.image(\n                            withCompositingOperation: compositingOperation,\n                            alpha: alpha,\n                            backgroundColor: backgroundColor)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n#endif\n\n/// Represents a radius specified in a ``RoundCornerImageProcessor``.\npublic enum Radius: Sendable {\n    \n    /// The radius should be calculated as a fraction of the image width. Typically, the associated value should be\n    /// between 0 and 0.5, where 0 represents no radius, and 0.5 represents using half of the image width.\n    case widthFraction(CGFloat)\n    \n    /// The radius should be calculated as a fraction of the image height. Typically, the associated value should be\n    /// between 0 and 0.5, where 0 represents no radius, and 0.5 represents using half of the image height.\n    case heightFraction(CGFloat)\n    \n    /// Use a fixed point value as the round corner radius.\n    case point(CGFloat)\n\n    var radiusIdentifier: String {\n        switch self {\n        case .widthFraction(let f):\n            return \"w_frac_\\(f)\"\n        case .heightFraction(let f):\n            return \"h_frac_\\(f)\"\n        case .point(let p):\n            return p.description\n        }\n    }\n    \n    public func compute(with size: CGSize) -> CGFloat {\n        let cornerRadius: CGFloat\n        switch self {\n        case .point(let point):\n            cornerRadius = point\n        case .widthFraction(let widthFraction):\n            cornerRadius = size.width * widthFraction\n        case .heightFraction(let heightFraction):\n            cornerRadius = size.height * heightFraction\n        }\n        return cornerRadius\n    }\n}\n\n/// Processor for creating round corner images. \n///\n/// Supported for CG-based images on macOS. If a non-CG image is passed in, the processor will have no effect.\n///\n/// > Tip: The input image will be rendered with round corner pixels removed. If the image itself does not contain an\n/// > alpha channel (for example, a JPEG image), the processed image will contain an alpha channel in memory for\n/// > correct rendering. However, when cached to disk, Kingfisher defaults to preserving the original image format.\n/// > This means the alpha channel will be removed for these images. If you load the processed image from the cache \n/// > again, you will lose the transparent corners.\n/// >\n/// > You can use ``FormatIndicatedCacheSerializer/png`` to force Kingfisher to serialize the image to PNG format in this\n/// > case.\n\npublic struct RoundCornerImageProcessor: ImageProcessor {\n\n    public let identifier: String\n\n    /// The radius to be applied during processing. \n    ///\n    /// Specify a specific point value with ``Radius/point(_:)``, or a fraction of the target image with\n    /// ``Radius/widthFraction(_:)`` or ``Radius/heightFraction(_:)``. For example, if you have a square image with\n    /// equal width and height, `.widthFraction(0.5)` means using half of the width of the size to create a round image.\n    public let radius: Radius\n    \n    /// The target corners to round.\n    public let roundingCorners: RectCorner\n    \n    /// The target size for the output image. If `nil`, the image will retain its original size after processing.\n    public let targetSize: CGSize?\n\n    /// The background color for the output image. If `nil`, it will use a transparent background.\n    public let backgroundColor: KFCrossPlatformColor?\n\n    /// Create a ``RoundCornerImageProcessor`` with given parameters.\n    ///\n    /// - Parameters:\n    ///   - cornerRadius: The corner radius in points to be applied during processing.\n    ///   - targetSize: The target size for the output image. If `nil`, the image will retain its original size after \n    ///   processing. Default is `nil`.\n    ///   - corners: The target corners to round. Default is ``RectCorner/all``.\n    ///   - backgroundColor: The background color to apply to the output image. Default is `nil`.\n    ///\n    /// This initializer accepts a specific point value for `cornerRadius`. If you don't know the image size but still \n    /// want to apply a full round corner (making the final image round), or specify the corner radius as a fraction of\n    /// one dimension of the target image, use the ``init(radius:targetSize:roundingCorners:backgroundColor:)``\n    /// instead.\n    public init(\n        cornerRadius: CGFloat,\n        targetSize: CGSize? = nil,\n        roundingCorners corners: RectCorner = .all,\n        backgroundColor: KFCrossPlatformColor? = nil\n    )\n    {\n        let radius = Radius.point(cornerRadius)\n        self.init(radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor)\n    }\n\n    /// Create a `RoundCornerImageProcessor`.\n    ///\n    /// - Parameters:\n    ///   - radius: The radius to be applied during processing.\n    ///   - targetSize: The target size for the output image. If `nil`, the image will retain its original size after \n    ///   processing. Default is `nil`.\n    ///   - corners: The target corners to round. Default is ``RectCorner/all``.\n    ///   - backgroundColor: The background color to apply to the output image. Default is `nil`.\n    public init(\n        radius: Radius,\n        targetSize: CGSize? = nil,\n        roundingCorners corners: RectCorner = .all,\n        backgroundColor: KFCrossPlatformColor? = nil\n    )\n    {\n        self.radius = radius\n        self.targetSize = targetSize\n        self.roundingCorners = corners\n        self.backgroundColor = backgroundColor\n\n        self.identifier = {\n            var identifier = \"\"\n\n            if let size = targetSize {\n                identifier = \"com.onevcat.Kingfisher.RoundCornerImageProcessor\" +\n                             \"(\\(radius.radiusIdentifier)_\\(size)\\(corners.cornerIdentifier))\"\n            } else {\n                identifier = \"com.onevcat.Kingfisher.RoundCornerImageProcessor\" +\n                             \"(\\(radius.radiusIdentifier)\\(corners.cornerIdentifier))\"\n            }\n            if let backgroundColor = backgroundColor {\n                identifier += \"_\\(backgroundColor)\"\n            }\n\n            return identifier\n        }()\n    }\n\n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            let size = targetSize ?? image.kf.size\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.image(\n                            withRadius: radius,\n                            fit: size,\n                            roundingCorners: roundingCorners,\n                            backgroundColor: backgroundColor)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// Represents a border to be added to the image.\n///\n/// Typically used with ``BorderImageProcessor``, which adds the border to the image.\npublic struct Border: Sendable {\n    \n    /// The color of the border to create.\n    public var color: KFCrossPlatformColor\n    \n    /// The line width of the border to create.\n    public var lineWidth: CGFloat\n    \n    /// The radius to be applied during processing.\n    ///\n    /// Specify a specific point value with ``Radius/point(_:)``, or a fraction of the target image with\n    /// ``Radius/widthFraction(_:)`` or ``Radius/heightFraction(_:)``. For example, if you have a square image with\n    /// equal width and height, `.widthFraction(0.5)` means using half of the width of the size to create a round image.\n    public var radius: Radius\n    \n    /// The target corners which will be applied rounding.\n    public var roundingCorners: RectCorner\n    \n    /// Creates a border.\n    /// - Parameters:\n    ///   - color: The color will be used to render the border.\n    ///   - lineWidth: The line width of the border.\n    ///   - radius: The radius of the border corner.\n    ///   - roundingCorners: The target corners type.\n    public init(\n        color: KFCrossPlatformColor = .black,\n        lineWidth: CGFloat = 4,\n        radius: Radius = .point(0),\n        roundingCorners: RectCorner = .all\n    ) {\n        self.color = color\n        self.lineWidth = lineWidth\n        self.radius = radius\n        self.roundingCorners = roundingCorners\n    }\n    \n    var identifier: String {\n        \"\\(color.rgbaDescription)_\\(lineWidth)_\\(radius.radiusIdentifier)_\\(roundingCorners.cornerIdentifier)\"\n    }\n}\n\n/// Processor for creating bordered images.\npublic struct BorderImageProcessor: ImageProcessor {\n    \n    public var identifier: String { \"com.onevcat.Kingfisher.BorderImageProcessor(\\(border)\" }\n    \n    /// The border to be added to the image.\n    public let border: Border\n    \n    /// Create a `BorderImageProcessor` with a given `Border`.\n    ///\n    /// - Parameter border: The border to be added to the image.\n    public init(border: Border) {\n        self.border = border\n    }\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.addingBorder(border)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// Represents how a size of content adjusts itself to fit a target size.\npublic enum ContentMode: Sendable {\n    /// Does not scale the content.\n    case none\n    /// Scales the content to fit the size of the view while maintaining the aspect ratio.\n    case aspectFit\n    /// Scales the content to fill the size of the view.\n    case aspectFill\n}\n\n/// Processor for resizing images.\n///\n/// If you need to resize an image represented by data to a smaller size, use ``DownsamplingImageProcessor`` instead,\n/// which is more efficient and uses less memory.\npublic struct ResizingImageProcessor: ImageProcessor {\n    \n    public let identifier: String\n    \n    /// The reference size for the resizing operation in points.\n    public let referenceSize: CGSize\n    \n    /// The target content mode of the output image.\n    public let targetContentMode: ContentMode\n    \n    /// Create a ``ResizingImageProcessor``.\n    ///\n    /// - Parameters:\n    ///   - referenceSize: The reference size for the resizing operation in points.\n    ///   - mode: The target content mode of the output image.\n    ///\n    /// The instance of ``ResizingImageProcessor`` will follow the `mode` argument and attempt to resize the input\n    /// images to fit or fill the `referenceSize`. This means if you are using a `mode` besides `.none`, you may get an\n    /// image with a size that is not the same as the `referenceSize`.\n    ///\n    /// For example, with an input image size of {100, 200}, `referenceSize` of {100, 100}, and `mode` of `.aspectFit`,\n    /// you will get an output image with a size of {50, 100} that \"fits\" the `referenceSize`.\n    ///\n    /// > If you need an output image to be exactly a specified size, append or use a ``CroppingImageProcessor``.\n    public init(referenceSize: CGSize, mode: ContentMode = .none) {\n        self.referenceSize = referenceSize\n        self.targetContentMode = mode\n        \n        if mode == .none {\n            self.identifier = \"com.onevcat.Kingfisher.ResizingImageProcessor(\\(referenceSize))\"\n        } else {\n            self.identifier = \"com.onevcat.Kingfisher.ResizingImageProcessor(\\(referenceSize), \\(mode))\"\n        }\n    }\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.resize(to: referenceSize, for: targetContentMode)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// Processor for adding a blur effect to images. \n///\n/// Uses `Accelerate.framework` under the hood for better performance. Applies a simulated Gaussian blur with the\n/// specified blur radius.\npublic struct BlurImageProcessor: ImageProcessor {\n    \n    public let identifier: String\n    \n    /// The blur radius for the simulated Gaussian blur.\n    public let blurRadius: CGFloat\n\n    /// Create a `BlurImageProcessor`.\n    ///\n    /// - Parameter blurRadius: The blur radius for the simulated Gaussian blur.\n    public init(blurRadius: CGFloat) {\n        self.blurRadius = blurRadius\n        self.identifier = \"com.onevcat.Kingfisher.BlurImageProcessor(\\(blurRadius))\"\n    }\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            let radius = blurRadius * options.scaleFactor\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.blurred(withRadius: radius)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// Processor for adding an overlay to images.\n///\n/// > Only CG-based images are supported.\npublic struct OverlayImageProcessor: ImageProcessor {\n    \n    public let identifier: String\n    \n    /// The overlay color used to overlay the input image.\n    public let overlay: KFCrossPlatformColor\n    \n    /// The fraction used when overlaying the color to the image.\n    public let fraction: CGFloat\n    \n    /// Create an ``OverlayImageProcessor``.\n    ///\n    /// - Parameters:\n    ///   - overlay: The overlay color used to overlay the input image.\n    ///   - fraction: The fraction used when overlaying the color to the image.\n    ///               Ranges from 0.0 to 1.0. 0.0 means a solid color, and 1.0 means a transparent overlay.\n    public init(overlay: KFCrossPlatformColor, fraction: CGFloat = 0.5) {\n        self.overlay = overlay\n        self.fraction = fraction\n        self.identifier = \"com.onevcat.Kingfisher.OverlayImageProcessor(\\(overlay.rgbaDescription)_\\(fraction))\"\n    }\n\n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.overlaying(with: overlay, fraction: fraction)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// Processor for tinting images with color.\n///\n/// > Only CG-based images are supported.\n///\n/// > Important: On watchOS, there is no tint support and the original image will be returned.\npublic struct TintImageProcessor: ImageProcessor {\n    \n    public let identifier: String\n    \n    /// The tint color used to tint the input image.\n    public let tint: KFCrossPlatformColor\n    \n    /// Create a ``TintImageProcessor``.\n    ///\n    /// - Parameter tint: The tint color used to tint the input image.\n    public init(tint: KFCrossPlatformColor) {\n        self.tint = tint\n        self.identifier = \"com.onevcat.Kingfisher.TintImageProcessor(\\(tint.rgbaDescription))\"\n    }\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.tinted(with: tint)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// Processor for applying color control to images.\n///\n/// > Only CG-based images are supported.\n///\n/// > Important: On watchOS, there is no color control support and the original image will be returned.\npublic struct ColorControlsProcessor: ImageProcessor {\n    \n    public let identifier: String\n    \n    /// The brightness change applied to the image.\n    public let brightness: CGFloat\n    \n    /// The contrast change applied to the image.\n    public let contrast: CGFloat\n    \n    /// The saturation change applied to the image.\n    public let saturation: CGFloat\n    \n    /// The EV (F-stops brighter or darker) change applied to the image.\n    public let inputEV: CGFloat\n    \n    /// Create a ``ColorControlsProcessor``.\n    ///\n    /// - Parameters:\n    ///   - brightness: The brightness change applied to the image.\n    ///   - contrast: The contrast change applied to the image.\n    ///   - saturation: The saturation change applied to the image.\n    ///   - inputEV: The EV (F-stops brighter or darker) change applied to the image.\n    public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) {\n        self.brightness = brightness\n        self.contrast = contrast\n        self.saturation = saturation\n        self.inputEV = inputEV\n        self.identifier = \"com.onevcat.Kingfisher.ColorControlsProcessor(\\(brightness)_\\(contrast)_\\(saturation)_\\(inputEV))\"\n    }\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV)\n        case .data:\n            return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// Processor for applying black and white effect to images. Only CG-based images are supported.\n///\n/// > Only CG-based images are supported.\n///\n/// > Important: On watchOS, there is no color control support and the original image will be returned.\npublic struct BlackWhiteProcessor: ImageProcessor {\n    \n    public let identifier = \"com.onevcat.Kingfisher.BlackWhiteProcessor\"\n    \n    /// Creates a ``BlackWhiteProcessor``\n    public init() {}\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7)\n            .process(item: item, options: options)\n    }\n}\n\n/// Processor for cropping an image.\npublic struct CroppingImageProcessor: ImageProcessor {\n    \n    public let identifier: String\n    \n    /// The target size of the output image.\n    public let size: CGSize\n    \n    /// Anchor point from which the output size should be calculate.\n    ///\n    /// The anchor point is consisted by two values between 0.0 and 1.0.\n    /// It indicates a related point in current image.\n    ///\n    /// See ``CroppingImageProcessor/init(size:anchor:)`` for more.\n    public let anchor: CGPoint\n    \n    /// Create a ``CroppingImageProcessor``.\n    ///\n    /// - Parameters:\n    ///   - size: The target size of the output image.\n    ///   - anchor: The anchor point from which the size should be calculated. Default is `CGPoint(x: 0.5, y: 0.5)`,\n    ///             which represents the center of the input image.\n    ///\n    /// The anchor point is composed of two values between 0.0 and 1.0. It indicates a relative point in the current\n    /// image, e.g:\n    /// - (0.0, 0.0) for the top-left corner\n    /// - (0.5, 0.5) for the center\n    /// - (1.0, 1.0) for the bottom-right corner\n    ///\n    /// The ``CroppingImageProcessor/size`` property will be used along with ``CroppingImageProcessor/anchor`` to\n    /// calculate a target rectangle in the image size.\n    ///\n    /// > The target size will be automatically calculated with a reasonable behavior. For example, when you have an\n    /// > image size of `CGSize(width: 100, height: 100)` and a target size of `CGSize(width: 20, height: 20)`:\n    /// >\n    /// > - with a (0.0, 0.0) anchor (top-left), the crop rect will be `{0, 0, 20, 20}`;\n    /// > - with a (0.5, 0.5) anchor (center), it will be `{40, 40, 20, 20}`;\n    /// > - while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}`.\n    public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) {\n        self.size = size\n        self.anchor = anchor\n        self.identifier = \"com.onevcat.Kingfisher.CroppingImageProcessor(\\(size)_\\(anchor))\"\n    }\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image.kf.scaled(to: options.scaleFactor)\n                        .kf.crop(to: size, anchorOn: anchor)\n        case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options)\n        }\n    }\n}\n\n/// Processor for downsampling an image. \n///\n/// Compared to ``ResizingImageProcessor``, this processor does not render the images to resize. Instead, it\n/// downsamples the input data directly to an image. It is more efficient than ``ResizingImageProcessor``.\n///\n/// > Tip: It is preferable to use ``DownsamplingImageProcessor`` whenever possible rather than the\n/// > ``ResizingImageProcessor``.\n///\n/// > Important: Only CG-based images are supported. Animated images (such as GIFs) are not supported.\npublic struct DownsamplingImageProcessor: ImageProcessor {\n    \n    /// The target size of the output image.\n    ///\n    /// It should be smaller than the size of the input image. If it is larger, the resulting image will be the same\n    /// size as the input data without downsampling.\n    public let size: CGSize\n    \n    public let identifier: String\n    \n    /// Creates a `DownsamplingImageProcessor`.\n    ///\n    /// - Parameters:\n    ///     - size: The target size of the downsampling operation.\n    ///\n    /// > Important: The size should be smaller than the size of the input image. If it is larger, the resulting image\n    /// will be the same size as the input data without downsampling.\n    public init(size: CGSize) {\n        self.size = size\n        self.identifier = \"com.onevcat.Kingfisher.DownsamplingImageProcessor(\\(size))\"\n    }\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            guard let data = image.kf.data(format: .unknown) else {\n                return nil\n            }\n            return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor)\n        case .data(let data):\n            return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor)\n        }\n    }\n}\n\n// This is an internal processor to provide the same interface for Live Photos.\n// It is not intended to be open and used from external.\nstruct LivePhotoImageProcessor: ImageProcessor {\n    \n    public static let `default` = LivePhotoImageProcessor()\n    private init() { }\n    \n    public let identifier = \"com.onevcat.Kingfisher.LivePhotoImageProcessor\"\n    \n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        switch item {\n        case .image(let image):\n            return image\n        case .data:\n            return KFCrossPlatformImage()\n        }\n    }\n}\n\ninfix operator |>: AdditionPrecedence\n\n/// Concatenates two `ImageProcessor`s to create a new one, in which the `left` and `right` are combined in order to \n/// process the image.\n///\n/// - Parameters:\n///     - left: The first processor.\n///     - right: The second processor that follows the `left`.\n/// - Returns: The new processor that processes the image or the image data in left-to-right order.\npublic func |>(left: any ImageProcessor, right: any ImageProcessor) -> any ImageProcessor {\n    return left.append(another: right)\n}\n\nextension KFCrossPlatformColor {\n    \n    var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {\n        var r: CGFloat = 0\n        var g: CGFloat = 0\n        var b: CGFloat = 0\n        var a: CGFloat = 0\n\n        #if os(macOS)\n        (usingColorSpace(.extendedSRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a)\n        #else\n        getRed(&r, green: &g, blue: &b, alpha: &a)\n        #endif\n        \n        return (r, g, b, a)\n    }\n    \n    var rgbaDescription: String {\n        let components = self.rgba\n        return String(format: \"(%.2f,%.2f,%.2f,%.2f)\", components.r, components.g, components.b, components.a)\n    }\n}\n"
  },
  {
    "path": "Sources/Image/ImageProgressive.swift",
    "content": "//\n//  ImageProgressive.swift\n//  Kingfisher\n//\n//  Created by lixiang on 2019/5/10.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport CoreGraphics\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\nprivate let sharedProcessingQueue: CallbackQueue =\n    .dispatch(DispatchQueue(label: \"com.onevcat.Kingfisher.ImageDownloader.Process\"))\n\n/// Represents a progressive loading for images which supports this feature.\npublic struct ImageProgressive: Sendable {\n    \n    /// The updating strategy when an intermediate progressive image is generated and about to be set to the hosting view.\n    public enum UpdatingStrategy {\n        \n        /// Use the progressive image as it is.\n        ///\n        /// > It is the standard behavior when handling the progressive image.\n        case `default`\n        \n        /// Discard this progressive image and keep the current displayed one.\n        case keepCurrent\n        \n        /// Replace the image to a new one. \n        ///\n        /// If the progressive loading is initialized by a view extension in Kingfisher, the replacing image will be\n        /// used to update the view.\n        case replace(KFCrossPlatformImage?)\n    }\n    \n    /// A default `ImageProgressive` could be used across. It blurs the progressive loading with the fastest\n    /// scan enabled and scan interval as 0.\n    @available(*, deprecated, message: \"Getting a default `ImageProgressive` is deprecated due to its syntax semantic is not clear. Use `ImageProgressive.init` instead.\", renamed: \"init()\")\n    public static let `default` = ImageProgressive(\n        isBlur: true,\n        isFastestScan: true,\n        scanInterval: 0\n    )\n    \n    /// Indicates whether to enable blur effect processing.\n    public var isBlur: Bool\n    \n    /// Indicates whether to enable the fastest scan.\n    public var isFastestScan: Bool\n    \n    /// The minimum time interval for each scan.\n    public var scanInterval: TimeInterval\n    \n    /// Called when an intermediate image is prepared and about to be set to the image view. \n    ///\n    /// If implemented, you should return an ``UpdatingStrategy`` value from this delegate. This value will be used to\n    /// update the hosting view, if any. Otherwise, if there is no hosting view (i.e., the image retrieval is not\n    /// happening from a view extension method), the returned ``UpdatingStrategy`` is ignored.\n    public let onImageUpdated = Delegate<KFCrossPlatformImage, UpdatingStrategy>()\n    \n    /// Creates an `ImageProgressive` value with default settings. \n    ///\n    /// It enables progressive loading with the fastest scan enabled and a scan interval of 0, resulting in a blurred \n    /// effect.\n    public init() {\n        self.init(isBlur: true, isFastestScan: true, scanInterval: 0)\n    }\n    \n    /// Creates an `ImageProgressive` value with the given values.\n    ///\n    /// - Parameters:\n    ///     - isBlur: Indicates whether to enable blur effect processing.\n    ///     - isFastestScan: Indicates whether to enable the fastest scan.\n    ///     - scanInterval: The minimum time interval for each scan.\n    public init(\n        isBlur: Bool,\n        isFastestScan: Bool,\n        scanInterval: TimeInterval\n    )\n    {\n        self.isBlur = isBlur\n        self.isFastestScan = isFastestScan\n        self.scanInterval = scanInterval\n    }\n}\n\n// A data receiving provider to update the image. Working with an `ImageProgressive`, it helps to implement the image\n// progressive effect.\nfinal class ImageProgressiveProvider: DataReceivingSideEffect, @unchecked Sendable {\n    \n    private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.ImageProgressiveProviderPropertyQueue\")\n    \n    private var _onShouldApply: () -> Bool = { return true }\n    \n    var onShouldApply: () -> Bool {\n        get { propertyQueue.sync { _onShouldApply } }\n        set { propertyQueue.sync { _onShouldApply = newValue } }\n    }\n    \n    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {\n\n        DispatchQueue.main.async {\n            guard self.onShouldApply() else { return }\n            self.update(data: task.mutableData, with: task.callbacks)\n        }\n    }\n\n    private let progressive: ImageProgressive\n    private let refresh: (KFCrossPlatformImage) -> Void\n    \n    private let decoder: ImageProgressiveDecoder\n    private let queue = ImageProgressiveSerialQueue()\n    \n    init?(\n        options: KingfisherParsedOptionsInfo,\n        refresh: @escaping (KFCrossPlatformImage) -> Void\n    ) {\n        guard let progressive = options.progressiveJPEG else { return nil }\n        \n        self.progressive = progressive\n        self.refresh = refresh\n        self.decoder = ImageProgressiveDecoder(\n            progressive,\n            processingQueue: options.processingQueue ?? sharedProcessingQueue,\n            creatingOptions: options.imageCreatingOptions\n        )\n    }\n    \n    func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) {\n        guard !data.isEmpty else { return }\n        queue.add(minimum: progressive.scanInterval) { completion in\n\n            @Sendable func decode(_ data: Data) {\n                self.decoder.decode(data, with: callbacks) { image in\n                    defer { completion() }\n                    guard self.onShouldApply() else { return }\n                    guard let image = image else { return }\n                    self.refresh(image)\n                }\n            }\n            \n            Task { @MainActor in\n                let applyFlag = self.onShouldApply()\n                guard applyFlag else {\n                    self.queue.clean()\n                    completion()\n                    return\n                }\n\n                if self.progressive.isFastestScan {\n                    decode(self.decoder.scanning(data) ?? Data())\n                } else {\n                    self.decoder.scanning(data).forEach { decode($0) }\n                }\n            }\n        }\n    }\n}\n\nprivate final class ImageProgressiveDecoder: @unchecked Sendable {\n    \n    private let option: ImageProgressive\n    private let processingQueue: CallbackQueue\n    private let creatingOptions: ImageCreatingOptions\n    private(set) var scannedCount = 0\n    private(set) var scannedIndex = -1\n    \n    init(_ option: ImageProgressive,\n         processingQueue: CallbackQueue,\n         creatingOptions: ImageCreatingOptions) {\n        self.option = option\n        self.processingQueue = processingQueue\n        self.creatingOptions = creatingOptions\n    }\n    \n    func scanning(_ data: Data) -> [Data] {\n        guard data.kf.contains(jpeg: .SOF2) else {\n            return []\n        }\n        guard scannedIndex + 1 < data.count else {\n            return []\n        }\n        \n        var datas: [Data] = []\n        var index = scannedIndex + 1\n        var count = scannedCount\n        \n        while index < data.count - 1 {\n            scannedIndex = index\n            // 0xFF, 0xDA - Start Of Scan\n            let SOS = ImageFormat.JPEGMarker.SOS.bytes\n            if data[index] == SOS[0], data[index + 1] == SOS[1] {\n                if count > 0 {\n                    datas.append(data[0 ..< index])\n                }\n                count += 1\n            }\n            index += 1\n        }\n        \n        // Found more scans this the previous time\n        guard count > scannedCount else { return [] }\n        scannedCount = count\n        \n        // `> 1` checks that we've received a first scan (SOS) and then received\n        // and also received a second scan (SOS). This way we know that we have\n        // at least one full scan available.\n        guard count > 1 else { return [] }\n        return datas\n    }\n    \n    func scanning(_ data: Data) -> Data? {\n        guard data.kf.contains(jpeg: .SOF2) else {\n            return nil\n        }\n        guard scannedIndex + 1 < data.count else {\n            return nil\n        }\n        \n        var index = scannedIndex + 1\n        var count = scannedCount\n        var lastSOSIndex = 0\n        \n        while index < data.count - 1 {\n            scannedIndex = index\n            // 0xFF, 0xDA - Start Of Scan\n            let SOS = ImageFormat.JPEGMarker.SOS.bytes\n            if data[index] == SOS[0], data[index + 1] == SOS[1] {\n                lastSOSIndex = index\n                count += 1\n            }\n            index += 1\n        }\n        \n        // Found more scans this the previous time\n        guard count > scannedCount else { return nil }\n        scannedCount = count\n        \n        // `> 1` checks that we've received a first scan (SOS) and then received\n        // and also received a second scan (SOS). This way we know that we have\n        // at least one full scan available.\n        guard count > 1 && lastSOSIndex > 0 else { return nil }\n        return data[0 ..< lastSOSIndex]\n    }\n    \n    func decode(_ data: Data,\n                with callbacks: [SessionDataTask.TaskCallback],\n                completion: @escaping @Sendable (KFCrossPlatformImage?) -> Void) {\n        guard data.kf.contains(jpeg: .SOF2) else {\n            CallbackQueue.mainCurrentOrAsync.execute { completion(nil) }\n            return\n        }\n        \n        @Sendable func processing(_ data: Data) {\n            let processor = ImageDataProcessor(\n                data: data,\n                callbacks: callbacks,\n                processingQueue: processingQueue\n            )\n            processor.onImageProcessed.delegate(on: self) { (self, result) in\n                guard let image = try? result.0.get() else {\n                    CallbackQueue.mainCurrentOrAsync.execute { completion(nil) }\n                    return\n                }\n                \n                CallbackQueue.mainCurrentOrAsync.execute { completion(image) }\n            }\n            processor.process()\n        }\n        \n        // Blur partial images.\n        let count = scannedCount\n        \n        if option.isBlur, count < 6 {\n            processingQueue.execute {\n                // Progressively reduce blur as we load more scans.\n                let image = KingfisherWrapper<KFCrossPlatformImage>.image(\n                    data: data,\n                    options: self.creatingOptions\n                )\n                let radius = max(2, 14 - count * 4)\n                let temp = image?.kf.blurred(withRadius: CGFloat(radius))\n                processing(temp?.kf.data(format: .JPEG) ?? data)\n            }\n            \n        } else {\n            processing(data)\n        }\n    }\n}\n\nprivate final class ImageProgressiveSerialQueue: @unchecked Sendable {\n    typealias ClosureCallback = @Sendable ((@escaping @Sendable () -> Void)) -> Void\n    \n    private let queue: DispatchQueue\n    private var items: [DispatchWorkItem] = []\n    private var notify: (() -> Void)?\n    private var lastTime: TimeInterval?\n\n    init() {\n        self.queue = DispatchQueue(label: \"com.onevcat.Kingfisher.ImageProgressive.SerialQueue\")\n    }\n    \n    func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) {\n        let completion = { @Sendable [weak self] in\n            guard let self = self else { return }\n            \n            self.queue.async { [weak self] in\n                guard let self = self else { return }\n                guard !self.items.isEmpty else { return }\n                \n                self.items.removeFirst()\n                \n                if let next = self.items.first {\n                    self.queue.asyncAfter(\n                        deadline: .now() + interval,\n                        execute: next\n                    )\n                    \n                } else {\n                    self.lastTime = Date().timeIntervalSince1970\n                    self.notify?()\n                    self.notify = nil\n                }\n            }\n        }\n        \n        queue.async { [weak self] in\n            guard let self = self else { return }\n            \n            let item = DispatchWorkItem {\n                closure(completion)\n            }\n            if self.items.isEmpty {\n                let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0)\n                let delay = difference < interval ? interval - difference : 0\n                self.queue.asyncAfter(deadline: .now() + delay, execute: item)\n            }\n            self.items.append(item)\n        }\n    }\n    \n    func clean() {\n        queue.async { [weak self] in\n            guard let self = self else { return }\n            self.items.forEach { $0.cancel() }\n            self.items.removeAll()\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Image/ImageTransition.swift",
    "content": "//\n//  ImageTransition.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/9/18.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n#if os(iOS) || os(tvOS) || os(visionOS)\nimport UIKit\n\n/// Transition effect to be used when an image is downloaded and set using the `UIImageView` extension API in Kingfisher.\n///\n/// You can assign an enum value with a transition duration as an item in `KingfisherOptionsInfo` to enable the animation\n/// transition. Apple's `UIViewAnimationOptions` are used under the hood.\n///\n/// For custom transitions, you should specify your own transition options, animations, and completion handler as well.\npublic enum ImageTransition: Sendable {\n    /// No animation transition.\n    case none\n    /// Fade effect to the loaded image over a specified duration.\n    case fade(TimeInterval)\n    /// Flip from left transition.\n    case flipFromLeft(TimeInterval)\n    /// Flip from right transition.\n    case flipFromRight(TimeInterval)\n    /// Flip from top transition.\n    case flipFromTop(TimeInterval)\n    /// Flip from bottom transition.\n    case flipFromBottom(TimeInterval)\n    /// Custom transition defined by a general animation block.\n    ///\n    /// - Parameters:\n    ///    - duration: The duration of this custom transition.\n    ///    - options: The `UIView.AnimationOptions` to use in the transition.\n    ///    - animations: The animation block to apply when setting the image.\n    ///    - completion: A block called when the transition animation finishes.\n    case custom(duration: TimeInterval,\n                 options: UIView.AnimationOptions,\n              animations: (@Sendable @MainActor (UIImageView, UIImage) -> Void)?,\n              completion: (@Sendable (Bool) -> Void)?)\n    \n    var duration: TimeInterval {\n        switch self {\n        case .none:                          return 0\n        case .fade(let duration):            return duration\n            \n        case .flipFromLeft(let duration):    return duration\n        case .flipFromRight(let duration):   return duration\n        case .flipFromTop(let duration):     return duration\n        case .flipFromBottom(let duration):  return duration\n            \n        case .custom(let duration, _, _, _): return duration\n        }\n    }\n    \n    var animationOptions: UIView.AnimationOptions {\n        switch self {\n        case .none:                         return []\n        case .fade:                         return .transitionCrossDissolve\n            \n        case .flipFromLeft:                 return .transitionFlipFromLeft\n        case .flipFromRight:                return .transitionFlipFromRight\n        case .flipFromTop:                  return .transitionFlipFromTop\n        case .flipFromBottom:               return .transitionFlipFromBottom\n            \n        case .custom(_, let options, _, _): return options\n        }\n    }\n    \n    @MainActor\n    var animations: ((UIImageView, UIImage) -> Void)? {\n        switch self {\n        case .custom(_, _, let animations, _): return animations\n        default: return { $0.image = $1 }\n        }\n    }\n    \n    var completion: ((Bool) -> Void)? {\n        switch self {\n        case .custom(_, _, _, let completion): return completion\n        default: return nil\n        }\n    }\n}\n#else\n// Just a placeholder for compiling on macOS.\npublic enum ImageTransition: Sendable {\n    case none\n    /// This is a placeholder on macOS now. It is for SwiftUI (KFImage) to identify the fade option only.\n    case fade(TimeInterval)\n}\n#endif\n"
  },
  {
    "path": "Sources/Image/Placeholder.swift",
    "content": "//\n//  Placeholder.swift\n//  Kingfisher\n//\n//  Created by Tieme van Veen on 28/08/2017.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\n#endif\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\n/// Represents a placeholder type that could be set during loading as well as when loading is finished without\n/// getting an image.\npublic protocol Placeholder {\n    \n    /// Called when the placeholder needs to be added to a given image view.\n    /// \n    /// To conform to ``Placeholder``, you implement this method and add your own placeholder view to the \n    /// given `imageView`.\n    ///\n    /// - Parameter imageView: The image view where the placeholder should be added to.\n    @MainActor func add(to imageView: KFCrossPlatformImageView)\n    \n    /// Called when the placeholder needs to be removed from a given image view.\n    ///\n    /// To conform to ``Placeholder``, you implement this method and remove your own placeholder view from the\n    /// given `imageView`.\n    ///\n    /// - Parameter imageView: The image view where the placeholder is already added to and now should be removed from.\n    @MainActor func remove(from imageView: KFCrossPlatformImageView)\n}\n\n@MainActor\nextension KFCrossPlatformImage: Placeholder {\n    public func add(to imageView: KFCrossPlatformImageView) {\n        imageView.image = self\n    }\n    \n    public func remove(from imageView: KFCrossPlatformImageView) {\n        imageView.image = nil\n    }\n    \n    public func add(to base: any KingfisherHasImageComponent) {\n        base.image = self\n    }\n    \n    public func remove(from base: any KingfisherHasImageComponent) {\n        base.image = nil\n    }\n}\n\n/// Default implementation of an arbitrary view as a placeholder. The view will be\n/// added as a subview when adding and removed from its superview when removing.\n///\n/// To use your customized View type as a placeholder, simply have it conform to\n/// `Placeholder` using an extension: `extension MyView: Placeholder {}`.\n@MainActor\nextension Placeholder where Self: KFCrossPlatformView {\n    \n    public func add(to imageView: KFCrossPlatformImageView) {\n        imageView.addSubview(self)\n        translatesAutoresizingMaskIntoConstraints = false\n\n        centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true\n        centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true\n        heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true\n        widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true\n    }\n\n    public func remove(from imageView: KFCrossPlatformImageView) {\n        removeFromSuperview()\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Sources/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>FMWK</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>8.8.0</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>3260</string>\n\t<key>NSPrincipalClass</key>\n\t<string></string>\n</dict>\n</plist>\n"
  },
  {
    "path": "Sources/Networking/AuthenticationChallengeResponsable.swift",
    "content": "//\n//  AuthenticationChallengeResponsable.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/10/11.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n@available(*, deprecated, message: \"Typo. Use `AuthenticationChallengeResponsible` instead\", renamed: \"AuthenticationChallengeResponsible\")\npublic typealias AuthenticationChallengeResponsable = AuthenticationChallengeResponsible\n\n/// Protocol indicates that an authentication challenge could be handled.\npublic protocol AuthenticationChallengeResponsible: AnyObject {\n\n    /// Called when a session level authentication challenge is received.\n    ///\n    /// This method provides a chance to handle and respond to the authentication challenge before the downloading can\n    /// start.\n    ///\n    /// - Parameters:\n    ///   - downloader: The downloader that receives this challenge.\n    ///   - challenge: An object that contains the request for authentication.\n    /// - Returns: The challenge disposition on how the challenge should be handled, and the credential if the\n    /// disposition is `.useCredential`.\n    ///\n    /// > This method is a forward from `URLSessionDelegate.urlSession(_:didReceive:completionHandler:)`.\n    /// > Please refer to the documentation of it in `URLSessionDelegate`.\n    func downloader(\n        _ downloader: ImageDownloader,\n        didReceive challenge: URLAuthenticationChallenge\n    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?)\n\n    /// Called when a task level authentication challenge is received. \n    ///\n    /// This method provides a chance to handle and respond to the authentication challenge before the downloading can\n    /// start.\n    ///\n    /// - Parameters:\n    ///   - downloader: The downloader that receives this challenge.\n    ///   - task: The task whose request requires authentication.\n    ///   - challenge: An object that contains the request for authentication.\n    /// - Returns: The challenge disposition on how the challenge should be handled, and the credential if the\n    /// disposition is `.useCredential`.\n    ///\n    /// > This method is a forward from `URLSessionDataDelegate.urlSession(_:dataTask:didReceive:completionHandler:)`.\n    /// > Please refer to the documentation of it in `URLSessionDataDelegate`.\n    func downloader(\n        _ downloader: ImageDownloader,\n        task: URLSessionTask,\n        didReceive challenge: URLAuthenticationChallenge\n    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?)\n}\n\nextension AuthenticationChallengeResponsible {\n\n    public func downloader(\n        _ downloader: ImageDownloader,\n        didReceive challenge: URLAuthenticationChallenge\n    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?)\n    {\n        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {\n            if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {\n                let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)\n                return (.useCredential, credential)\n            }\n        }\n\n        return (.performDefaultHandling, nil)\n    }\n    \n    public func downloader(\n        _ downloader: ImageDownloader,\n        task: URLSessionTask,\n        didReceive challenge: URLAuthenticationChallenge\n    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {\n        (.performDefaultHandling, nil)\n    }\n\n}\n"
  },
  {
    "path": "Sources/Networking/ImageDataProcessor.swift",
    "content": "//\n//  ImageDataProcessor.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/10/11.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\nprivate let sharedProcessingQueue: CallbackQueue =\n    .dispatch(DispatchQueue(label: \"com.onevcat.Kingfisher.ImageDownloader.Process\"))\n\n// Handles image processing work on an own process queue.\nfinal class ImageDataProcessor: Sendable {\n    let data: Data\n    let callbacks: [SessionDataTask.TaskCallback]\n    let queue: CallbackQueue\n\n    // Note: We have an optimization choice there, to reduce queue dispatch by checking callback\n    // queue settings in each option...\n    let onImageProcessed = Delegate<(Result<KFCrossPlatformImage, KingfisherError>, SessionDataTask.TaskCallback), Void>()\n\n    init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) {\n        self.data = data\n        self.callbacks = callbacks\n        self.queue = processingQueue ?? sharedProcessingQueue\n    }\n\n    func process() {\n        queue.execute {\n            self.doProcess()\n        }\n    }\n\n    private func doProcess() {\n        var processedImages = [String: KFCrossPlatformImage]()\n        for callback in callbacks {\n            let processor = callback.options.processor\n            var image = processedImages[processor.identifier]\n            if image == nil {\n                image = processor.process(item: .data(data), options: callback.options)\n                processedImages[processor.identifier] = image\n            }\n\n            let result: Result<KFCrossPlatformImage, KingfisherError>\n            if let image = image {\n                let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image\n                result = .success(finalImage)\n            } else {\n                let error = KingfisherError.processorError(\n                    reason: .processingFailed(processor: processor, item: .data(data)))\n                result = .failure(error)\n            }\n            onImageProcessed.call((result, callback))\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/ImageDownloader+LivePhoto.swift",
    "content": "//\n//  ImageDownloader+LivePhoto.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2024/10/01.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\npublic struct LivePhotoResourceDownloadingResult: Sendable {\n    \n    /// The original URL of the image request.\n    public let url: URL?\n\n    /// The raw data received from the downloader.\n    public let originalData: Data\n\n    /// Creates an `ImageDownloadResult` object.\n    ///\n    /// - Parameters:\n    ///   - url: The URL from which the image was downloaded.\n    ///   - originalData: The binary data of the image.\n    public init(originalData: Data, url: URL? = nil) {\n        self.url = url\n        self.originalData = originalData\n    }\n}\n\nextension ImageDownloader {\n    \n    public func downloadLivePhotoResource(\n        with url: URL,\n        options: KingfisherParsedOptionsInfo\n    ) async throws -> LivePhotoResourceDownloadingResult {\n        let task = CancellationDownloadTask()\n        return try await withTaskCancellationHandler {\n            try await withCheckedThrowingContinuation { continuation in\n                let downloadTask = downloadLivePhotoResource(with: url, options: options) { result in\n                    continuation.resume(with: result)\n                }\n                if Task.isCancelled {\n                    downloadTask.cancel()\n                } else {\n                    Task {\n                        await task.setTask(downloadTask)\n                    }\n                }\n            }\n        } onCancel: {\n            Task {\n                await task.task?.cancel()\n            }\n        }\n    }\n    \n    @discardableResult\n    public func downloadLivePhotoResource(\n        with url: URL,\n        options: KingfisherParsedOptionsInfo,\n        completionHandler: (@Sendable (Result<LivePhotoResourceDownloadingResult, KingfisherError>) -> Void)? = nil\n    ) -> DownloadTask {\n        var checkedOptions = options\n        if options.processor == DefaultImageProcessor.default {\n            // The default processor is a default behavior so we replace it silently.\n            checkedOptions.processor = LivePhotoImageProcessor.default\n        } else if options.processor != LivePhotoImageProcessor.default {\n            assertionFailure(\"[Kingfisher] Using of custom processors during loading of live photo resource is not supported.\")\n            checkedOptions.processor = LivePhotoImageProcessor.default\n        }\n        return downloadImage(with: url, options: checkedOptions) { result in\n            guard let completionHandler else {\n                return\n            }\n            let newResult = result.map { LivePhotoResourceDownloadingResult(originalData: $0.originalData, url: $0.url) }\n            completionHandler(newResult)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/ImageDownloader.swift",
    "content": "//\n//  ImageDownloader.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/6.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\ntypealias DownloadResult = Result<ImageLoadingResult, KingfisherError>\n\n/// Represents a successful result of an image downloading process.\npublic struct ImageLoadingResult: Sendable {\n\n    /// The downloaded image.\n    public let image: KFCrossPlatformImage\n\n    /// The original URL of the image request.\n    public let url: URL?\n\n    /// The raw data received from the downloader.\n    public let originalData: Data\n    \n    /// The network metrics collected during the download process.\n    public let metrics: NetworkMetrics?\n\n    /// Creates an `ImageDownloadResult` object.\n    ///\n    /// - Parameters:\n    ///   - image: The image of the download result.\n    ///   - url: The URL from which the image was downloaded.\n    ///   - originalData: The binary data of the image.\n    ///   - metrics: The network metrics collected during the download.\n    public init(image: KFCrossPlatformImage, url: URL? = nil, originalData: Data, metrics: NetworkMetrics? = nil) {\n        self.image = image\n        self.url = url\n        self.originalData = originalData\n        self.metrics = metrics\n    }\n}\n\n/// Represents a task in the image downloading process.\n///\n/// When a download starts in Kingfisher, the involved methods always return you an instance of ``DownloadTask``. If you\n/// need to cancel the task during the download process, you can keep a reference to the instance and call ``cancel()``\n/// on it.\npublic final class DownloadTask: @unchecked Sendable {\n    \n    private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.DownloadTaskPropertyQueue\")\n    \n    init(sessionTask: SessionDataTask, cancelToken: SessionDataTask.CancelToken) {\n        _sessionTask = sessionTask\n        _cancelToken = cancelToken\n    }\n    \n    init() { }\n\n    private var _sessionTask: SessionDataTask? = nil\n    \n    /// The ``SessionDataTask`` object associated with this download task. Multiple `DownloadTask`s could refer to the\n    /// same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading tasks for the same\n    /// URL resource simultaneously.\n    ///\n    /// When you call ``DownloadTask/cancel()``, this ``SessionDataTask`` and its cancellation token will be passed\n    /// along. You can use them to identify the cancelled task.\n    public private(set) var sessionTask: SessionDataTask? {\n        get { propertyQueue.sync { _sessionTask } }\n        set { propertyQueue.sync { _sessionTask = newValue } }\n    }\n\n    private var _cancelToken: SessionDataTask.CancelToken? = nil\n    \n    /// The cancellation token used to cancel the task.\n    ///\n    /// This is solely for identifying the task when it is cancelled. To cancel a ``DownloadTask``, call\n    ///  ``DownloadTask/cancelToken``.\n    public private(set) var cancelToken: SessionDataTask.CancelToken? {\n        get { propertyQueue.sync { _cancelToken } }\n        set { propertyQueue.sync { _cancelToken = newValue } }\n    }\n\n    /// Cancel this single download task if it is running.\n    ///\n    /// This method will do nothing if this task is not running.\n    ///\n    /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is currently\n    /// being downloaded. However, even when internally no new session task is created, a ``DownloadTask`` will still\n    /// be created and returned when you call related methods. It will share the session downloading task with a\n    /// previous task.\n    ///\n    /// In this case, if multiple ``DownloadTask``s share a single session download task, calling this method\n    /// does not cancel the actual download process, since there are other `DownloadTask`s need it. It only removes\n    /// `self` from the download list.\n    ///\n    /// > Tip: If you need to cancel all on-going ``DownloadTask``s of a certain URL, use\n    /// ``ImageDownloader/cancel(url:)``. If you need to cancel all downloading tasks of an ``ImageDownloader``, \n    /// use ``ImageDownloader/cancelAll()``.\n    public func cancel() {\n        guard let sessionTask, let cancelToken else { return }\n        sessionTask.cancel(token: cancelToken)\n    }\n    \n    public var isInitialized: Bool {\n        propertyQueue.sync {\n            _sessionTask != nil && _cancelToken != nil\n        }\n    }\n    \n    func linkToTask(_ task: DownloadTask) {\n        self.sessionTask = task.sessionTask\n        self.cancelToken = task.cancelToken\n    }\n}\n\nactor CancellationDownloadTask {\n    var task: DownloadTask?\n    func setTask(_ task: DownloadTask?) {\n        self.task = task\n    }\n}\n\nextension DownloadTask {\n    enum WrappedTask {\n        case download(DownloadTask)\n        case dataProviding\n\n        func cancel() {\n            switch self {\n            case .download(let task): task.cancel()\n            case .dataProviding: break\n            }\n        }\n\n        var value: DownloadTask? {\n            switch self {\n            case .download(let task): return task\n            case .dataProviding: return nil\n            }\n        }\n    }\n}\n\n/// Represents a download manager for requesting an image with a URL from the server.\nopen class ImageDownloader: @unchecked Sendable {\n\n    // MARK: Singleton\n    \n    /// The default downloader.\n    public static let `default` = ImageDownloader(name: \"default\")\n\n    private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.ImageDownloaderPropertyQueue\")\n    \n    // MARK: Public Properties\n    \n    private var _downloadTimeout: TimeInterval = 15.0\n    \n    /// The duration before the download times out.\n    ///\n    /// If the download does not complete before this duration, the URL session will raise a timeout error, which \n    /// Kingfisher wraps and forwards as a ``KingfisherError/ResponseErrorReason/URLSessionError(error:)``.\n    ///\n    /// The default timeout is set to 15 seconds.\n    open var downloadTimeout: TimeInterval {\n        get { propertyQueue.sync { _downloadTimeout } }\n        set { propertyQueue.sync { _downloadTimeout = newValue } }\n    }\n    \n    /// A set of trusted hosts when receiving server trust challenges.\n    ///\n    /// A challenge with host name contained in this set will be ignored. You can use this set to specify the\n    /// self-signed site. It only will be used if you don't specify the\n    ///  ``ImageDownloader/authenticationChallengeResponder``.\n    ///\n    /// > If ``ImageDownloader/authenticationChallengeResponder`` is set, this property will be ignored and the\n    /// implementation of ``ImageDownloader/authenticationChallengeResponder`` will be used instead.\n    open var trustedHosts: Set<String>?\n    \n    /// Use this to supply a configuration for the downloader. \n    ///\n    /// By default, `URLSessionConfiguration.ephemeral` will be used.\n    ///\n    /// You can modify the configuration before a downloading task begins. A configuration without persistent storage \n    /// for caches is necessary for the downloader to function correctly.\n    ///\n    /// > Setting a new session delegate to the downloader will invalidate the existing session and create a new one \n    /// > with the new value and the ``sessionDelegate``.\n    open var sessionConfiguration = URLSessionConfiguration.ephemeral {\n        didSet {\n            session.invalidateAndCancel()\n            session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)\n        }\n    }\n    \n    /// The session delegate which is used to handle the session related tasks.\n    ///\n    /// > Setting a new session delegate to the downloader will invalidate the existing session and create a new one \n    /// > with the new value and the ``sessionConfiguration``.\n    open var sessionDelegate: SessionDelegate {\n        didSet {\n            session.invalidateAndCancel()\n            session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)\n            setupSessionHandler()\n        }\n    }\n    \n    /// Whether the download requests should use pipeline or not. \n    ///\n    /// It sets the `httpShouldUsePipelining` of the `URLRequest` for the download task. Default is false.\n    open var requestsUsePipelining = false\n\n    /// The delegate of this `ImageDownloader` object.\n    ///\n    /// See the ``ImageDownloaderDelegate`` protocol for more information.\n    open weak var delegate: (any ImageDownloaderDelegate)?\n\n    /// A responder for authentication challenges.\n    ///\n    /// The downloader forwards the received authentication challenge for the downloading session to this responder.\n    /// See ``AuthenticationChallengeResponsible`` for more.\n    open weak var authenticationChallengeResponder: (any AuthenticationChallengeResponsible)?\n\n    // The downloader name.\n    private let name: String\n    \n    // The session bound to the downloader.\n    private var session: URLSession\n\n    private let lock = NSLock()\n\n    // MARK: Initializers\n\n    /// Creates a downloader with a given name.\n    ///\n    /// - Parameter name: The name for the downloader. It should not be empty.\n    public init(name: String) {\n        if name.isEmpty {\n            fatalError(\"[Kingfisher] You should specify a name for the downloader. \"\n                + \"A downloader with empty name is not permitted.\")\n        }\n\n        self.name = name\n\n        sessionDelegate = SessionDelegate()\n        session = URLSession(\n            configuration: sessionConfiguration,\n            delegate: sessionDelegate,\n            delegateQueue: nil)\n\n        authenticationChallengeResponder = self\n        setupSessionHandler()\n    }\n\n    deinit { session.invalidateAndCancel() }\n\n    private func setupSessionHandler() {\n        sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in\n            await (self.authenticationChallengeResponder ?? self).downloader(self, didReceive: invoke.1)\n        }\n        sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in\n            await (self.authenticationChallengeResponder ?? self).downloader(self, task: invoke.1, didReceive: invoke.2)\n        }\n        sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in\n            (self.delegate ?? self).isValidStatusCode(code, for: self)\n        }\n        sessionDelegate.onResponseReceived.delegate(on: self) { (self, response) in\n            await (self.delegate ?? self).imageDownloader(self, didReceive: response)\n        }\n        sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in\n            let (url, result) = value\n            do {\n                let value = try result.get()\n                self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil)\n            } catch {\n                self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error)\n            }\n        }\n        sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in\n            (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task)\n        }\n    }\n\n    // Wraps `completionHandler` to `onCompleted` respectively.\n    private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate<DownloadResult, Void>? {\n        completionHandler.map { block -> Delegate<DownloadResult, Void> in\n            let delegate =  Delegate<Result<ImageLoadingResult, KingfisherError>, Void>()\n            delegate.delegate(on: self) { (self, callback) in\n                block(callback)\n            }\n            return delegate\n        }\n    }\n\n    private func createTaskCallback(\n        _ completionHandler: ((DownloadResult) -> Void)?,\n        options: KingfisherParsedOptionsInfo\n    ) -> SessionDataTask.TaskCallback\n    {\n        SessionDataTask.TaskCallback(\n            onCompleted: createCompletionCallBack(completionHandler),\n            options: options\n        )\n    }\n\n    private func createDownloadContext(\n        with url: URL,\n        options: KingfisherParsedOptionsInfo,\n        done: @escaping (@Sendable (Result<DownloadingContext, KingfisherError>) -> Void)\n    )\n    {\n        @Sendable func checkRequestAndDone(r: URLRequest) {\n            // There is a possibility that request modifier changed the url to `nil` or empty.\n            // In this case, throw an error.\n            guard let url = r.url, !url.absoluteString.isEmpty else {\n                done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r))))\n                return\n            }\n            done(.success(DownloadingContext(url: url, request: r, options: options)))\n        }\n\n        // Creates default request.\n        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)\n        request.httpShouldUsePipelining = requestsUsePipelining\n        if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil {\n            request.allowsConstrainedNetworkAccess = false\n        }\n        \n        guard let requestModifier = options.requestModifier else {\n            checkRequestAndDone(r: request)\n            return\n        }\n        \n        // Modifies request before sending.\n        // FIXME: A temporary solution for keep the sync `ImageDownloadRequestModifier` behavior as before.\n        // We should be able to combine two cases once the full async support can be introduced to Kingfisher.\n        if let m = requestModifier as? any ImageDownloadRequestModifier {\n            guard let result = m.modified(for: request) else {\n                done(.failure(KingfisherError.requestError(reason: .emptyRequest)))\n                return\n            }\n            checkRequestAndDone(r: result)\n        } else  {\n            Task { [request] in\n                guard let result = await requestModifier.modified(for: request) else {\n                    done(.failure(KingfisherError.requestError(reason: .emptyRequest)))\n                    return\n                }\n                checkRequestAndDone(r: result)\n            }\n        }\n    }\n\n    private func addDownloadTask(\n        context: DownloadingContext,\n        callback: SessionDataTask.TaskCallback\n    ) -> DownloadTask\n    {\n        lock.lock()\n        defer { lock.unlock() }\n\n        // Ready to start download. Add it to session task manager (`sessionHandler`)\n        let downloadTask: DownloadTask\n        if let existingTask = sessionDelegate.task(for: context.url) {\n            downloadTask = sessionDelegate.append(existingTask, callback: callback)\n        } else {\n            let sessionDataTask = session.dataTask(with: context.request)\n            sessionDataTask.priority = context.options.downloadPriority\n            downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback)\n        }\n        return downloadTask\n    }\n\n    private func reportWillDownloadImage(url: URL, request: URLRequest) {\n        delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)\n    }\n\n    private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) {\n        var response: URLResponse?\n        var err: (any Error)?\n        do {\n            response = try result.get().1\n        } catch {\n            err = error\n        }\n        self.delegate?.imageDownloader(\n            self,\n            didFinishDownloadingImageForURL: url,\n            with: response,\n            error: err\n        )\n    }\n\n    private func reportDidProcessImage(\n        result: Result<KFCrossPlatformImage, KingfisherError>, url: URL, response: URLResponse?\n    )\n    {\n        if let image = try? result.get() {\n            self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)\n        }\n    }\n\n    private func startDownloadTask(\n        context: DownloadingContext,\n        callback: SessionDataTask.TaskCallback,\n        beforeTaskResume: ((DownloadTask) -> Void)? = nil\n    ) -> DownloadTask\n    {\n        let downloadTask = addDownloadTask(context: context, callback: callback)\n\n        guard let sessionTask = downloadTask.sessionTask, !sessionTask.started else {\n            beforeTaskResume?(downloadTask)\n            return downloadTask\n        }\n\n        sessionTask.onTaskDone.delegate(on: self) { [weak sessionTask] (self, done) in\n            // Underlying downloading finishes.\n            // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]\n            let (result, callbacks) = done\n\n            // Before processing the downloaded data.\n            self.reportDidDownloadImageData(result: result, url: context.url)\n\n            switch result {\n            // Download finished. Now process the data to an image.\n            case .success(let (data, response)):\n                let processor = ImageDataProcessor(\n                    data: data, callbacks: callbacks, processingQueue: context.options.processingQueue\n                )\n                processor.onImageProcessed.delegate(on: self) { (self, done) in\n                    // `onImageProcessed` will be called for `callbacks.count` times, with each\n                    // `SessionDataTask.TaskCallback` as the input parameter.\n                    // result: Result<Image>, callback: SessionDataTask.TaskCallback\n                    let (result, callback) = done\n\n                    self.reportDidProcessImage(result: result, url: context.url, response: response)\n\n                    let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data, metrics: sessionTask?.metrics) }\n                    let queue = callback.options.callbackQueue\n                    queue.execute { callback.onCompleted?.call(imageResult) }\n                }\n                processor.process()\n\n            case .failure(let error):\n                callbacks.forEach { callback in\n                    let queue = callback.options.callbackQueue\n                    queue.execute { callback.onCompleted?.call(.failure(error)) }\n                }\n            }\n        }\n\n        // Ensure `beforeTaskResume` runs before `resume()`. Some stubbing layers may complete the request\n        // synchronously during `resume()`, so any \"task started\" callback should be invoked before that.\n        beforeTaskResume?(downloadTask)\n\n        reportWillDownloadImage(url: context.url, request: context.request)\n        sessionTask.resume()\n        return downloadTask\n    }\n\n    // MARK: Downloading Task\n    /// Downloads an image with a URL and options.\n    ///\n    /// - Parameters:\n    ///   - url: The target URL.\n    ///   - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.\n    ///   - completionHandler: Called when the download progress finishes. This block will be called in the queue \n    ///   defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter.\n    ///\n    /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task.\n    @discardableResult\n    open func downloadImage(\n        with url: URL,\n        options: KingfisherParsedOptionsInfo,\n        completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask\n    {\n        let downloadTask = DownloadTask()\n        createDownloadContext(with: url, options: options) { result in\n            switch result {\n            case .success(let context):\n                let taskCallback = self.createTaskCallback(completionHandler, options: options)\n                if let modifier = options.requestModifier {\n                    _ = self.startDownloadTask(context: context, callback: taskCallback, beforeTaskResume: { actualDownloadTask in\n                        downloadTask.linkToTask(actualDownloadTask)\n                        modifier.onDownloadTaskStarted?(downloadTask)\n                    })\n                } else {\n                    let actualDownloadTask = self.startDownloadTask(context: context, callback: taskCallback)\n                    downloadTask.linkToTask(actualDownloadTask)\n                }\n            case .failure(let error):\n                options.callbackQueue.execute {\n                    completionHandler?(.failure(error))\n                }\n            }\n        }\n\n        return downloadTask\n    }\n\n    /// Downloads an image with a URL and options.\n    ///\n    /// - Parameters:\n    ///   - url: The target URL.\n    ///   - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.\n    ///   - progressBlock: Called when the download progress is updated. This block will always be called on the main \n    ///   queue.\n    ///   - completionHandler: Called when the download progress finishes. This block will be called in the queue \n    ///   defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter.\n    ///\n    /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task.\n    @discardableResult\n    open func downloadImage(\n        with url: URL,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil,\n        completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask\n    {\n        var info = KingfisherParsedOptionsInfo(options)\n        if let block = progressBlock {\n            info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]\n        }\n        return downloadImage(\n            with: url,\n            options: info,\n            completionHandler: completionHandler)\n    }\n\n    /// Downloads an image with a URL and options.\n    ///\n    /// - Parameters:\n    ///   - url: The target URL.\n    ///   - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.\n    ///   - completionHandler: Called when the download progress finishes. This block will be called in the queue\n    ///   defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter.\n    ///\n    /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task.\n    @discardableResult\n    open func downloadImage(\n        with url: URL,\n        options: KingfisherOptionsInfo? = nil,\n        completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask\n    {\n        downloadImage(\n            with: url,\n            options: KingfisherParsedOptionsInfo(options),\n            completionHandler: completionHandler\n        )\n    }\n}\n\n// Concurrency\nextension ImageDownloader {\n    /// Downloads an image with a URL and option.\n    ///\n    /// - Parameters:\n    ///   - url: Target URL.\n    ///   - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.\n    /// - Returns: The image loading result.\n    ///\n    /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in.\n    public func downloadImage(\n        with url: URL,\n        options: KingfisherParsedOptionsInfo\n    ) async throws -> ImageLoadingResult {\n        let task = CancellationDownloadTask()\n        return try await withTaskCancellationHandler {\n            try await withCheckedThrowingContinuation { continuation in\n                let downloadTask = downloadImage(with: url, options: options) { result in\n                    continuation.resume(with: result)\n                }\n                if Task.isCancelled {\n                    downloadTask.cancel()\n                } else {\n                    Task {\n                        await task.setTask(downloadTask)\n                    }\n                }\n            }\n        } onCancel: {\n            Task {\n                await task.task?.cancel()\n            }\n        }\n    }\n    \n    /// Downloads an image with a URL and option.\n    ///\n    /// - Parameters:\n    ///   - url: Target URL.\n    ///   - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.\n    ///   - progressBlock: Called when the download progress updated. This block will be always be called in main queue.\n    /// - Returns: The image loading result.\n    ///\n    /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in.\n    public func downloadImage(\n        with url: URL,\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: DownloadProgressBlock? = nil\n    ) async throws -> ImageLoadingResult\n    {\n        var info = KingfisherParsedOptionsInfo(options)\n        if let block = progressBlock {\n            info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]\n        }\n        return try await downloadImage(with: url, options: info)\n    }\n    \n    /// Downloads an image with a URL and option.\n    ///\n    /// - Parameters:\n    ///   - url: Target URL.\n    ///   - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.\n    /// - Returns: The image loading result.\n    ///\n    /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in.\n    public func downloadImage(\n        with url: URL,\n        options: KingfisherOptionsInfo? = nil\n    ) async throws -> ImageLoadingResult\n    {\n        try await downloadImage(with: url, options: KingfisherParsedOptionsInfo(options))\n    }\n}\n\n// MARK: Cancelling Task\nextension ImageDownloader {\n\n    /// Cancel all downloading tasks for this ``ImageDownloader``.\n    ///\n    /// It will trigger the completion handlers for all not-yet-finished downloading tasks with a cancellation error.\n    ///\n    /// If you need to only cancel a certain task, call ``DownloadTask/cancel()`` on the task returned by the\n    /// downloading methods. If you need to cancel all ``DownloadTask``s of a certain URL, use\n    /// ``ImageDownloader/cancel(url:)``.\n    public func cancelAll() {\n        sessionDelegate.cancelAll()\n    }\n\n    /// Cancel all downloading tasks for a given URL.\n    ///\n    /// It will trigger the completion handlers for all not-yet-finished downloading tasks for the URL with a\n    /// cancellation error.\n    ///\n    /// - Parameter url: The URL for which you want to cancel downloading.\n    public func cancel(url: URL) {\n        sessionDelegate.cancel(url: url)\n    }\n}\n\n// Use the default implementation from extension of `AuthenticationChallengeResponsible`.\nextension ImageDownloader: AuthenticationChallengeResponsible {}\n\n// Use the default implementation from extension of `ImageDownloaderDelegate`.\nextension ImageDownloader: ImageDownloaderDelegate {}\n\nextension ImageDownloader {\n    struct DownloadingContext {\n        let url: URL\n        let request: URLRequest\n        let options: KingfisherParsedOptionsInfo\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/ImageDownloaderDelegate.swift",
    "content": "//\n//  ImageDownloaderDelegate.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/10/11.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// Protocol for handling events for ``ImageDownloader``.\n///\n/// This delegate protocol provides a set of methods related to the stages and rules of the image downloader. You use\n/// the provided methods to inspect the downloader working phases or respond to some events to make decisions.\npublic protocol ImageDownloaderDelegate: AnyObject {\n\n    /// Called when the ``ImageDownloader`` object is about to start downloading an image from a specified URL.\n    ///\n    /// - Parameters:\n    ///   - downloader: The ``ImageDownloader`` object used for the downloading operation.\n    ///   - url: The URL of the starting request.\n    ///   - request: The request object for the download process.\n    func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?)\n\n    /// Called when the ``ImageDownloader`` completes a downloading request with success or failure.\n    ///\n    /// - Parameters:\n    ///   - downloader: The ``ImageDownloader`` object used for the downloading operation.\n    ///   - url: The URL of the original request.\n    ///   - response: The response object of the downloading process.\n    ///   - error: The error in case of failure.\n    func imageDownloader(\n        _ downloader: ImageDownloader,\n        didFinishDownloadingImageForURL url: URL,\n        with response: URLResponse?,\n        error: (any Error)?)\n    \n    /// Called when the ``ImageDownloader`` object successfully downloads image data with a specified task.\n    ///\n    /// This is your last chance to verify or modify the downloaded data before Kingfisher attempts to perform\n    /// additional processing on the image data.\n    ///\n    /// - Parameters:\n    ///   - downloader: The ``ImageDownloader`` object used for the downloading operation.\n    ///   - data: The original downloaded data.\n    ///   - task: The data task containing request and response information for the download.\n    /// - Returns: The data that Kingfisher should use to create an image. You need to provide valid data that is in\n    /// one of the supported image file formats. Kingfisher will process this data and attempt to convert it into an\n    /// image object.\n    func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data?\n  \n    /// Called when the ``ImageDownloader`` object successfully downloads image data from a specified URL.\n    ///\n    /// This is your last chance to verify or modify the downloaded data before Kingfisher attempts to perform\n    /// additional processing on the image data.\n    ///\n    /// - Parameters:\n    ///   - downloader: The ``ImageDownloader`` object used for the downloading operation.\n    ///   - data: The original downloaded data.\n    ///   - url: The URL of the original request.\n    ///\n    /// - Returns: The data that Kingfisher should use to create an image. You need to provide valid data that is in\n    /// one of the supported image file formats. Kingfisher will process this data and attempt to convert it into an\n    /// image object.\n    ///\n    /// This method can be used to preprocess raw image data before the creation of the `Image` instance (e.g.,\n    /// decrypting or verification). If `nil` is returned, the processing is interrupted and a\n    /// ``KingfisherError/ResponseErrorReason/dataModifyingFailed(task:)`` error will be raised. You can use this fact\n    /// to stop the image processing flow if you find that the data is corrupted or malformed.\n    ///\n    /// > If the ``SessionDataTask`` version of `imageDownloader(_:didDownload:with:)` is implemented, this method will\n    /// > not be called anymore.\n    func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data?\n\n    /// Called when the ``ImageDownloader`` object successfully downloads and processes an image from a specified URL.\n    ///\n    /// - Parameters:\n    ///   - downloader: The ``ImageDownloader`` object used for the downloading operation.\n    ///   - image: The downloaded and processed image.\n    ///   - url: The URL of the original request.\n    ///   - response: The original response object of the downloading process.\n    func imageDownloader(\n        _ downloader: ImageDownloader,\n        didDownload image: KFCrossPlatformImage,\n        for url: URL,\n        with response: URLResponse?)\n\n    /// Checks if a received HTTP status code is valid or not.\n    ///\n    /// By default, a status code in the range `200..<400` is considered as valid. If an invalid code is received,\n    /// the downloader will raise a ``KingfisherError/ResponseErrorReason/invalidHTTPStatusCode(response:)`` error.\n    ///\n    /// - Parameters:\n    ///   - code: The received HTTP status code.\n    ///   - downloader: The ``ImageDownloader`` object requesting validation of the status code.\n    /// - Returns: A value indicating whether this HTTP status code is valid or not.\n    ///\n    /// > If the default range of `200..<400` as valid codes does not suit your needs, you can implement this method to\n    /// change that behavior.\n    func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool\n\n    /// Called when the task has received a valid HTTP response after passing other checks such as the status code. \n    /// You can perform additional checks or verifications on the response to determine if the download should be\n    /// allowed or cancelled.\n    ///\n    /// For example, this is useful if you want to verify some header values in the response before actually starting \n    /// the download.\n    ///\n    /// If implemented, you have to return a proper response disposition, such as `.allow` to start the actual\n    /// downloading or `.cancel` to cancel the task. If `.cancel` is used as the disposition, the downloader will raise \n    /// a ``KingfisherError/ResponseErrorReason/cancelledByDelegate(response:)`` error. If not implemented, any response\n    /// that passes other checks will be allowed, and the download will start.\n    ///\n    /// - Parameters:\n    ///   - downloader: The `ImageDownloader` object used for the downloading operation.\n    ///   - response: The original response object of the downloading process.\n    ///\n    /// - Returns: The disposition for the download task. You have to return either `.allow` or `.cancel`.\n    func imageDownloader(\n        _ downloader: ImageDownloader,\n        didReceive response: URLResponse\n    ) async -> URLSession.ResponseDisposition\n}\n\n// Default implementation for `ImageDownloaderDelegate`.\nextension ImageDownloaderDelegate {\n    public func imageDownloader(\n        _ downloader: ImageDownloader,\n        willDownloadImageForURL url: URL,\n        with request: URLRequest?) {}\n\n    public func imageDownloader(\n        _ downloader: ImageDownloader,\n        didFinishDownloadingImageForURL url: URL,\n        with response: URLResponse?,\n        error: (any Error)?) {}\n\n    public func imageDownloader(\n        _ downloader: ImageDownloader,\n        didDownload image: KFCrossPlatformImage,\n        for url: URL,\n        with response: URLResponse?) {}\n\n    public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {\n        return (200..<400).contains(code)\n    }\n  \n    public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? {\n        guard let url = task.originalURL else {\n            return data\n        }\n        return imageDownloader(downloader, didDownload: data, for: url)\n    }\n  \n    public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {\n        return data\n    }\n\n    public func imageDownloader(\n        _ downloader: ImageDownloader,\n        didReceive response: URLResponse\n    ) async -> URLSession.ResponseDisposition {\n        .allow\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/ImageModifier.swift",
    "content": "//\n//  ImageModifier.swift\n//  Kingfisher\n//\n//  Created by Ethan Gill on 2017/11/28.\n//\n//  Copyright (c) 2019 Ethan Gill <ethan.gill@me.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// An ``ImageModifier`` can be used to change properties on an image between cache serialization and the actual use of\n/// the image.\n///\n/// The ``ImageModifier/modify(_:)`` method will be called after the image is retrieved from its source and before it\n/// is returned to the caller. This modified image is expected to be used only for rendering purposes; any changes\n/// applied by the ``ImageModifier`` will not be serialized or cached.\npublic protocol ImageModifier: Sendable {\n    \n    /// Modify an input `Image`.\n    ///\n    /// - Parameter image: The image which will be modified by `self`.\n    ///\n    /// - Returns: The modified image.\n    ///\n    /// > Important: The return value will be unmodified if modification is not possible on the current platform.\n    func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage\n}\n\n/// A wrapper that simplifies the creation of an ``ImageModifier``.\n///  \n/// This type conforms to ``ImageModifier`` and encapsulates an image modification block. If the `block` throws an\n/// error, the original image will be used.\npublic struct AnyImageModifier: ImageModifier {\n\n    /// A block that modifies images, or returns the original image if modification cannot be performed, along with an \n    /// error.\n    let block: @Sendable (KFCrossPlatformImage) throws -> KFCrossPlatformImage\n\n    /// Creates an ``AnyImageModifier`` with a given `modify` block.\n    /// - Parameter modify: A block which is used to modify the input image.\n    public init(modify: @escaping @Sendable (KFCrossPlatformImage) throws -> KFCrossPlatformImage) {\n        block = modify\n    }\n\n    public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {\n        return (try? block(image)) ?? image\n    }\n}\n\n#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)\nimport UIKit\n\n/// Modifier for setting the rendering mode of images.\npublic struct RenderingModeImageModifier: ImageModifier {\n\n    /// The rendering mode to apply to the image.\n    public let renderingMode: UIImage.RenderingMode\n\n    /// Creates a ``RenderingModeImageModifier``.\n    ///\n    /// - Parameter renderingMode: The rendering mode to apply to the image. The default is `.automatic`.\n    public init(renderingMode: UIImage.RenderingMode = .automatic) {\n        self.renderingMode = renderingMode\n    }\n\n    public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {\n        return image.withRenderingMode(renderingMode)\n    }\n}\n\n/// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images.\npublic struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier {\n\n    /// Creates a ``FlipsForRightToLeftLayoutDirectionImageModifier``.\n    public init() {}\n\n    public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {\n        return image.imageFlippedForRightToLeftLayoutDirection()\n    }\n}\n\n/// Modifier for setting the `alignmentRectInsets` property of images.\npublic struct AlignmentRectInsetsImageModifier: ImageModifier {\n\n    /// The alignment insets to apply to the image.\n    public let alignmentInsets: UIEdgeInsets\n    \n    /// Creates a ``AlignmentRectInsetsImageModifier``.\n    /// - Parameter alignmentInsets: The alignment insets to apply to the image.\n    public init(alignmentInsets: UIEdgeInsets) {\n        self.alignmentInsets = alignmentInsets\n    }\n\n    public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {\n        return image.withAlignmentRectInsets(alignmentInsets)\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Networking/ImagePrefetcher.swift",
    "content": "//\n//  ImagePrefetcher.swift\n//  Kingfisher\n//\n//  Created by Claire Knight <claire.knight@moggytech.co.uk> on 24/02/2016\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n\n#if os(macOS)\nimport AppKit\n#else\nimport UIKit\n#endif\n\n/// Progress update block of prefetcher when initialized with a list of resources.\n///\n/// - Parameters:\n///   - skippedResources: An array of resources that are already cached before the prefetching begins.\n///   - failedResources: An array of resources that fail to be downloaded. This could be because of being cancelled while downloading, encountering an error during downloading, or the download not being started at all.\n///   - completedResources: An array of resources that are downloaded and cached successfully.\npublic typealias PrefetcherProgressBlock =\n    @Sendable (_ skippedResources: [any Resource], _ failedResources: [any Resource], _ completedResources: [any Resource]) -> Void\n\n/// Progress update block of prefetcher when initialized with a list of resources.\n///\n/// - Parameters:\n///   - skippedSources: An array of sources that are already cached before the prefetching begins.\n///   - failedSources: An array of sources that fail to be fetched.\n///   - completedResources: An array of sources that are fetched and cached successfully.\npublic typealias PrefetcherSourceProgressBlock =\n    @Sendable (_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void\n\n/// Completion block of prefetcher when initialized with a list of sources.\n///\n/// - Parameters:\n///   - skippedResources: An array of resources that are already cached before the prefetching begins.\n///   - failedResources: An array of resources that fail to be downloaded. This could be because of being cancelled while downloading, encountering an error during downloading, or the download not being started at all.\n///   - completedResources: An array of resources that are downloaded and cached successfully.\npublic typealias PrefetcherCompletionHandler =\n    @Sendable (_ skippedResources: [any Resource], _ failedResources: [any Resource], _ completedResources: [any Resource]) -> Void\n\n/// Completion block of prefetcher when initialized with a list of sources.\n///\n/// - Parameters:\n///   - skippedSources: An array of sources that are already cached before the prefetching begins.\n///   - failedSources: An array of sources that fail to be fetched.\n///   - completedSources: An array of sources that are fetched and cached successfully.\npublic typealias PrefetcherSourceCompletionHandler =\n    @Sendable (_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void\n\n/// ``ImagePrefetcher`` represents a downloading manager for requesting many images via URLs and then caching them.\n///\n/// Use this class when you know a list of image resources and want to download them before showing. It also works with\n/// some Cocoa prefetching mechanisms like table view or collection view `prefetchDataSource` to start image downloading\n/// and caching before they are displayed on screen.\npublic class ImagePrefetcher: CustomStringConvertible, @unchecked Sendable {\n\n    public var description: String {\n        return \"\\(Unmanaged.passUnretained(self).toOpaque())\"\n    }\n    \n    /// The maximum concurrent downloads to use when prefetching images.\n    ///\n    ///  The default is 5.\n    public var maxConcurrentDownloads = 5\n\n    private let prefetchSources: [Source]\n    private let optionsInfo: KingfisherParsedOptionsInfo\n\n    private var progressBlock: PrefetcherProgressBlock?\n    private var completionHandler: PrefetcherCompletionHandler?\n\n    private var progressSourceBlock: PrefetcherSourceProgressBlock?\n    private var completionSourceHandler: PrefetcherSourceCompletionHandler?\n    \n    private var tasks = [String: DownloadTask.WrappedTask]()\n    \n    private var pendingSources: ArraySlice<Source>\n    private var skippedSources = [Source]()\n    private var completedSources = [Source]()\n    private var failedSources = [Source]()\n    \n    private var stopped = false\n    \n    // A manager used for prefetching. We will use the helper methods in manager.\n    private let manager: KingfisherManager\n\n    private let prefetchQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.ImagePrefetcher.prefetchQueue\")\n    private static let requestingQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue\")\n\n    private var finished: Bool {\n        let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count\n        return totalFinished == prefetchSources.count && tasks.isEmpty\n    }\n\n    /// Creates an image prefetcher with an array of URLs.\n    ///\n    /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.\n    /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the\n    /// prefetching process. The images that are already cached will be skipped without being downloaded again.\n    ///\n    /// - Parameters:\n    ///   - urls: The URLs to be prefetched.\n    ///   - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information.\n    ///   - progressBlock: Called every time a resource is downloaded, skipped, or canceled.\n    ///   - completionHandler: Called when the whole prefetching process is finished.\n    ///\n    /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache\n    /// targets, respectively. You can specify other downloaders or caches by using a customized\n    /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The\n    /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method.\n    public convenience init(\n        urls: [URL],\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: PrefetcherProgressBlock? = nil,\n        completionHandler: PrefetcherCompletionHandler? = nil)\n    {\n        let resources: [any Resource] = urls.map { $0 }\n        self.init(\n            resources: resources,\n            options: options,\n            progressBlock: progressBlock,\n            completionHandler: completionHandler)\n    }\n\n    /// Creates an image prefetcher with an array of ``Resource``s.\n    ///\n    /// The prefetcher should be initiated with a list of prefetching targets. The resource list is immutable.\n    /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the\n    /// prefetching process. The images that are already cached will be skipped without being downloaded again.\n    ///\n    /// - Parameters:\n    ///   - resources: An array of resource to be prefetched. See ``ImageResource``.\n    ///   - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information.\n    ///   - progressBlock: Called every time a resource is downloaded, skipped, or canceled.\n    ///   - completionHandler: Called when the whole prefetching process is finished.\n    ///\n    /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache\n    /// targets, respectively. You can specify other downloaders or caches by using a customized\n    /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The\n    /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method.\n    public convenience init(\n        resources: [any Resource],\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: PrefetcherProgressBlock? = nil,\n        completionHandler: PrefetcherCompletionHandler? = nil)\n    {\n        self.init(sources: resources.map { $0.convertToSource() }, options: options)\n        self.progressBlock = progressBlock\n        self.completionHandler = completionHandler\n    }\n\n    /// Creates an image prefetcher with an array of ``Source``s.\n    ///\n    /// The prefetcher should be initiated with a list of prefetching targets. The source list is immutable.\n    /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the\n    /// prefetching process. The images that are already cached will be skipped without being downloaded again.\n    ///\n    /// - Parameters:\n    ///   - sources: An array of resource to be prefetched. See ``Source``.\n    ///   - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information.\n    ///   - progressBlock: Called every time a resource is downloaded, skipped, or canceled.\n    ///   - completionHandler: Called when the whole prefetching process is finished.\n    ///\n    /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache\n    /// targets, respectively. You can specify other downloaders or caches by using a customized\n    /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The\n    /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method.\n    public convenience init(sources: [Source],\n        options: KingfisherOptionsInfo? = nil,\n        progressBlock: PrefetcherSourceProgressBlock? = nil,\n        completionHandler: PrefetcherSourceCompletionHandler? = nil)\n    {\n        self.init(sources: sources, options: options)\n        self.progressSourceBlock = progressBlock\n        self.completionSourceHandler = completionHandler\n    }\n\n    init(sources: [Source], options: KingfisherOptionsInfo?) {\n        var options = KingfisherParsedOptionsInfo(options)\n        prefetchSources = sources\n        pendingSources = ArraySlice(sources)\n\n        // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options.\n        // Add our own callback dispatch queue to make sure all internal callbacks are\n        // coming back in our expected queue.\n        options.callbackQueue = .dispatch(prefetchQueue)\n        optionsInfo = options\n\n        let cache = optionsInfo.targetCache ?? .default\n        let downloader = optionsInfo.downloader ?? .default\n        manager = KingfisherManager(downloader: downloader, cache: cache)\n    }\n\n    /// Starts downloading the resources and caching them.\n    ///\n    /// This can be useful for the background downloading of assets that are required for later use in an app. This\n    /// code will not try to update any UI with the results of the process.\n    public func start() {\n        prefetchQueue.async {\n            guard !self.stopped else {\n                assertionFailure(\"You can not restart the same prefetcher. Try to create a new prefetcher.\")\n                self.handleComplete()\n                return\n            }\n\n            guard self.maxConcurrentDownloads > 0 else {\n                assertionFailure(\"There should be concurrent downloads value should be at least 1.\")\n                self.handleComplete()\n                return\n            }\n\n            // Empty case.\n            guard self.prefetchSources.count > 0 else {\n                self.handleComplete()\n                return\n            }\n\n            let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads)\n            for _ in 0 ..< initialConcurrentDownloads {\n                if let resource = self.pendingSources.popFirst() {\n                    self.startPrefetching(resource)\n                }\n            }\n        }\n    }\n\n    /// Stops the current downloading progress and cancels any future prefetching activity that might be occurring.\n    public func stop() {\n        prefetchQueue.async {\n            if self.finished { return }\n            self.stopped = true\n            self.tasks.values.forEach { $0.cancel() }\n        }\n    }\n    \n    private func downloadAndCache(_ source: Source, retryContext: RetryContext? = nil) {\n\n        let retryStrategy = optionsInfo.retryStrategy\n\n        @Sendable func completeWithSuccess() {\n            self.completedSources.append(source)\n            self.reportProgress()\n            if self.stopped {\n                if self.tasks.isEmpty {\n                    self.failedSources.append(contentsOf: self.pendingSources)\n                    self.handleComplete()\n                }\n            } else {\n                self.reportCompletionOrStartNext()\n            }\n        }\n\n        @Sendable func completeWithFailure() {\n            self.failedSources.append(source)\n            self.reportProgress()\n            if self.stopped {\n                if self.tasks.isEmpty {\n                    self.failedSources.append(contentsOf: self.pendingSources)\n                    self.handleComplete()\n                }\n            } else {\n                self.reportCompletionOrStartNext()\n            }\n        }\n\n        let downloadTaskCompletionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void) = {\n            result in\n\n            self.tasks.removeValue(forKey: source.cacheKey)\n            switch result {\n            case .success:\n                completeWithSuccess()\n            case .failure(let error):\n                guard let retryStrategy else {\n                    completeWithFailure()\n                    return\n                }\n\n                let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error)\n                retryStrategy.retry(context: context) { decision in\n                    switch decision {\n                    case .retry(let userInfo):\n                        context.userInfo = userInfo\n                        self.prefetchQueue.async {\n                            guard !self.stopped else {\n                                completeWithFailure()\n                                return\n                            }\n                            self.downloadAndCache(source, retryContext: context)\n                        }\n                    case .stop:\n                        self.prefetchQueue.async {\n                            guard !self.stopped else {\n                                completeWithFailure()\n                                return\n                            }\n                            completeWithFailure()\n                        }\n                    }\n                }\n            }\n        }\n\n        var downloadTask: DownloadTask.WrappedTask?\n        ImagePrefetcher.requestingQueue.sync {\n            let context = RetrievingContext(\n                options: optionsInfo, originalSource: source\n            )\n            downloadTask = manager.loadAndCacheImage(\n                source: source,\n                context: context,\n                completionHandler: downloadTaskCompletionHandler)\n        }\n\n        if let downloadTask = downloadTask {\n            tasks[source.cacheKey] = downloadTask\n        }\n    }\n    \n    private func append(cached source: Source) {\n        skippedSources.append(source)\n \n        reportProgress()\n        reportCompletionOrStartNext()\n    }\n    \n    private func startPrefetching(_ source: Source)\n    {\n        if optionsInfo.forceRefresh {\n            downloadAndCache(source)\n            return\n        }\n        \n        let cacheType = manager.cache.imageCachedType(\n            forKey: source.cacheKey,\n            processorIdentifier: optionsInfo.processor.identifier\n        )\n        switch cacheType {\n        case .memory:\n            append(cached: source)\n        case .disk:\n            if optionsInfo.alsoPrefetchToMemory {\n                let context = RetrievingContext(options: optionsInfo, originalSource: source)\n                _ = manager.retrieveImageFromCache(\n                    source: source,\n                    context: context,\n                    downloadTaskUpdated: nil)\n                {\n                    _ in\n                    self.append(cached: source)\n                }\n            } else {\n                append(cached: source)\n            }\n        case .none:\n            downloadAndCache(source)\n        }\n    }\n    \n    private func reportProgress() {\n\n        if progressBlock == nil && progressSourceBlock == nil {\n            return\n        }\n\n        let skipped = self.skippedSources\n        let failed = self.failedSources\n        let completed = self.completedSources\n        CallbackQueue.mainCurrentOrAsync.execute {\n            self.progressSourceBlock?(skipped, failed, completed)\n            self.progressBlock?(\n                skipped.compactMap { $0.asResource },\n                failed.compactMap { $0.asResource },\n                completed.compactMap { $0.asResource }\n            )\n        }\n    }\n    \n    private func reportCompletionOrStartNext() {\n        if let resource = self.pendingSources.popFirst() {\n            // Loose call stack for huge amount of sources.\n            prefetchQueue.async { self.startPrefetching(resource) }\n        } else {\n            guard allFinished else { return }\n            self.handleComplete()\n        }\n    }\n\n    var allFinished: Bool {\n        return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count\n    }\n    \n    private func handleComplete() {\n\n        if completionHandler == nil && completionSourceHandler == nil {\n            return\n        }\n\n        // Snapshot arrays/handlers before switching threads to avoid concurrent mutation crashes.\n        let skipped = self.skippedSources\n        let failed = self.failedSources\n        let completed = self.completedSources\n        let completionSourceHandler = self.completionSourceHandler\n        let completionHandler = self.completionHandler\n\n        // The completion handler should be called on the main thread\n        CallbackQueue.mainCurrentOrAsync.execute {\n            completionSourceHandler?(skipped, failed, completed)\n            completionHandler?(\n                skipped.compactMap { $0.asResource },\n                failed.compactMap { $0.asResource },\n                completed.compactMap { $0.asResource }\n            )\n            self.completionHandler = nil\n            self.progressBlock = nil\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/NetworkMetrics.swift",
    "content": "//\n//  NetworkMetrics.swift\n//  Kingfisher\n//\n//  Created by FunnyValentine on 2025/07/25.\n//\n//  Copyright (c) 2025 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Represents the network performance metrics collected during an image download task.\npublic struct NetworkMetrics: Sendable {\n\n    /// The original URLSessionTaskMetrics for advanced use cases.\n    public let rawMetrics: URLSessionTaskMetrics\n\n    /// The duration of the actual image retrieval (excluding redirects).\n    public let retrieveImageDuration: TimeInterval?\n\n    /// The total time from request start to completion (including redirects).\n    public let totalRequestDuration: TimeInterval\n\n    /// The time it took to perform DNS lookup.\n    public let domainLookupDuration: TimeInterval?\n    \n    /// The time it took to establish the TCP connection.\n    public let connectDuration: TimeInterval?\n    \n    /// The time it took to perform TLS handshake.\n    public let secureConnectionDuration: TimeInterval?\n    \n    /// The number of bytes sent in the request body.\n    public let requestBodyBytesSent: Int64\n    \n    /// The number of bytes received in the response body.\n    public let responseBodyBytesReceived: Int64\n    \n    /// The HTTP response status code, if available.\n    public let httpStatusCode: Int?\n    \n    /// The number of redirects that occurred during the request.\n    public let redirectCount: Int\n    \n    /// Creates a NetworkMetrics instance from URLSessionTaskMetrics\n    init?(from urlMetrics: URLSessionTaskMetrics) {\n        // Find the first successful transaction (200-299 status) ignoring redirects\n        // We need to ensure we get metrics from an actual successful download, not from \n        // intermediate redirects (301/302) which don't represent real download performance\n        var successfulTransaction: URLSessionTaskTransactionMetrics?\n        for transaction in urlMetrics.transactionMetrics {\n            if let httpResponse = transaction.response as? HTTPURLResponse,\n               (200...299).contains(httpResponse.statusCode) {\n                successfulTransaction = transaction\n                break\n            }\n        }\n\n        // make sure we have a valid successful transaction\n        guard let successfulTransaction else {\n            return nil\n        }\n\n        // Store raw metrics for advanced use cases\n        self.rawMetrics = urlMetrics\n        \n        // Calculate the image retrieval duration from the successful transaction\n        self.retrieveImageDuration = Self.calculateRetrieveImageDuration(from: successfulTransaction)\n        \n        // Calculate the total request duration from the task interval\n        self.totalRequestDuration = urlMetrics.taskInterval.duration\n        \n        // Calculate timing metrics from the successful transaction\n        self.domainLookupDuration = Self.calculateDomainLookupDuration(from: successfulTransaction)\n        self.connectDuration = Self.calculateConnectDuration(from: successfulTransaction)\n        self.secureConnectionDuration = Self.calculateSecureConnectionDuration(from: successfulTransaction)\n        \n        // Extract data transfer information from the successful transaction\n        self.requestBodyBytesSent = successfulTransaction.countOfRequestBodyBytesSent\n        self.responseBodyBytesReceived = successfulTransaction.countOfResponseBodyBytesReceived\n        \n        // Extract HTTP status code from the successful transaction\n        self.httpStatusCode = Self.extractHTTPStatusCode(from: successfulTransaction)\n        \n        // Extract redirect count\n        self.redirectCount = urlMetrics.redirectCount\n    }\n    \n    // MARK: - Private Calculation Methods\n    \n    /// Calculates DNS lookup duration\n    /// Formula: domainLookupEndDate - domainLookupStartDate\n    /// Represents: Time spent resolving domain name to IP address\n    private static func calculateDomainLookupDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {\n        guard let start = transaction.domainLookupStartDate,\n              let end = transaction.domainLookupEndDate else { return nil }\n        return end.timeIntervalSince(start)\n    }\n    \n    /// Calculates TCP connection establishment duration\n    /// Formula: connectEndDate - connectStartDate\n    /// Represents: Time spent establishing TCP connection to server\n    private static func calculateConnectDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {\n        guard let start = transaction.connectStartDate,\n              let end = transaction.connectEndDate else { return nil }\n        return end.timeIntervalSince(start)\n    }\n    \n    /// Calculates TLS/SSL handshake duration\n    /// Formula: secureConnectionEndDate - secureConnectionStartDate  \n    /// Represents: Time spent performing TLS/SSL handshake for HTTPS connections\n    private static func calculateSecureConnectionDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {\n        guard let start = transaction.secureConnectionStartDate,\n              let end = transaction.secureConnectionEndDate else { return nil }\n        return end.timeIntervalSince(start)\n    }\n    \n    /// Calculates the image retrieval duration for a single transaction \n    /// Formula: responseEndDate - requestStartDate\n    /// Represents: Time from sending HTTP request to receiving complete image response\n    private static func calculateRetrieveImageDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {\n        guard let start = transaction.requestStartDate,\n              let end = transaction.responseEndDate else { \n            return nil \n        }\n        return end.timeIntervalSince(start)\n    }\n    \n    /// Extracts HTTP status code from response\n    /// Returns: HTTP status code (200, 404, etc.) or nil for non-HTTP responses\n    private static func extractHTTPStatusCode(from transaction: URLSessionTaskTransactionMetrics) -> Int? {\n        return (transaction.response as? HTTPURLResponse)?.statusCode\n    }\n}\n\n// MARK: - Convenience Properties\n\nextension NetworkMetrics {\n    \n    /// The download speed in bytes per second.\n    ///\n    /// Calculated as `responseBodyBytesReceived / retrieveImageDuration`. \n    /// Returns `nil` if the duration is unavailable or zero, or if no data was received.\n    ///\n    /// - Note: This uses the actual image retrieval duration, excluding redirects and other overhead,\n    ///   to provide the most accurate representation of the data transfer rate.\n    public var downloadSpeed: Double? {\n        guard responseBodyBytesReceived > 0,\n              let duration = retrieveImageDuration,\n              duration > 0 else { return nil }\n        \n        return Double(responseBodyBytesReceived) / duration\n    }\n    \n    /// The download speed in megabytes per second (MB/s).\n    ///\n    /// This is a convenience property that converts `downloadSpeed` from bytes per second \n    /// to megabytes per second for easier readability.\n    ///\n    /// - Returns: Download speed in MB/s, or `nil` if `downloadSpeed` is unavailable.\n    public var downloadSpeedMBps: Double? {\n        guard let speed = downloadSpeed else { return nil }\n        return speed / (1024 * 1024)\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/NetworkMonitor.swift",
    "content": "//\n//  NetworkMonitor.swift\n//  Kingfisher\n//\n//  Created by Vladislav Komkov on 2025/09/22.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Network\nimport Foundation\n\n/// A protocol for network connectivity monitoring that allows for dependency injection and testing.\ninternal protocol NetworkMonitoring: Sendable {\n    /// Whether the network is currently connected.\n    var isConnected: Bool { get }\n\n    /// Observes network connectivity changes with an optional timeout.\n    /// - Parameters:\n    ///   - timeoutInterval: The timeout for waiting for network reconnection. If nil, no timeout is applied.\n    ///   - callback: The callback to be called when network state changes or timeout occurs.\n    /// - Returns: A cancellable observer that can be used to cancel the observation.\n    func observeConnectivity(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void) -> NetworkObserver\n}\n\n/// A protocol for network observers that can be cancelled.\ninternal protocol NetworkObserver: Sendable {\n    /// Cancels the network observation.\n    func cancel()\n}\n\n/// A shared singleton that manages network connectivity monitoring.\n/// This prevents creating multiple NWPathMonitor instances when many NetworkRetryStrategy instances are used.\n/// The monitor is created lazily only when first accessed.\ninternal final class NetworkMonitor: @unchecked Sendable, NetworkMonitoring {\n    static let `default` = NetworkMonitor()\n\n    /// Whether the network is currently connected.\n    var isConnected: Bool {\n        return monitor.currentPath.status == .satisfied\n    }\n\n    /// The network path monitor for observing connectivity changes.\n    private let monitor = NWPathMonitor()\n\n    /// The queue for monitoring network changes.\n    private let monitorQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.NetworkMonitor\", qos: .utility)\n\n    /// Observers waiting for network reconnection.\n    private var observers: [NetworkObserverImpl] = []\n    private let observersQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.NetworkMonitor.Observers\", attributes: .concurrent)\n\n    /// Whether the monitor has been started.\n    private var isStarted = false\n    private let startQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.NetworkMonitor.Start\")\n\n    private init() {\n        // Set up path monitoring\n        monitor.pathUpdateHandler = { [weak self] path in\n            self?.handlePathUpdate(path)\n        }\n    }\n\n    /// Starts monitoring if not already started.\n    private func startMonitoring() {\n        startQueue.sync {\n            guard !isStarted else { return }\n            monitor.start(queue: monitorQueue)\n            isStarted = true\n        }\n    }\n\n    /// Handles network path updates and notifies observers.\n    private func handlePathUpdate(_ path: NWPath) {\n        let connected = path.status == .satisfied\n        guard connected else { return }\n\n        // Notify all observers that network is available\n        observersQueue.async(flags: .barrier) {\n            let activeObservers = self.observers\n            self.observers.removeAll()\n\n            DispatchQueue.main.async {\n                activeObservers.forEach { $0.notify(isConnected: true) }\n            }\n        }\n    }\n\n    /// Adds an observer for network reconnection.\n    private func addObserver(_ observer: NetworkObserverImpl) {\n        startMonitoring()\n\n        observersQueue.async(flags: .barrier) {\n            self.observers.append(observer)\n        }\n    }\n\n    /// Removes an observer.\n    internal func removeObserver(_ observer: NetworkObserverImpl) {\n        observersQueue.async(flags: .barrier) {\n            self.observers.removeAll { $0 === observer }\n        }\n    }\n\n    // MARK: - NetworkMonitoring\n\n    public func observeConnectivity(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void) -> NetworkObserver {\n        let observer = NetworkObserverImpl(\n            timeoutInterval: timeoutInterval,\n            callback: callback,\n            monitor: self\n        )\n        addObserver(observer)\n        return observer\n    }\n}\n\n/// Internal implementation of network observer that manages timeout and callbacks.\ninternal final class NetworkObserverImpl: @unchecked Sendable, NetworkObserver {\n    let timeoutInterval: TimeInterval?\n    let callback: @Sendable (Bool) -> Void\n    private weak var monitor: NetworkMonitor?\n    private var timeoutWorkItem: DispatchWorkItem?\n    private let queue = DispatchQueue(label: \"com.onevcat.Kingfisher.NetworkObserver\", qos: .utility)\n\n    init(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void, monitor: NetworkMonitor) {\n        self.timeoutInterval = timeoutInterval\n        self.callback = callback\n        self.monitor = monitor\n\n        // Set up timeout if specified\n        if let timeoutInterval = timeoutInterval {\n            let workItem = DispatchWorkItem { [weak self] in\n                self?.notify(isConnected: false)\n            }\n            timeoutWorkItem = workItem\n            queue.asyncAfter(deadline: .now() + timeoutInterval, execute: workItem)\n        }\n    }\n\n    func notify(isConnected: Bool) {\n        queue.async { [weak self] in\n            guard let self else { return }\n\n            // Cancel timeout if we're notifying\n            timeoutWorkItem?.cancel()\n            timeoutWorkItem = nil\n\n            // Remove from monitor\n            monitor?.removeObserver(self)\n\n            // Call the callback\n            DispatchQueue.main.async {\n                self.callback(isConnected)\n            }\n        }\n    }\n\n    func cancel() {\n        queue.async { [weak self] in\n            guard let self else { return }\n\n            // Cancel timeout\n            timeoutWorkItem?.cancel()\n            timeoutWorkItem = nil\n\n            // Remove from monitor\n            monitor?.removeObserver(self)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/RedirectHandler.swift",
    "content": "//\n//  RedirectHandler.swift\n//  Kingfisher\n//\n//  Created by Roman Maidanovych on 2018/12/10.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// The ``ImageDownloadRedirectHandler`` is used to modify the request before redirection.\n///\n/// This allows you to customize the image download request during redirection. You can make modifications for\n/// purposes such as adding an authentication token to the header, performing basic HTTP authentication, or URL\n/// mapping.\n///\n/// Typically, you pass an ``ImageDownloadRedirectHandler`` as the associated value of\n/// ``KingfisherOptionsInfoItem/redirectHandler(_:)`` and use it as the `options` parameter in relevant methods.\n///\n/// If you do not make any changes to the input `request` and return it as is, the downloading process will redirect\n/// using it.\n///\npublic protocol ImageDownloadRedirectHandler: Sendable {\n\n    /// Called when a redirect is received and the downloader waiting for the request to continue the download task.\n    ///\n    /// - Parameters:\n    ///   - task: The current ``SessionDataTask`` that triggers this redirect.\n    ///   - response: The response received during redirection.\n    ///   - newRequest: The new request received from the URL session for redirection that can be modified.\n    /// - Returns: The modified request.\n    func handleHTTPRedirection(\n        for task: SessionDataTask,\n        response: HTTPURLResponse,\n        newRequest: URLRequest\n    ) async -> URLRequest?\n}\n\n/// A wrapper for creating an ``ImageDownloadRedirectHandler`` instance more easily.\n///\n/// This type conforms to ``ImageDownloadRedirectHandler`` and wraps an image modification block.\npublic struct AnyRedirectHandler: ImageDownloadRedirectHandler {\n    \n    let block: @Sendable (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void\n    \n    public func handleHTTPRedirection(\n        for task: SessionDataTask, response: HTTPURLResponse, newRequest: URLRequest\n    ) async -> URLRequest? {\n        return await withCheckedContinuation { continuation in\n            block(task, response, newRequest, { urlRequest in\n                continuation.resume(returning: urlRequest)\n            })\n        }\n    }\n    \n    /// Creates a value of ``ImageDownloadRedirectHandler`` that executes the `modify` block.\n    ///\n    /// - Parameter handle: The block that modifies the request when a request modification task is triggered.\n    public init(handle: @escaping @Sendable (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void) {\n        block = handle\n    }\n    \n}\n"
  },
  {
    "path": "Sources/Networking/RequestModifier.swift",
    "content": "//\n//  RequestModifier.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2016/09/05.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Represents and wraps a method for modifying a request before an image download request starts asynchronously.\n///\n/// Usually, you pass an ``AsyncImageDownloadRequestModifier`` instance as the associated value of\n/// ``KingfisherOptionsInfoItem/requestModifier(_:)`` and use it as the `options` parameter in related methods.\n///\n/// For example, the code below defines a modifier to add a header field and its value to the request.\n///\n/// ```swift\n/// class HeaderFieldModifier: AsyncImageDownloadRequestModifier {\n///   var onDownloadTaskStarted: ((DownloadTask?) -> Void)? = nil\n///   func modified(for request: URLRequest) async -> URLRequest? {\n///     var r = request\n///     let token = await service.fetchToken()\n///     r.setValue(token, forHTTPHeaderField: \"token\")\n///     return r\n///   }\n/// }\n///\n/// imageView.kf.setImage(with: url, options: [.requestModifier(HeaderFieldModifier())])\n/// ```\npublic protocol AsyncImageDownloadRequestModifier: Sendable {\n\n    /// This method will be called just before the `request` is sent.\n    /// \n    /// This is the last chance to modify the image download request. You can modify the request for some customizing\n    /// purposes, such as adding an auth token to the header, performing basic HTTP auth, or something like URL mapping.\n    ///  \n    /// After making the modification, you should return the modified request, and the data will be downloaded with\n    /// this modified request.\n    ///\n    /// > If you do nothing with the input `request` and return it as-is, the download process will start with it as the\n    /// modifier doesn't exist. If you return `nil`, the downloading will be interrupted with an\n    ///  ``KingfisherError/RequestErrorReason/emptyRequest`` error.\n    ///\n    /// - Parameter request: The input request contains necessary information like `url`. This request is generated\n    /// according to your resource URL as a GET request.\n    /// - Returns: The modified request which should be used to trigger the download.\n    func modified(for request: URLRequest) async -> URLRequest?\n\n    /// A block that will be called when the download task starts.\n    ///\n    /// If an ``AsyncImageDownloadRequestModifier`` and asynchronous modification occur before the download, the\n    /// related download method will not return a valid ``DownloadTask`` value. Instead, you can get one from this\n    /// method.\n    ///\n    /// User the ``DownloadTask`` value to track the task, or cancel it when you need to.\n    var onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)? { get }\n}\n\n/// Represents and wraps a method for modifying a request before an image download request starts synchronously.\n///\n/// Usually, you pass an ``ImageDownloadRequestModifier`` instance as the associated value of\n/// ``KingfisherOptionsInfoItem/requestModifier(_:)`` and use it as the `options` parameter in related methods.\n///\n/// For example, the code below defines a modifier to add a header field and its value to the request.\n///\n/// ```swift\n/// class HeaderFieldModifier: AsyncImageDownloadRequestModifier {\n///   func modified(for request: URLRequest) -> URLRequest? {\n///     var r = request\n///     r.setValue(\"value\", forHTTPHeaderField: \"key\")\n///     return r\n///   }\n/// }\n///\n/// imageView.kf.setImage(with: url, options: [.requestModifier(HeaderFieldModifier())])\n/// ```\npublic protocol ImageDownloadRequestModifier: AsyncImageDownloadRequestModifier {\n\n    /// This method will be called just before the `request` is sent.\n    ///\n    /// This is the last chance to modify the image download request. You can modify the request for some customizing\n    /// purposes, such as adding an auth token to the header, performing basic HTTP auth, or something like URL mapping.\n    ///\n    /// After making the modification, you should return the modified request, and the data will be downloaded with\n    /// this modified request.\n    ///\n    /// > If you do nothing with the input `request` and return it as-is, the download process will start with it as the\n    /// modifier doesn't exist. If you return `nil`, the downloading will be interrupted with an\n    ///  ``KingfisherError/RequestErrorReason/emptyRequest`` error.\n    ///\n    /// > Tip: If you are trying to execute an async operation during the modify, choose to conform the\n    ///  ``AsyncImageDownloadRequestModifier`` instead.\n    ///\n    /// - Parameter request: The input request contains necessary information like `url`. This request is generated\n    /// according to your resource URL as a GET request.\n    /// - Returns: The modified request which should be used to trigger the download.\n    func modified(for request: URLRequest) -> URLRequest?\n}\n\nextension ImageDownloadRequestModifier {\n    /// This is `nil` for a sync `ImageDownloadRequestModifier` by default. You can get the `DownloadTask` from the\n    /// return value of downloader method.\n    public var onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)? { return nil }\n}\n\n/// A wrapper for creating an ``ImageDownloadRequestModifier`` instance more easily.\n///\n/// This type conforms to ``ImageDownloadRequestModifier`` and wraps an image modification block.\npublic struct AnyModifier: ImageDownloadRequestModifier {\n    \n    let block: @Sendable (URLRequest) -> URLRequest?\n\n    public func modified(for request: URLRequest) -> URLRequest? {\n        return block(request)\n    }\n    \n    /// Creates a value of ``ImageDownloadRequestModifier`` that runs the `modify` block.\n    ///\n    /// - Parameter modify: The request modifying block runs when a request modifying task comes.\n    public init(modify: @escaping @Sendable (URLRequest) -> URLRequest?) {\n        block = modify\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/RetryStrategy.swift",
    "content": "//\n//  RetryStrategy.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/05/04.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Represents a retry context that could be used to determine the current retry status.\n///\n/// The instance of this type can be shared between different retry attempts.\npublic class RetryContext: @unchecked Sendable {\n\n    private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.RetryContextPropertyQueue\")\n\n    /// The source from which the target image should be retrieved.\n    public let source: Source\n\n    /// The source from which the target image should be retrieved.\n    public let error: KingfisherError\n\n    private var _retriedCount: Int\n\n    /// The number of retries attempted before the current retry happens.\n    ///\n    /// This value is `0` if the current retry is for the first time.\n    public var retriedCount: Int {\n        get { propertyQueue.sync { _retriedCount } }\n        set { propertyQueue.sync { _retriedCount = newValue } }\n    }\n\n    private var _userInfo: Any? = nil\n\n    /// A user-set value for passing any other information during the retry.\n    ///\n    /// If you choose to use ``RetryDecision/retry(userInfo:)`` as the retry decision for\n    /// ``RetryStrategy/retry(context:retryHandler:)``, the associated value of ``RetryDecision/retry(userInfo:)`` will\n    /// be delivered to you in the next retry.\n    public internal(set) var userInfo: Any? {\n        get { propertyQueue.sync { _userInfo } }\n        set { propertyQueue.sync { _userInfo = newValue } }\n    }\n\n    init(source: Source, error: KingfisherError) {\n        self.source = source\n        self.error = error\n        _retriedCount = 0\n    }\n\n    @discardableResult\n    func increaseRetryCount() -> RetryContext {\n        retriedCount += 1\n        return self\n    }\n}\n\n/// Represents the decision on the behavior for the current retry.\npublic enum RetryDecision {\n    /// A retry should happen. The associated `userInfo` value will be passed to the next retry in the\n    /// ``RetryContext`` parameter.\n    case retry(userInfo: Any?)\n    /// There should be no more retry attempts. The image retrieving process will fail with an error.\n    case stop\n}\n\n/// Defines a retry strategy that can be applied to the ``KingfisherOptionsInfoItem/retryStrategy(_:)`` option.\npublic protocol RetryStrategy: Sendable {\n\n    /// Kingfisher calls this method if an error occurs during the image retrieving process from ``KingfisherManager``.\n    ///\n    /// You implement this method to provide the necessary logic based on the `context` parameter. Then you need to call\n    /// `retryHandler` to pass the retry decision back to Kingfisher.\n    ///\n    /// - Parameters:\n    ///   - context: The retry context containing information of the current retry attempt.\n    ///   - retryHandler: A block you need to call with a decision on whether the retry should happen or not.\n    func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void)\n}\n\n/// A retry strategy that guides Kingfisher to perform retry operation with some delay.\n///\n/// When an error of ``KingfisherError/ResponseErrorReason`` happens, Kingfisher uses the retry strategy in its option\n/// to retry. This strategy defines a specified maximum retry count and a certain interval mechanism.\npublic struct DelayRetryStrategy: RetryStrategy {\n\n    /// Represents the interval mechanism used in a ``DelayRetryStrategy``.\n    public enum Interval : Sendable{\n\n        /// The next retry attempt should happen in a fixed number of seconds.\n        ///\n        /// For example, if the associated value is 3, the attempt happens 3 seconds after the previous decision is\n        /// made.\n        case seconds(TimeInterval)\n\n        /// The next retry attempt should happen in an accumulated duration.\n        ///\n        /// For example, if the associated value is 3, the attempts happen with intervals of 3, 6, 9, 12, ... seconds.\n        case accumulated(TimeInterval)\n\n        /// Uses a block to determine the next interval.\n        ///\n        /// The current retry count is given as a parameter.\n        case custom(block: @Sendable (_ retriedCount: Int) -> TimeInterval)\n\n        func timeInterval(for retriedCount: Int) -> TimeInterval {\n            let retryAfter: TimeInterval\n            switch self {\n            case .seconds(let interval):\n                retryAfter = interval\n            case .accumulated(let interval):\n                retryAfter = Double(retriedCount + 1) * interval\n            case .custom(let block):\n                retryAfter = block(retriedCount)\n            }\n            return retryAfter\n        }\n    }\n\n    /// The maximum number of retries allowed by the retry strategy.\n    public let maxRetryCount: Int\n\n    /// The interval between retry attempts in the retry strategy.\n    public let retryInterval: Interval\n\n    /// Creates a delayed retry strategy.\n    ///\n    /// - Parameters:\n    ///   - maxRetryCount: The maximum number of retries allowed.\n    ///   - retryInterval: The mechanism defining the interval between retry attempts.\n    ///\n    /// By default, ``Interval/seconds(_:)`` with an associated value `3` is used to establish a constant retry\n    /// interval.\n    public init(maxRetryCount: Int, retryInterval: Interval = .seconds(3)) {\n        self.maxRetryCount = maxRetryCount\n        self.retryInterval = retryInterval\n    }\n\n    public func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void) {\n        // Retry count exceeded.\n        guard context.retriedCount < maxRetryCount else {\n            retryHandler(.stop)\n            return\n        }\n\n        // User cancel the task. No retry.\n        guard !context.error.isTaskCancelled else {\n            retryHandler(.stop)\n            return\n        }\n\n        // Only retry for a response error.\n        guard case KingfisherError.responseError = context.error else {\n            retryHandler(.stop)\n            return\n        }\n\n        let interval = retryInterval.timeInterval(for: context.retriedCount)\n        if interval == 0 {\n            retryHandler(.retry(userInfo: nil))\n        } else {\n            DispatchQueue.main.asyncAfter(deadline: .now() + interval) {\n                retryHandler(.retry(userInfo: nil))\n            }\n        }\n    }\n}\n\n/// A retry strategy that observes network state and retries on reconnect.\n///\n/// This strategy only retries when network becomes available after a disconnection.\n/// It does not use any delay mechanisms - it retries immediately when network is restored.\n///\n/// The network monitor is created lazily only when this strategy is first used,\n/// ensuring no unnecessary resource usage when the strategy is not in use.\npublic struct NetworkRetryStrategy: RetryStrategy {\n\n    /// The timeout for waiting for network reconnection (in seconds).\n    private let timeoutInterval: TimeInterval?\n\n    /// The network monitoring service used to observe connectivity changes.\n    private let networkMonitor: NetworkMonitoring\n\n    /// Creates a network-aware retry strategy.\n    ///\n    /// - Parameters:\n    ///   - timeoutInterval: The timeout for waiting for network reconnection. If nil, no timeout is applied. Defaults to 30 seconds.\n    public init(timeoutInterval: TimeInterval? = 30) {\n        self.init(\n            timeoutInterval: timeoutInterval,\n            networkMonitor: NetworkMonitor.default\n        )\n    }\n\n    internal init(\n        timeoutInterval: TimeInterval?,\n        networkMonitor: NetworkMonitoring\n    ) {\n        self.timeoutInterval = timeoutInterval\n        self.networkMonitor = networkMonitor\n    }\n\n    public func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void) {\n        // Dispose of any previous disposable from userInfo\n        if let previousObserver = context.userInfo as? NetworkObserver {\n            previousObserver.cancel()\n        }\n\n        // User cancel the task. No retry.\n        guard !context.error.isTaskCancelled else {\n            retryHandler(.stop)\n            return\n        }\n\n        // Only retry for a response error.\n        guard case KingfisherError.responseError = context.error else {\n            retryHandler(.stop)\n            return\n        }\n\n        // Check if we have network connectivity\n        if networkMonitor.isConnected {\n            // Network is available, retry immediately\n            retryHandler(.retry(userInfo: nil))\n        } else {\n            // Network is not available, wait for reconnection\n            waitForReconnection(context: context, retryHandler: retryHandler)\n        }\n    }\n\n    // MARK: - Private helpers\n\n    private func waitForReconnection(\n        context: RetryContext,\n        retryHandler: @escaping @Sendable (RetryDecision) -> Void\n    ) {\n        let observer = networkMonitor.observeConnectivity(timeoutInterval: timeoutInterval) { [weak context] isConnected in\n            if isConnected {\n                // Connection is restored, retry immediately\n                retryHandler(.retry(userInfo: context?.userInfo))\n            } else {\n                // Timeout reached or cancelled\n                retryHandler(.stop)\n            }\n        }\n\n        // Store the observer in userInfo so it can be cancelled if needed\n        context.userInfo = observer\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/SessionDataTask.swift",
    "content": "//\n//  SessionDataTask.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/11/1.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Represents a session data task in ``ImageDownloader``.\n///\n/// Essentially, a ``SessionDataTask`` wraps a `URLSessionDataTask` and manages the download data.\n/// It uses a ``SessionDataTask/CancelToken`` to track the task and manage its cancellation.\npublic class SessionDataTask: @unchecked Sendable {\n\n    /// Represents the type of token used for canceling a task.\n    public typealias CancelToken = Int\n\n    struct TaskCallback {\n        let onCompleted: Delegate<Result<ImageLoadingResult, KingfisherError>, Void>?\n        let options: KingfisherParsedOptionsInfo\n    }\n\n    private var _mutableData: Data\n    /// The downloaded raw data of the current task.\n    public var mutableData: Data {\n        lock.lock()\n        defer { lock.unlock() }\n        return _mutableData\n    }\n\n    // This is a copy of `task.originalRequest?.url`. It is for obtaining race-safe behavior for a pitfall on iOS 13.\n    // Ref: https://github.com/onevcat/Kingfisher/issues/1511\n    public let originalURL: URL?\n\n    /// The underlying download task. \n    ///\n    /// It is only for debugging purposes when you encounter an error. You should not modify the content of this task\n    /// or start it yourself.\n    public let task: URLSessionDataTask\n    \n    private var callbacksStore = [CancelToken: TaskCallback]()\n\n    var callbacks: [SessionDataTask.TaskCallback] {\n        lock.lock()\n        defer { lock.unlock() }\n        return Array(callbacksStore.values)\n    }\n\n    private var currentToken = 0\n    private let lock = NSLock()\n    \n    private var _metrics: NetworkMetrics?\n    /// The network metrics collected during the download task.\n    public var metrics: NetworkMetrics? {\n        lock.lock()\n        defer { lock.unlock() }\n        return _metrics\n    }\n\n    let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>()\n    let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>()\n\n    var started = false\n    var containsCallbacks: Bool {\n        // We should be able to use `task.state != .running` to check it.\n        // However, in some rare cases, cancelling the task does not change\n        // task state to `.cancelling` immediately, but still in `.running`.\n        // So we need to check callbacks count to for sure that it is safe to remove the\n        // task in delegate.\n        return !callbacks.isEmpty\n    }\n\n    init(task: URLSessionDataTask) {\n        self.task = task\n        self.originalURL = task.originalRequest?.url\n        _mutableData = Data()\n    }\n\n    func addCallback(_ callback: TaskCallback) -> CancelToken {\n        lock.lock()\n        defer { lock.unlock() }\n        callbacksStore[currentToken] = callback\n        defer { currentToken += 1 }\n        return currentToken\n    }\n\n    func removeCallback(_ token: CancelToken) -> TaskCallback? {\n        lock.lock()\n        defer { lock.unlock() }\n        if let callback = callbacksStore[token] {\n            callbacksStore[token] = nil\n            return callback\n        }\n        return nil\n    }\n    \n    @discardableResult\n    func removeAllCallbacks() -> [TaskCallback] {\n        lock.lock()\n        defer { lock.unlock() }\n        let callbacks = callbacksStore.values\n        callbacksStore.removeAll()\n        return Array(callbacks)\n    }\n\n    func resume() {\n        guard !started else { return }\n        started = true\n        task.resume()\n    }\n\n    func cancel(token: CancelToken) {\n        guard let callback = removeCallback(token) else {\n            return\n        }\n        onCallbackCancelled.call((token, callback))\n    }\n\n    func forceCancel() {\n        for token in callbacksStore.keys {\n            cancel(token: token)\n        }\n    }\n\n    func didReceiveData(_ data: Data) {\n        lock.lock()\n        defer { lock.unlock() }\n        _mutableData.append(data)\n    }\n    \n    func didCollectMetrics(_ metrics: NetworkMetrics) {\n        lock.lock()\n        defer { lock.unlock() }\n        _metrics = metrics\n    }\n}\n"
  },
  {
    "path": "Sources/Networking/SessionDelegate.swift",
    "content": "//\n//  SessionDelegate.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/11/1.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// Represents the delegate object of the downloader session.\n///\n/// It also behaves like a task manager for downloading.\n@objc(KFSessionDelegate) // Fix for ObjC header name conflicting. https://github.com/onevcat/Kingfisher/issues/1530\nopen class SessionDelegate: NSObject, @unchecked Sendable {\n\n    typealias SessionChallengeFunc = (\n        URLSession,\n        URLAuthenticationChallenge\n    )\n\n    typealias SessionTaskChallengeFunc = (\n        URLSession,\n        URLSessionTask,\n        URLAuthenticationChallenge\n    )\n\n    private var tasks: [URL: SessionDataTask] = [:]\n    private let lock = NSLock()\n\n    let onValidStatusCode = Delegate<Int, Bool>()\n    let onResponseReceived = Delegate<URLResponse, URLSession.ResponseDisposition>()\n    let onDownloadingFinished = Delegate<(URL, Result<URLResponse, KingfisherError>), Void>()\n    let onDidDownloadData = Delegate<SessionDataTask, Data?>()\n\n    let onReceiveSessionChallenge = Delegate<SessionChallengeFunc, (URLSession.AuthChallengeDisposition, URLCredential?)>()\n    let onReceiveSessionTaskChallenge = Delegate<SessionTaskChallengeFunc, (URLSession.AuthChallengeDisposition, URLCredential?)>()\n\n    func add(\n        _ dataTask: URLSessionDataTask,\n        url: URL,\n        callback: SessionDataTask.TaskCallback) -> DownloadTask\n    {\n        lock.lock()\n        defer { lock.unlock() }\n\n        // Create a new task if necessary.\n        let task = SessionDataTask(task: dataTask)\n        task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in\n            guard let task = task else { return }\n\n            let (token, callback) = value\n\n            let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token))\n            task.onTaskDone.call((.failure(error), [callback]))\n            // No other callbacks waiting, we can clear the task now.\n            if !task.containsCallbacks {\n                let dataTask = task.task\n\n                self.cancelTask(dataTask)\n                self.remove(task)\n            }\n        }\n        let token = task.addCallback(callback)\n        tasks[url] = task\n        return DownloadTask(sessionTask: task, cancelToken: token)\n    }\n\n    private func cancelTask(_ dataTask: URLSessionDataTask) {\n        lock.lock()\n        defer { lock.unlock() }\n        dataTask.cancel()\n    }\n\n    func append(\n        _ task: SessionDataTask,\n        callback: SessionDataTask.TaskCallback) -> DownloadTask\n    {\n        let token = task.addCallback(callback)\n        return DownloadTask(sessionTask: task, cancelToken: token)\n    }\n\n    private func remove(_ task: SessionDataTask) {\n        lock.lock()\n        defer { lock.unlock() }\n\n        guard let url = task.originalURL else {\n            return\n        }\n        task.removeAllCallbacks()\n        tasks[url] = nil\n    }\n\n    private func task(for task: URLSessionTask) -> SessionDataTask? {\n        lock.lock()\n        defer { lock.unlock() }\n\n        guard let url = task.originalRequest?.url else {\n            return nil\n        }\n        guard let sessionTask = tasks[url] else {\n            return nil\n        }\n        guard sessionTask.task.taskIdentifier == task.taskIdentifier else {\n            return nil\n        }\n        return sessionTask\n    }\n\n    func task(for url: URL) -> SessionDataTask? {\n        lock.lock()\n        defer { lock.unlock() }\n        return tasks[url]\n    }\n\n    func cancelAll() {\n        lock.lock()\n        let taskValues = tasks.values\n        lock.unlock()\n        for task in taskValues {\n            task.forceCancel()\n        }\n    }\n\n    func cancel(url: URL) {\n        lock.lock()\n        let task = tasks[url]\n        lock.unlock()\n        task?.forceCancel()\n    }\n}\n\nextension SessionDelegate: URLSessionDataDelegate {\n\n    open func urlSession(\n        _ session: URLSession,\n        dataTask: URLSessionDataTask,\n        didReceive response: URLResponse\n    ) async -> URLSession.ResponseDisposition {\n        guard let httpResponse = response as? HTTPURLResponse else {\n            let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response))\n            onCompleted(task: dataTask, result: .failure(error))\n            return .cancel\n        }\n        \n        let httpStatusCode = httpResponse.statusCode\n        guard onValidStatusCode.call(httpStatusCode) == true else {\n            let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse))\n            onCompleted(task: dataTask, result: .failure(error))\n            return .cancel\n        }\n        \n        guard let disposition = await onResponseReceived.callAsync(response) else {\n            return .cancel\n        }\n        \n        if disposition == .cancel {\n            let error = KingfisherError.responseError(reason: .cancelledByDelegate(response: response))\n            self.onCompleted(task: dataTask, result: .failure(error))\n        }\n        \n        return disposition\n    }\n\n    open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {\n        guard let task = self.task(for: dataTask) else {\n            return\n        }\n        \n        task.didReceiveData(data)\n        \n        task.callbacks.forEach { callback in\n            callback.options.onDataReceived?.forEach { sideEffect in\n                sideEffect.onDataReceived(session, task: task, data: data)\n            }\n        }\n    }\n\n    open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {\n        guard let sessionTask = self.task(for: task) else { return }\n\n        if let url = sessionTask.originalURL {\n            let result: Result<URLResponse, KingfisherError>\n            if let error = error {\n                result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))\n            } else if let response = task.response {\n                result = .success(response)\n            } else {\n                result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask)))\n            }\n            onDownloadingFinished.call((url, result))\n        }\n\n        let result: Result<(Data, URLResponse?), KingfisherError>\n        if let error = error {\n            result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))\n        } else {\n            if let data = onDidDownloadData.call(sessionTask) {\n                result = .success((data, task.response))\n            } else {\n                result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask)))\n            }\n        }\n        onCompleted(task: task, result: result)\n    }\n\n    open func urlSession(\n        _ session: URLSession,\n        didReceive challenge: URLAuthenticationChallenge\n    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?)\n    {\n        await onReceiveSessionChallenge.callAsync((session, challenge)) ?? (.performDefaultHandling, nil)\n    }\n    \n    open func urlSession(\n        _ session: URLSession,\n        task: URLSessionTask,\n        didReceive challenge: URLAuthenticationChallenge\n    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?)\n    {\n        await onReceiveSessionTaskChallenge.callAsync((session, task, challenge)) ?? (.performDefaultHandling, nil)\n    }\n    \n    \n    open func urlSession(\n        _ session: URLSession,\n        task: URLSessionTask,\n        willPerformHTTPRedirection response: HTTPURLResponse,\n        newRequest request: URLRequest\n    ) async -> URLRequest?\n    {\n        guard let sessionDataTask = self.task(for: task),\n              let redirectHandler = Array(sessionDataTask.callbacks).last?.options.redirectHandler else\n        {\n            return request\n        }\n        return await redirectHandler.handleHTTPRedirection(\n            for: sessionDataTask,\n            response: response,\n            newRequest: request\n        )\n    }\n    \n    open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {\n        guard let sessionTask = self.task(for: task) else { return }\n        \n        // Collect network metrics for the completed task\n        if let networkMetrics = NetworkMetrics(from: metrics) {\n            sessionTask.didCollectMetrics(networkMetrics)\n        }\n    }\n\n    private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?), KingfisherError>) {\n        guard let sessionTask = self.task(for: task) else {\n            return\n        }\n        let callbacks = sessionTask.removeAllCallbacks()\n        sessionTask.onTaskDone.call((result, callbacks))\n        remove(sessionTask)\n    }\n}\n"
  },
  {
    "path": "Sources/PrivacyInfo.xcprivacy",
    "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    <key>NSPrivacyAccessedAPITypes</key>\n    <array>\n        <dict>\n            <key>NSPrivacyAccessedAPIType</key>\n            <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>\n            <key>NSPrivacyAccessedAPITypeReasons</key>\n            <array>\n                <string>C617.1</string>\n            </array>\n        </dict>\n    </array>\n    <key>NSPrivacyTracking</key>\n    <false/>\n  <key>NSPrivacyTrackingDomains</key>\n  <array>\n  </array>\n  <key>NSPrivacyCollectedDataTypes</key>\n  <array>\n  </array>\n</dict>\n</plist>\n"
  },
  {
    "path": "Sources/SwiftUI/ImageBinder.swift",
    "content": "//\n//  ImageBinder.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2019/06/27.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(SwiftUI) && canImport(Combine)\nimport SwiftUI\nimport Combine\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension KFImage {\n\n    /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs\n    /// image downloading and progress reporting based on `KingfisherManager`.\n    @MainActor\n    class ImageBinder: ObservableObject {\n        \n        init() {}\n\n        var downloadTask: DownloadTask?\n        private var loading = false\n\n        var loadingOrSucceeded: Bool {\n            return loading || loadedImage != nil\n        }\n\n        // Do not use @Published due to https://github.com/onevcat/Kingfisher/issues/1717. Revert to @Published once\n        // we can drop iOS 12.\n        private(set) var loaded = false\n\n        private(set) var animating = false\n\n        var loadedImage: KFCrossPlatformImage? = nil { willSet { objectWillChange.send() } }\n        var failureView: (() -> AnyView)? = nil { willSet { objectWillChange.send() } }\n        var progress: Progress = .init()\n\n        func markLoading() {\n            loading = true\n        }\n\n        func markLoaded(sendChangeEvent: Bool) {\n            loaded = true\n            if sendChangeEvent {\n                objectWillChange.send()\n            }\n        }\n\n        func start<HoldingView: KFImageHoldingView>(context: Context<HoldingView>) where HoldingView: Sendable {\n            guard let source = context.source else {\n                CallbackQueueMain.currentOrAsync {\n                    context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource))\n                    if let view = context.failureView {\n                        self.failureView = view\n                    } else if let image = context.options.onFailureImage {\n                        self.loadedImage = image\n                    }\n                    self.loading = false\n                    self.markLoaded(sendChangeEvent: false)\n                }\n                return\n            }\n\n            loading = true\n            \n            progress = .init()\n            downloadTask = KingfisherManager.shared\n                .retrieveImage(\n                    with: source,\n                    options: context.options,\n                    progressBlock: { size, total in\n                        self.updateProgress(downloaded: size, total: total)\n                        context.onProgressDelegate.call((size, total))\n                    },\n                    progressiveImageSetter: { image in\n                        CallbackQueueMain.currentOrAsync {\n                            self.markLoaded(sendChangeEvent: true)\n                            self.loadedImage = image\n                        }\n                    },\n                    completionHandler: { [weak self] result in\n\n                        guard let self else { return }\n\n                        CallbackQueueMain.currentOrAsync {\n                            self.downloadTask = nil\n                            self.loading = false\n                        }\n                        \n                        switch result {\n                        case .success(let value):\n                            CallbackQueueMain.currentOrAsync {\n                                if context.swiftUITransition != nil,\n                                   context.shouldApplyFade(cacheType: value.cacheType) {\n                                    // Apply SwiftUI loadTransition with custom animation (higher priority than fade)\n                                    self.animating = true\n                                    let animation = context.swiftUIAnimation ?? .default\n                                    withAnimation(animation) {\n                                        self.markLoaded(sendChangeEvent: true)\n                                    }\n                                } else if let fadeDuration = context.fadeTransitionDuration(cacheType: value.cacheType) {\n                                    self.animating = true\n                                    let animation = Animation.linear(duration: fadeDuration)\n                                    withAnimation(animation) {\n                                        // Trigger the view render to apply the animation.\n                                        self.markLoaded(sendChangeEvent: true)\n                                    }\n                                } else {\n                                    self.markLoaded(sendChangeEvent: false)\n                                }\n                                self.loadedImage = value.image\n                                self.animating = false\n                            }\n\n                            CallbackQueueMain.async {\n                                context.onSuccessDelegate.call(value)\n                            }\n                        case .failure(let error):\n                            CallbackQueueMain.currentOrAsync {\n                                if let view = context.failureView {\n                                    self.failureView = view\n                                } else if let image = context.options.onFailureImage {\n                                    self.loadedImage = image\n                                }\n                                self.markLoaded(sendChangeEvent: false)\n                            }\n                            \n                            CallbackQueueMain.async {\n                                context.onFailureDelegate.call(error)\n                            }\n                        }\n                })\n        }\n        \n        private func updateProgress(downloaded: Int64, total: Int64) {\n            progress.totalUnitCount = total\n            progress.completedUnitCount = downloaded\n            objectWillChange.send()\n        }\n\n        /// Cancels the download task if it is in progress.\n        func cancel() {\n            downloadTask?.cancel()\n            downloadTask = nil\n            loading = false\n        }\n        \n        /// Restores the download task priority to default if it is in progress.\n        func restorePriorityOnAppear() {\n            guard let downloadTask = downloadTask, loading == true else { return }\n            downloadTask.sessionTask?.task.priority = URLSessionTask.defaultPriority\n        }\n        \n        /// Reduce the download task priority if it is in progress.\n        func reducePriorityOnDisappear() {\n            guard let downloadTask = downloadTask, loading == true else { return }\n            downloadTask.sessionTask?.task.priority = URLSessionTask.lowPriority\n        }\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/SwiftUI/ImageContext.swift",
    "content": "//\n//  ImageContext.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/05/08.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(SwiftUI) && canImport(Combine)\nimport SwiftUI\nimport Combine\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension KFImage {\n    public class Context<HoldingView: KFImageHoldingView>: @unchecked Sendable where HoldingView: Sendable {\n        \n        private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.KFImageContextPropertyQueue\")\n        \n        let source: Source?\n        var _options = KingfisherParsedOptionsInfo(\n            KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously]\n        )\n        var options: KingfisherParsedOptionsInfo {\n            get { propertyQueue.sync { _options } }\n            set { propertyQueue.sync { _options = newValue } }\n        }\n\n        var _configurations: [(HoldingView) -> HoldingView] = []\n        var configurations: [(HoldingView) -> HoldingView] {\n            get { propertyQueue.sync { _configurations } }\n            set { propertyQueue.sync { _configurations = newValue } }\n        }\n        \n        var _renderConfigurations: [(HoldingView.RenderingView) -> Void] = []\n        var renderConfigurations: [(HoldingView.RenderingView) -> Void] {\n            get { propertyQueue.sync { _renderConfigurations } }\n            set { propertyQueue.sync { _renderConfigurations = newValue } }\n        }\n        \n        var _contentConfiguration: ((HoldingView) -> AnyView)? = nil\n        var contentConfiguration: ((HoldingView) -> AnyView)? {\n            get { propertyQueue.sync { _contentConfiguration } }\n            set { propertyQueue.sync { _contentConfiguration = newValue } }\n        }\n        \n        var _cancelOnDisappear: Bool = false\n        var cancelOnDisappear: Bool {\n            get { propertyQueue.sync { _cancelOnDisappear } }\n            set { propertyQueue.sync { _cancelOnDisappear = newValue } }\n        }\n\n        var _reducePriorityOnDisappear: Bool = false\n\t\tvar reducePriorityOnDisappear: Bool {\n            get { propertyQueue.sync { _reducePriorityOnDisappear } }\n            set { propertyQueue.sync { _reducePriorityOnDisappear = newValue } }\n        }\n        \n        var _placeholder: ((Progress) -> AnyView)? = nil\n        var placeholder: ((Progress) -> AnyView)? {\n            get { propertyQueue.sync { _placeholder } }\n            set { propertyQueue.sync { _placeholder = newValue } }\n        }\n\n        var _failureView: (() -> AnyView)? = nil\n        var failureView: (() -> AnyView)? {\n            get { propertyQueue.sync { _failureView } }\n            set { propertyQueue.sync { _failureView = newValue } }\n        }\n\n        var _startLoadingBeforeViewAppear: Bool = false\n        var startLoadingBeforeViewAppear: Bool {\n            get { propertyQueue.sync { _startLoadingBeforeViewAppear } }\n            set { propertyQueue.sync { _startLoadingBeforeViewAppear = newValue } }\n        }\n        \n        // SwiftUI transition support\n        var _swiftUITransition: AnyTransition? = nil\n        var swiftUITransition: AnyTransition? {\n            get { propertyQueue.sync { _swiftUITransition } }\n            set { propertyQueue.sync { _swiftUITransition = newValue } }\n        }\n        \n        var _swiftUIAnimation: Animation? = nil\n        var swiftUIAnimation: Animation? {\n            get { propertyQueue.sync { _swiftUIAnimation } }\n            set { propertyQueue.sync { _swiftUIAnimation = newValue } }\n        }\n\n        let onFailureDelegate = Delegate<KingfisherError, Void>()\n        let onSuccessDelegate = Delegate<RetrieveImageResult, Void>()\n        let onProgressDelegate = Delegate<(Int64, Int64), Void>()\n        \n        init(source: Source?) {\n            self.source = source\n        }\n        \n        func shouldApplyFade(cacheType: CacheType) -> Bool {\n            options.forceTransition || cacheType == .none\n        }\n\n        func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? {\n            shouldApplyFade(cacheType: cacheType)\n            ? options.transition.fadeDuration\n                : nil\n        }\n    }\n}\n\nextension ImageTransition {\n    // Only for fade effect in SwiftUI.\n    fileprivate var fadeDuration: TimeInterval? {\n        switch self {\n        case .fade(let duration):\n            return duration\n        default:\n            return nil\n        }\n    }\n}\n\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension KFImage.Context: Hashable {\n    public static func == (lhs: KFImage.Context<HoldingView>, rhs: KFImage.Context<HoldingView>) -> Bool {\n        lhs.source == rhs.source &&\n        lhs.options.processor.identifier == rhs.options.processor.identifier\n    }\n\n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(source)\n        hasher.combine(options.processor.identifier)\n    }\n}\n\n#if !os(watchOS)\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)\nextension KFAnimatedImage {\n    public typealias Context = KFImage.Context\n    typealias ImageBinder = KFImage.ImageBinder\n}\n#endif\n\n#endif\n"
  },
  {
    "path": "Sources/SwiftUI/KFAnimatedImage.swift",
    "content": "//\n//  KFAnimatedImage.swift\n//  Kingfisher\n//\n//  Created by wangxingbin on 2021/4/29.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(SwiftUI) && canImport(Combine) && !os(watchOS)\nimport SwiftUI\nimport Combine\n\n/// Represents an animated image view in SwiftUI that manages its content using Kingfisher.\n///\n/// Similar to ``KFImage``, this view provides support for animated image formats like GIF.\n///\n/// - Important: Like ``KFImage``, `KFAnimatedImage` loads disk cached images synchronously by default \n/// (`.loadDiskFileSynchronously()` is enabled). This prevents image flickering during SwiftUI view updates \n/// but may impact performance when loading large animated images from disk. You can disable this behavior \n/// by calling `.loadDiskFileSynchronously(false)` if you prefer better loading performance over visual consistency.\n///\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)\npublic struct KFAnimatedImage: KFImageProtocol {\n    public typealias HoldingView = KFAnimatedImageViewRepresenter\n    public var context: Context<HoldingView>\n    public init(context: KFImage.Context<HoldingView>) {\n        self.context = context\n    }\n    \n    /// Configures current rendering view with a `block`. This block will be applied when the under-hood\n    /// `AnimatedImageView` is created in `UIViewRepresentable.makeUIView(context:)`\n    ///\n    /// - Parameter block: The block applies to the animated image view.\n    /// - Returns: A `KFAnimatedImage` view that being configured by the `block`.\n    public func configure(_ block: @escaping (HoldingView.RenderingView) -> Void) -> Self {\n        context.renderConfigurations.append(block)\n        return self\n    }\n\n#if os(iOS)\n    /// Whether the animated frame buffer should be purged when the app enters background.\n    ///\n    /// This is an opt-in behavior to reduce memory footprint when your app is in background. When enabled,\n    /// the internal `AnimatedImageView` stops animating and purges preloaded frames on\n    /// `UIApplication.didEnterBackgroundNotification`. If the view was animating before entering background, it will\n    /// prepare frames and resume animation on `UIApplication.willEnterForegroundNotification`.\n    ///\n    /// - Parameter purge: Whether to enable the frame purging behavior. Default is `true`.\n    /// - Returns: A `KFAnimatedImage` view that configures the behavior.\n    public func purgeFramesOnBackground(_ purge: Bool = true) -> Self {\n        configure { $0.purgeFramesOnBackground = purge }\n    }\n#endif\n}\n\n#if os(macOS)\n@available(macOS 11.0, *)\ntypealias KFCrossPlatformViewRepresentable = NSViewRepresentable\n#else\n@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *)\ntypealias KFCrossPlatformViewRepresentable = UIViewRepresentable\n#endif\n\n/// A wrapped `UIViewRepresentable` of `AnimatedImageView`\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\npublic struct KFAnimatedImageViewRepresenter: KFCrossPlatformViewRepresentable, KFImageHoldingView, Sendable {\n    public typealias RenderingView = AnimatedImageView\n    public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> KFAnimatedImageViewRepresenter {\n        KFAnimatedImageViewRepresenter(image: image, context: context)\n    }\n    \n    var image: KFCrossPlatformImage?\n    let context: KFImage.Context<KFAnimatedImageViewRepresenter>\n    \n    #if os(macOS)\n    public func makeNSView(context: Context) -> AnimatedImageView {\n        return makeImageView()\n    }\n    \n    public func updateNSView(_ nsView: AnimatedImageView, context: Context) {\n        updateImageView(nsView)\n    }\n    #else\n    public func makeUIView(context: Context) -> AnimatedImageView {\n        return makeImageView()\n    }\n    \n    public func updateUIView(_ uiView: AnimatedImageView, context: Context) {\n        updateImageView(uiView)\n    }\n    #endif\n    \n    @MainActor\n    private func makeImageView() -> AnimatedImageView {\n        let view = AnimatedImageView()\n        \n        #if !os(macOS)\n        view.isUserInteractionEnabled = true\n        #endif\n        \n        self.context.renderConfigurations.forEach { $0(view) }\n        \n        view.image = image\n        \n        // Allow SwiftUI scale (fit/fill) working fine.\n        view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)\n        view.setContentCompressionResistancePriority(.defaultLow, for: .vertical)\n        return view\n    }\n    \n    @MainActor\n    private func updateImageView(_ imageView: AnimatedImageView) {\n        imageView.image = image\n    }\n}\n\n#if DEBUG\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nstruct KFAnimatedImage_Previews: PreviewProvider {\n    static var previews: some View {\n        Group {\n            KFAnimatedImage(source: .network(URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif\")!))\n                .onSuccess { r in\n                    print(r)\n                }\n                .placeholder {\n                    ProgressView()\n                }\n                .padding()\n        }\n    }\n}\n#endif\n#endif\n"
  },
  {
    "path": "Sources/SwiftUI/KFImage.swift",
    "content": "//\n//  KFImage.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2019/06/26.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(SwiftUI) && canImport(Combine)\nimport SwiftUI\nimport Combine\n\n/// Represents an image view in SwiftUI that manages its content using Kingfisher.\n///\n/// This view asynchronously loads the content. You can set a ``Source`` to load for the ``KFImage`` through\n/// its ``KFImage/init(source:)`` or ``KFImage/init(_:)`` initializers or other relevant methods in ``KF`` Builder type.\n///  Kingfisher will first look for the required image in the cache. If it is not found, it will load it via the\n///  ``Source`` and provide the result for display, following sending the result to cache and for the future use.\n///\n/// When using a `URL` valve as the ``Source``, it is similar to SwiftUI's `AsyncImage` but with additional support\n/// for caching.\n///\n/// Here is a basic example of using ``KFImage``:\n///\n/// ```swift\n/// var body: some View {\n///   KFImage(URL(string: \"https://example.com/image.png\")!)\n/// }\n/// ```\n///\n/// Usually, you can also use the value by calling additional modifiers defined on it, to configure the view:\n///\n/// ```swift\n/// var body: some View {\n///     KFImage.url(url)\n///       .placeholder(placeholderImage)\n///       .setProcessor(processor)\n///       .loadDiskFileSynchronously()\n///       .cacheMemoryOnly()\n///       .onSuccess { result in  }\n/// }\n/// ```\n/// Here only very few are listed as demonstration. To check other available modifiers, see ``KFOptionSetter`` and its\n/// extension methods.\n///\n/// - Important: `KFImage` loads disk cached images synchronously by default (`.loadDiskFileSynchronously()` is enabled).\n/// This prevents image flickering during SwiftUI view updates but may impact performance when loading large images from disk.\n/// You can disable this behavior by calling `.loadDiskFileSynchronously(false)` if you prefer better loading performance\n/// over visual consistency.\n///\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\npublic struct KFImage: KFImageProtocol {\n    \n    /// Represent the wrapping context of the image view.\n    ///\n    /// Inside ``KFImage`` it is using the `SwiftUI.Image` to render the image.\n    public var context: Context<Image>\n    \n    /// Initializes the ``KFImage`` with a context.\n    ///\n    /// This should be only used internally in Kingfisher. Do not use this initializer yourself. Instead, use\n    ///  ``KFImage/init(source:)`` or ``KFImage/init(_:)`` initializers or other relevant methods in ``KF`` Builder\n    ///  type.\n    /// - Parameter context: The context value that the image view should wrap.\n    public init(context: Context<Image>) {\n        self.context = context\n    }\n}\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension Image: KFImageHoldingView {\n    public typealias RenderingView = Image\n    public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> Image {\n        Image(crossPlatformImage: image)\n    }\n}\n\n// MARK: - Image compatibility.\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension KFImage {\n\n    public func resizable(\n        capInsets: EdgeInsets = EdgeInsets(),\n        resizingMode: Image.ResizingMode = .stretch) -> KFImage\n    {\n        configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) }\n    }\n\n    public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage {\n        configure { $0.renderingMode(renderingMode) }\n    }\n\n    public func interpolation(_ interpolation: Image.Interpolation) -> KFImage {\n        configure { $0.interpolation(interpolation) }\n    }\n\n    public func antialiased(_ isAntialiased: Bool) -> KFImage {\n        configure { $0.antialiased(isAntialiased) }\n    }\n    \n    /// Starts the loading process of `self` immediately.\n    ///\n    /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading\n    /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a\n    /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once\n    /// could help avoiding the flickering, with some performance trade-off.\n    ///\n    /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data.\n    /// It does nothing now and please just remove it.\n    ///\n    /// - Returns: The `Self` value with changes applied.\n    @available(*, deprecated, message: \"This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.\")\n    public func loadImmediately(_ start: Bool = true) -> KFImage {\n        return self\n    }\n}\n\n#if DEBUG\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nstruct KFImage_Previews: PreviewProvider {\n    static var previews: some View {\n        Group {\n            KFImage.url(URL(string: \"https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png\")!)\n                .onSuccess { r in\n                    print(r)\n                }\n                .placeholder { p in\n                    ProgressView(p)\n                }\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .padding()\n        }\n    }\n}\n#endif\n#endif\n"
  },
  {
    "path": "Sources/SwiftUI/KFImageOptions.swift",
    "content": "//\n//  KFImageOptions.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/12/20.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(SwiftUI) && canImport(Combine)\nimport SwiftUI\nimport Combine\n\n// MARK: - KFImage creating.\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension KFImageProtocol {\n\n    /// Creates a Kingfisher-compatible image view with a given ``Source``.\n    ///\n    /// - Parameters:\n    ///   - source: The ``Source`` object that defines data information from the network or a data provider.\n    /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`.\n    public static func source(\n        _ source: Source?\n    ) -> Self\n    {\n        Self.init(source: source)\n    }\n\n    /// Creates a Kingfisher-compatible image view with a given ``Resource``.\n    ///\n    /// - Parameters:\n    ///   - resource: The ``Resource`` object that defines data information such as a key or URL.\n    /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`.\n    public static func resource(\n        _ resource: (any Resource)?\n    ) -> Self\n    {\n        source(resource?.convertToSource())\n    }\n\n    /// Creates a Kingfisher-compatible image view with a given `URL`.\n    ///\n    /// - Parameters:\n    ///   - url: The `URL` from which the image should be downloaded.\n    ///   - cacheKey: The key used to store the downloaded image in the cache. If `nil`, the `absoluteString` of `url`\n    ///   is used as the cache key.\n    /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`.\n    public static func url(\n        _ url: URL?, cacheKey: String? = nil\n    ) -> Self\n    {\n        source(url?.convertToSource(overrideCacheKey: cacheKey))\n    }\n\n    /// Creates a Kingfisher-compatible image view with a given ``ImageDataProvider``.\n    ///\n    /// - Parameters:\n    ///   - provider: The ``ImageDataProvider`` object that contains information about the data.\n    /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`.\n\n    public static func dataProvider(\n        _ provider: (any ImageDataProvider)?\n    ) -> Self\n    {\n        source(provider?.convertToSource())\n    }\n\n    /// Creates a builder for the provided raw data and a cache key.\n    ///\n    /// - Parameters:\n    ///   - data: The data object from which the image should be created.\n    ///   - cacheKey: The key used to store the downloaded image in the cache.\n    /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`.\n    public static func data(\n        _ data: Data?, cacheKey: String\n    ) -> Self\n    {\n        if let data = data {\n            return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey))\n        } else {\n            return dataProvider(nil)\n        }\n    }\n}\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension KFImageProtocol {\n    \n    /// Sets a placeholder `View` that is displayed during the image loading, with a progress parameter as input.\n    ///\n    /// - Parameter content: A view that represents the placeholder.\n    /// - Returns: A Kingfisher-compatible image view that includes the provided `content` as its placeholder.\n    public func placeholder<P: View>(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self {\n        context.placeholder = { progress in\n            return AnyView(content(progress))\n        }\n        return self\n    }\n    \n    /// Sets a placeholder `View` that is displayed during the image loading.\n    ///\n    /// - Parameter content: A view that represents the placeholder.\n    /// - Returns: A Kingfisher-compatible image view that includes the provided `content` as its placeholder.\n    public func placeholder<P: View>(@ViewBuilder _ content: @escaping () -> P) -> Self {\n        placeholder { _ in content() }\n    }\n\n    /// Sets a failure `View` that is displayed when the image fails to load.\n    ///\n    /// Use this modifier to provide a custom view when image loading fails. This offers more flexibility than the\n    /// deprecated `onFailureImage` API by allowing any SwiftUI view as the failure placeholder.\n    ///\n    /// Example:\n    /// ```swift\n    /// KFImage(url)\n    ///     .onFailureView {\n    ///         VStack {\n    ///             Image(systemName: \"exclamationmark.triangle\")\n    ///                 .foregroundColor(.red)\n    ///             Text(\"Failed to load image\")\n    ///                 .font(.caption)\n    ///             Button(\"Retry\") {\n    ///                 // Retry logic\n    ///             }\n    ///         }\n    ///     }\n    /// ```\n    ///\n    /// - Note: If both deprecated `onFailureImage` and `onFailureView` are set, `onFailureView` takes precedence.\n    /// \n    /// - Parameter content: A view builder that creates the failure view.\n    /// - Returns: A Kingfisher-compatible image view that displays the provided `content` when image loading fails.\n    public func onFailureView<F: View>(@ViewBuilder _ content: @escaping () -> F) -> Self {\n        context.failureView = { AnyView(content()) }\n        return self\n    }\n\n    /// Sets an image to display when the loading fails.\n    ///\n    /// - Deprecated: Use ``onFailureView(_:)`` instead, which lets you return any SwiftUI `View` and guarantees\n    ///   consistent behavior across SwiftUI platforms. The image-based fallback modifier is maintained purely for\n    ///   backward compatibility and will be removed in a future major release.\n    @available(*, deprecated, message: \"Use `onFailureView(_:)` to customize SwiftUI failure placeholders instead.\")\n    public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self {\n        options.onFailureImage = .some(image)\n        return self\n    }\n\n    /// Enables canceling the download task associated with `self` when the view disappears.\n    ///\n    /// - Parameter flag: A boolean value indicating whether to cancel the task.\n    /// - Returns: A Kingfisher-compatible image view that cancels the download task when it disappears.\n    public func cancelOnDisappear(_ flag: Bool) -> Self {\n        context.cancelOnDisappear = flag\n        return self\n    }\n    \n    /// Sets reduce priority  of the download task to low,  bound to `self` when the view disappearing.\n    /// - Parameter flag: Whether reduce the priority task or not.\n    /// - Returns: A `KFImage` view that reduces downloading task priority when disappears.\n    public func reducePriorityOnDisappear(_ flag: Bool) -> Self {\n        context.reducePriorityOnDisappear = flag\n        return self\n    }\n\n\n    /// Sets a fade transition for the image task.\n    ///\n    /// - Parameter duration: The duration of the fade transition.\n    /// - Returns: A Kingfisher-compatible image view with the applied changes.\n    ///\n    /// Kingfisher will use the fade transition to animate the image if it is downloaded from the web. The transition \n    /// will not occur when the image is retrieved from either memory or disk cache by default. If you need the\n    /// transition to occur even when the image is retrieved from the cache, also call\n    /// ``KFOptionSetter/forceRefresh(_:)`` on the returned view.\n    public func fade(duration: TimeInterval) -> Self {\n        context.options.transition = .fade(duration)\n        return self\n    }\n    \n    /// Sets whether to start the image loading before the view actually appears.\n    ///\n    /// - Parameter flag: A boolean value indicating whether the image loading should happen before the view appears. The default is `true`.\n    /// - Returns: A Kingfisher-compatible image view with the applied changes.\n    ///\n    /// By default, Kingfisher performs lazy loading for `KFImage`. The image loading won't start until the view's\n    /// `onAppear` is called. However, sometimes you may want to trigger aggressive loading for the view. By enabling\n    /// this, the `KFImage` will attempt to load the view when its `body` is evaluated if the image loading has not\n    /// yet started or if a previous loading attempt failed.\n    ///\n    /// > Important: This was a temporary workaround for an issue that arose in iOS 16, where the SwiftUI view's\n    /// > `onAppear` was not called when it was deeply embedded inside a `List` or `ForEach`. This is no longer necessary\n    /// > if built with Xcode 14.3 and deployed to iOS 16.4 or later. So, it is not needed anymore.\n    /// >\n    /// > Enabling this may cause performance regression, especially if you have a lot of images to load in the view.\n    /// > Use it at your own risk.\n    /// >\n    /// > Please refer to [#1988](https://github.com/onevcat/Kingfisher/issues/1988) for more information.\n    public func startLoadingBeforeViewAppear(_ flag: Bool = true) -> Self {\n        context.startLoadingBeforeViewAppear = flag\n        return self\n    }\n    \n    /// Sets a SwiftUI transition for the image loading.\n    ///\n    /// - Parameters:\n    ///   - transition: The SwiftUI transition to apply when the image appears.\n    ///   - animation: The animation to use with the transition. Defaults to `.default`.\n    /// - Returns: A Kingfisher-compatible image view with the applied transition.\n    ///\n    /// This is the recommended way to apply transitions in SwiftUI applications. Unlike the UIKit-based\n    /// ``KingfisherOptionsInfoItem/transition(_:)`` option, this method uses native SwiftUI transitions,\n    /// providing better integration with the SwiftUI animation system and access to all SwiftUI transition types.\n    ///\n    /// Available transitions include `.slide`, `.scale`, `.opacity`, `.move`, `.offset`, and custom transitions.\n    /// The transition will be applied when the image is loaded from the network, following the same\n    /// rules as the fade transition regarding cache behavior and `forceTransition`.\n    /// \n    /// When both `loadTransition` and `fade` are set, `loadTransition` takes precedence.\n    ///\n    /// Example:\n    /// ```swift\n    /// KFImage(url)\n    ///     .loadTransition(.slide, animation: .easeInOut(duration: 0.5))\n    /// ```\n    ///\n    /// - Note: For UIKit/AppKit applications, use ``KingfisherOptionsInfoItem/transition(_:)`` instead.\n    public func loadTransition(_ transition: AnyTransition, animation: Animation? = .default) -> Self {\n        context.swiftUITransition = transition\n        context.swiftUIAnimation = animation\n        return self\n    }\n    \n    /// Sets a SwiftUI transition for the image loading (iOS 17.0+).\n    ///\n    /// - Parameters:\n    ///   - transition: The SwiftUI transition conforming to the Transition protocol.\n    ///   - animation: The animation to use with the transition. Defaults to `.default`.\n    /// - Returns: A Kingfisher-compatible image view with the applied transition.\n    ///\n    /// This method provides access to newer SwiftUI transitions available in iOS 17.0+,\n    /// such as `BlurReplaceTransition`, `PushTransition`, and other transitions conforming to the `Transition` protocol.\n    /// This is the recommended approach for SwiftUI applications on iOS 17.0+.\n    /// \n    /// When both `loadTransition` and `fade` are set, `loadTransition` takes precedence.\n    ///\n    /// Example:\n    /// ```swift\n    /// KFImage(url)\n    ///     .loadTransition(.blurReplace(.downUp), animation: .bouncy)\n    /// ```\n    ///\n    /// - Note: For UIKit/AppKit applications, use ``KingfisherOptionsInfoItem/transition(_:)`` instead.\n    @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)\n    public func loadTransition<T: Transition>(_ transition: T, animation: Animation? = .default) -> Self {\n        context.swiftUITransition = AnyTransition(transition)\n        context.swiftUIAnimation = animation\n        return self\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/SwiftUI/KFImageProtocol.swift",
    "content": "//\n//  KFImageProtocol.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/05/08.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(SwiftUI) && canImport(Combine)\nimport SwiftUI\nimport Combine\n\n\n/// Represents a view that is compatible with Kingfisher in SwiftUI.\n///\n/// As a framework user, you do not need to know the details of this protocol. As the public types, ``KFImage`` and\n/// ``KFAnimatedImage`` conform this type and should be used in your app to represent an image view with network and\n/// cache support in SwiftUI.\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\n@MainActor\npublic protocol KFImageProtocol: View, KFOptionSetter {\n    associatedtype HoldingView: KFImageHoldingView & Sendable\n    var context: KFImage.Context<HoldingView> { get set }\n    init(context: KFImage.Context<HoldingView>)\n}\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension KFImageProtocol {\n    @MainActor\n    public var body: some View {\n        ZStack {\n            KFImageRenderer<HoldingView>(\n                context: context\n            ).id(context)\n        }\n    }\n    \n    /// Creates an image view compatible with Kingfisher for loading an image from the provided `Source`.\n    ///\n    /// - Parameters:\n    ///   - source: The `Source` of the image that specifies where to load the target image.\n    public init(source: Source?) {\n        let context = KFImage.Context<HoldingView>(source: source)\n        self.init(context: context)\n    }\n\n    /// Creates an image view compatible with Kingfisher for loading an image from the provided `URL`.\n    ///\n    /// - Parameters:\n    ///   - url: The `URL` defining the location from which to load the target image.\n    public init(_ url: URL?) {\n        self.init(source: url?.convertToSource())\n    }\n    \n    /// Configures the current image with a `block` and returns another `Image` to use as the final content.\n    ///\n    /// This block will be lazily applied when creating the final `Image`.\n    ///\n    /// If multiple `configure` modifiers are added to the image, they will be evaluated in order.\n    ///\n    /// - Parameter block: The block that applies to the loaded image. The block should return an `Image` that is\n    ///  configured.\n    /// - Returns: A ``KFImage`` or ``KFAnimatedImage`` view that configures the internal `Image` with the provided\n    /// `block`.\n    ///\n    /// > If you want to configure the input image (which is usually an `Image` value) and use a non-`Image` value as\n    /// > the configured result, use ``KFImageProtocol/contentConfigure(_:)`` instead.\n    public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self {\n        context.configurations.append(block)\n        return self\n    }\n\n    /// Configures the current image with a `block` and returns a `View` to use as the final content.\n    ///\n    /// This block will be lazily applied when creating the final `Image`.\n    ///\n    /// If multiple `contentConfigure` modifiers are added to the image, only the last one will be stored and used.\n    ///\n    /// - Parameter block: The block applies to the loaded image. The block should return a `View` that is configured.\n    /// - Returns: A ``KFImage`` or ``KFAnimatedImage`` view that configures the internal `Image` with the provided\n    /// `block`.\n    public func contentConfigure<V: View>(_ block: @escaping (HoldingView) -> V) -> Self {\n        context.contentConfiguration = { AnyView(block($0)) }\n        return self\n    }\n}\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\n@MainActor\npublic protocol KFImageHoldingView: View {\n    associatedtype RenderingView\n    static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> Self\n}\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension KFImageProtocol {\n    public var options: KingfisherParsedOptionsInfo {\n        get { context.options }\n        nonmutating set { context.options = newValue }\n    }\n\n    public var onFailureDelegate: Delegate<KingfisherError, Void> { context.onFailureDelegate }\n    public var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { context.onSuccessDelegate }\n    public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate }\n\n    public var delegateObserver: AnyObject { context }\n}\n\n\n#endif\n"
  },
  {
    "path": "Sources/SwiftUI/KFImageRenderer.swift",
    "content": "//\n//  KFImageRenderer.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2021/05/08.\n//\n//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(SwiftUI) && canImport(Combine)\nimport SwiftUI\nimport Combine\n\n/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`.\n/// Declaring a `KFImage` in a `View`'s body to trigger loading from the given `Source`.\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nstruct KFImageRenderer<HoldingView> : View where HoldingView: KFImageHoldingView & Sendable {\n    \n    @StateObject var binder: KFImage.ImageBinder = .init()\n    let context: KFImage.Context<HoldingView>\n    \n    var body: some View {\n        if context.startLoadingBeforeViewAppear && !binder.loadingOrSucceeded && !binder.animating {\n            binder.markLoading()\n            DispatchQueue.main.async { binder.start(context: context) }\n        }\n        \n        return ZStack {\n            if context.swiftUITransition != nil {\n                // SwiftUI loadTransition: insert/remove view for proper transition behavior\n                if binder.loaded {\n                    renderedImage()\n                }\n            } else {\n                // Fade transition or no transition: use opacity control\n                renderedImage()\n                    .opacity(binder.loaded ? 1.0 : 0.0)\n            }\n            if binder.loadedImage == nil {\n                ZStack {\n                    // Priority: failureView > placeholder > Color.clear\n                    // failureView is only set when image loading fails\n                    if let failureView = binder.failureView {\n                        failureView()\n                    } else if let placeholder = context.placeholder {\n                        placeholder(binder.progress)\n                    } else {\n                        Color.clear\n                    }\n                }\n                .onAppear { [weak binder = self.binder] in\n                    guard let binder = binder else {\n                        return\n                    }\n                    if !binder.loadingOrSucceeded {\n                        binder.start(context: context)\n                    } else {\n                        if context.reducePriorityOnDisappear {\n                            binder.restorePriorityOnAppear()\n                        }\n                    }\n                }\n                .onDisappear { [weak binder = self.binder] in\n                    guard let binder = binder else {\n                        return\n                    }\n                    if context.cancelOnDisappear {\n                        binder.cancel()\n                    } else if context.reducePriorityOnDisappear {\n                        binder.reducePriorityOnDisappear()\n                    }\n                }\n            }\n        }\n        // Workaround for https://github.com/onevcat/Kingfisher/issues/1988\n        // on iOS 16 there seems to be a bug that when in a List, the `onAppear` of the `ZStack` above in the\n        // `binder.loadedImage == nil` not get called. Adding this empty `onAppear` fixes it and the life cycle can\n        // work again.\n        //\n        // There is another \"fix\": adding an `else` clause and put a `Color.clear` there. But I believe this `onAppear`\n        // should work better.\n        //\n        // It should be a bug in iOS 16, I guess it is some kinds of over-optimization in list cell loading caused it.\n        .onAppear()\n    }\n    \n    @ViewBuilder\n    private func renderedImage() -> some View {\n        if let swiftUITransition = context.swiftUITransition {\n            // Apply SwiftUI loadTransition as the last step for correct rendering order\n            configuredImage.transition(swiftUITransition)\n        } else {\n            configuredImage\n        }\n    }\n    \n    @ViewBuilder\n    private var configuredImage: some View {\n        let configuredImage = context.configurations\n            .reduce(HoldingView.created(from: binder.loadedImage, context: context)) {\n                current, config in config(current)\n            }\n        \n        // Apply contentConfiguration first, then loadTransition as the final step\n        if let contentConfiguration = context.contentConfiguration {\n            contentConfiguration(configuredImage)\n        } else {\n            configuredImage\n        }\n    }\n}\n\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension Image {\n    // Creates an Image with either UIImage or NSImage.\n    init(crossPlatformImage: KFCrossPlatformImage?) {\n        #if canImport(UIKit)\n        self.init(uiImage: crossPlatformImage ?? KFCrossPlatformImage())\n        #elseif canImport(AppKit)\n        self.init(nsImage: crossPlatformImage ?? KFCrossPlatformImage())\n        #endif\n    }\n}\n\n#if canImport(UIKit)\n@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)\nextension UIImage.Orientation {\n    func toSwiftUI() -> Image.Orientation {\n        switch self {\n        case .down: return .down\n        case .up: return .up\n        case .left: return .left\n        case .right: return .right\n        case .upMirrored: return .upMirrored\n        case .downMirrored: return .downMirrored\n        case .leftMirrored: return .leftMirrored\n        case .rightMirrored: return .rightMirrored\n        @unknown default: return .up\n        }\n    }\n}\n#endif\n#endif\n"
  },
  {
    "path": "Sources/Utility/Box.swift",
    "content": "//\n//  Box.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/3/17.\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\nclass Box<T> {\n    var value: T\n    \n    init(_ value: T) {\n        self.value = value\n    }\n}\n"
  },
  {
    "path": "Sources/Utility/CallbackQueue.swift",
    "content": "//\n//  CallbackQueue.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/10/15.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\npublic typealias ExecutionQueue = CallbackQueue\n\n/// Represents the behavior of the callback queue selection when a closure is dispatched.\npublic enum CallbackQueue: Sendable {\n    \n    /// Dispatches the closure to `DispatchQueue.main` with an `async` behavior.\n    case mainAsync\n    \n    /// Dispatches the closure to `DispatchQueue.main` with an `async` behavior if the current queue is not `.main`.\n    ///  Otherwise, it calls the closure immediately on the current main queue.\n    case mainCurrentOrAsync\n    \n    /// Does not change the calling queue for the closure.\n    case untouch\n    \n    /// Dispatches the closure to a specified `DispatchQueue`.\n    case dispatch(DispatchQueue)\n\n    /// Dispatches the closure to an operation-queue–like type.\n    ///\n    /// Use this case when you want to integrate Kingfisher's work into your own scheduling policy.\n    /// For example, you can control concurrency, priority, or implement a LIFO execution order.\n    ///\n    /// - Note: Execution order and whether the block runs serially or concurrently depend on the\n    ///   provided queue. Kingfisher does not enforce ordering guarantees for this case.\n    case operationQueue(CallbackOperationQueue)\n\n    /// Executes the `block` in a dispatch queue defined by `self`.\n    /// - Parameter block: The block needs to be executed.\n    public func execute(_ block: @Sendable @escaping () -> Void) {\n        switch self {\n        case .mainAsync:\n            CallbackQueueMain.async { block() }\n        case .mainCurrentOrAsync:\n            CallbackQueueMain.currentOrAsync { block() }\n        case .untouch:\n            block()\n        case .dispatch(let queue):\n            queue.async { block() }\n        case .operationQueue(let queue):\n            queue.addOperation(block)\n        }\n    }\n\n    var queue: DispatchQueue {\n        switch self {\n        case .mainAsync: return .main\n        case .mainCurrentOrAsync: return .main\n        case .untouch: return OperationQueue.current?.underlyingQueue ?? .main\n        case .dispatch(let queue): return queue\n        case .operationQueue(let queue): return queue.underlyingQueue ?? .main\n        }\n    }\n}\n\nenum CallbackQueueMain {\n    static func currentOrAsync(_ block: @MainActor @Sendable @escaping () -> Void) {\n        if Thread.isMainThread {\n            MainActor.runUnsafely { block() }\n        } else {\n            DispatchQueue.main.async { block() }\n        }\n    }\n    \n    static func async(_ block: @MainActor @Sendable @escaping () -> Void) {\n        DispatchQueue.main.async { block() }\n    }\n}\n\nextension MainActor {\n    @_unavailableFromAsync\n    static func runUnsafely<T: Sendable>(_ body: @MainActor () throws -> T) rethrows -> T {\n#if swift(>=5.10)\n        return try MainActor.assumeIsolated(body)\n#else\n        dispatchPrecondition(condition: .onQueue(.main))\n        return try withoutActuallyEscaping(body) { fn in\n            try unsafeBitCast(fn, to: (() throws -> T).self)()\n        }\n#endif\n    }\n}\n\n/// A minimal abstraction used by ``CallbackQueue/operationQueue(_:)``.\n///\n/// Conform your own type to control how Kingfisher schedules work. `OperationQueue` already\n/// conforms to this protocol.\npublic protocol CallbackOperationQueue: AnyObject, Sendable {\n    /// The underlying `DispatchQueue` if available.\n    ///\n    /// Kingfisher uses this value only when it needs a best-effort `DispatchQueue` representation.\n    var underlyingQueue: DispatchQueue? { get }\n\n    /// Schedules a block for execution on this queue.\n    func addOperation(_ block: @Sendable @escaping () -> Void)\n}\n\nextension OperationQueue: CallbackOperationQueue {}\n"
  },
  {
    "path": "Sources/Utility/Delegate.swift",
    "content": "//\n//  Delegate.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/10/10.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n/// A class that maintains a weak reference to `self` when implementing `onXXX` behaviors.\n/// Instead of manually ensuring that `self` is kept as weak in a stored closure:\n///\n/// ```swift\n/// // MyClass.swift\n/// var onDone: (() -> Void)?\n/// func done() {\n///     onDone?()\n/// }\n///\n/// // ViewController.swift\n/// var obj: MyClass?\n///\n/// func doSomething() {\n///     obj = MyClass()\n///     obj!.onDone = { [weak self] in\n///         self?.reportDone()\n///     }\n/// }\n/// ```\n///\n/// You can create a `Delegate` and observe it on `self`. This ensures there is no retain cycle:\n///\n/// ```swift\n/// // MyClass.swift\n/// let onDone = Delegate<(), Void>()\n/// func done() {\n///     onDone.call()\n/// }\n///\n/// // ViewController.swift\n/// var obj: MyClass?\n/// \n/// func doSomething() {\n///     obj = MyClass()\n///     obj!.onDone.delegate(on: self) { (self, _) in\n///         // The `self` here is shadowed and does not retain a strong reference.\n///         // Thus, both the `MyClass` instance and the `ViewController` instance can be released.\n///         self.reportDone()\n///     }\n/// }\n///\npublic class Delegate<Input, Output>: @unchecked Sendable {\n    public init() {}\n\n    private let propertyQueue = DispatchQueue(label: \"com.onevcat.Kingfisher.DelegateQueue\")\n    \n    private var _block: ((Input) -> Output?)?\n    private var block: ((Input) -> Output?)? {\n        get { propertyQueue.sync { _block } }\n        set { propertyQueue.sync { _block = newValue } }\n    }\n    \n    private var _asyncBlock: ((Input) async -> Output?)?\n    private var asyncBlock: ((Input) async -> Output?)? {\n        get { propertyQueue.sync { _asyncBlock } }\n        set { propertyQueue.sync { _asyncBlock = newValue } }\n    }\n    \n    public func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) {\n        self.block = { [weak target] input in\n            guard let target = target else { return nil }\n            return block?(target, input)\n        }\n    }\n    \n    public func delegate<T: AnyObject>(on target: T, block: ((T, Input) async -> Output)?) {\n        self.asyncBlock = { [weak target] input in\n            guard let target = target else { return nil }\n            return await block?(target, input)\n        }\n    }\n\n    public func call(_ input: Input) -> Output? {\n        return block?(input)\n    }\n\n    public func callAsFunction(_ input: Input) -> Output? {\n        return call(input)\n    }\n    \n    public func callAsync(_ input: Input) async -> Output? {\n        return await asyncBlock?(input)\n    }\n    \n    public var isSet: Bool {\n        block != nil || asyncBlock != nil\n    }\n}\n\nextension Delegate where Input == Void {\n    public func call() -> Output? {\n        return call(())\n    }\n\n    public func callAsFunction() -> Output? {\n        return call()\n    }\n}\n\nextension Delegate where Input == Void, Output: OptionalProtocol {\n    public func call() -> Output {\n        return call(())\n    }\n\n    public func callAsFunction() -> Output {\n        return call()\n    }\n}\n\nextension Delegate where Output: OptionalProtocol {\n    public func call(_ input: Input) -> Output {\n        if let result = block?(input) {\n            return result\n        } else {\n            return Output._createNil\n        }\n    }\n\n    public func callAsFunction(_ input: Input) -> Output {\n        return call(input)\n    }\n}\n\npublic protocol OptionalProtocol {\n    static var _createNil: Self { get }\n}\nextension Optional : OptionalProtocol {\n    public static var _createNil: Optional<Wrapped> {\n         return nil\n    }\n}\n"
  },
  {
    "path": "Sources/Utility/DisplayLink.swift",
    "content": "//\n//  DisplayLink.swift\n//  Kingfisher\n//\n//  Created by yeatse on 2024/1/9.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n#if canImport(UIKit)\nimport UIKit\n#else\nimport AppKit\nimport CoreVideo\n#endif\n\nprotocol DisplayLinkCompatible: AnyObject, Sendable {\n    var isPaused: Bool { get set }\n    \n    var preferredFramesPerSecond: NSInteger { get }\n    var timestamp: CFTimeInterval { get }\n    var duration: CFTimeInterval { get }\n    \n    func add(to runLoop: RunLoop, forMode mode: RunLoop.Mode)\n    func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode)\n    \n    func invalidate()\n}\n\n#if !os(macOS)\nextension UIView {\n    func compatibleDisplayLink(target: Any, selector: Selector) -> any DisplayLinkCompatible {\n        return CADisplayLink(target: target, selector: selector)\n    }\n}\n\n#if compiler(>=6)\nextension CADisplayLink: DisplayLinkCompatible, @retroactive @unchecked Sendable {}\n#else\nextension CADisplayLink: DisplayLinkCompatible, @unchecked Sendable {}\n#endif\n\n#else\nextension NSView {\n    func compatibleDisplayLink(target: Any, selector: Selector) -> any DisplayLinkCompatible {\n#if swift(>=5.9) // macOS 14 SDK is included in Xcode 15, which comes with swift 5.9. Add this check to make old compilers happy.\n        if #available(macOS 14.0, *) {\n            return displayLink(target: target, selector: selector)\n        } else {\n            return DisplayLink(target: target, selector: selector)\n        }\n#else\n        return DisplayLink(target: target, selector: selector)\n#endif\n    }\n}\n\n#if swift(>=5.9)\n@available(macOS 14.0, *)\nextension CADisplayLink: DisplayLinkCompatible {\n    var preferredFramesPerSecond: NSInteger { return 0 }\n}\n#if compiler(>=6)\n@available(macOS 14.0, *)\nextension CADisplayLink: @retroactive @unchecked Sendable { }\n#else // compiler(>=6)\n@available(macOS 14.0, *)\nextension CADisplayLink: @unchecked Sendable { }\n#endif // compiler(>=6)\n#endif // swift(>=5.9)\n\nfinal class DisplayLink: DisplayLinkCompatible, @unchecked Sendable {\n    private var link: CVDisplayLink?\n    private var target: Any?\n    private var selector: Selector?\n    \n    private var schedulers: [RunLoop: [RunLoop.Mode]] = [:]\n    \n    var preferredFramesPerSecond: NSInteger = 0\n    var timestamp: CFTimeInterval = 0\n    var duration: CFTimeInterval = 0\n    \n    init(target: Any, selector: Selector) {\n        self.target = target\n        self.selector = selector\n        CVDisplayLinkCreateWithActiveCGDisplays(&link)\n        if let link = link {\n            CVDisplayLinkSetOutputHandler(link) { displayLink, inNow, inOutputTime, flagsIn, flagsOut in\n                self.displayLinkCallback(\n                    displayLink, inNow: inNow, inOutputTime: inOutputTime, flagsIn: flagsIn, flagsOut: flagsOut\n                )\n            }\n        }\n    }\n    \n    deinit {\n        self.invalidate()\n    }\n    \n    private func displayLinkCallback(_ link: CVDisplayLink,\n                                     inNow: UnsafePointer<CVTimeStamp>,\n                                     inOutputTime: UnsafePointer<CVTimeStamp>,\n                                     flagsIn: CVOptionFlags,\n                                     flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn\n    {\n        let outputTime = inOutputTime.pointee\n        DispatchQueue.main.async {\n            guard let selector = self.selector, let target = self.target else { return }\n            if outputTime.videoTimeScale != 0 {\n                self.duration = CFTimeInterval(Double(outputTime.videoRefreshPeriod) / Double(outputTime.videoTimeScale))\n            }\n            if self.timestamp != 0 {\n                for scheduler in self.schedulers {\n                    scheduler.key.perform(selector, target: target, argument: nil, order: 0, modes: scheduler.value)\n                }\n            }\n            self.timestamp = CFTimeInterval(Double(outputTime.hostTime) / 1_000_000_000)\n        }\n        return kCVReturnSuccess\n    }\n    \n    var isPaused: Bool = true {\n        didSet {\n            guard let link = link else { return }\n            if isPaused {\n                if CVDisplayLinkIsRunning(link) {\n                    CVDisplayLinkStop(link)\n                }\n            } else {\n                if !CVDisplayLinkIsRunning(link) {\n                    CVDisplayLinkStart(link)\n                }\n            }\n        }\n    }\n    \n    func add(to runLoop: RunLoop, forMode mode: RunLoop.Mode) {\n        assert(runLoop == .main)\n        schedulers[runLoop, default: []].append(mode)\n    }\n    \n    func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode) {\n        schedulers[runLoop]?.removeAll { $0 == mode }\n        if let modes = schedulers[runLoop], modes.isEmpty {\n            schedulers.removeValue(forKey: runLoop)\n        }\n    }\n    \n    func invalidate() {\n        schedulers = [:]\n        isPaused = true\n        target = nil\n        selector = nil\n        if let link = link {\n            CVDisplayLinkSetOutputHandler(link) { _, _, _, _, _ in kCVReturnSuccess }\n        }\n    }\n}\n#endif\n#endif\n"
  },
  {
    "path": "Sources/Utility/ExtensionHelpers.swift",
    "content": "//\n//  ExtensionHelpers.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/09/28.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\nextension CGFloat {\n    var isEven: Bool {\n        return truncatingRemainder(dividingBy: 2.0) == 0\n    }\n}\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\nextension NSBezierPath {\n    convenience init(roundedRect rect: NSRect, topLeftRadius: CGFloat, topRightRadius: CGFloat,\n                     bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat)\n    {\n        self.init()\n        \n        let maxCorner = min(rect.width, rect.height) / 2\n        \n        let radiusTopLeft = min(maxCorner, max(0, topLeftRadius))\n        let radiusTopRight = min(maxCorner, max(0, topRightRadius))\n        let radiusBottomLeft = min(maxCorner, max(0, bottomLeftRadius))\n        let radiusBottomRight = min(maxCorner, max(0, bottomRightRadius))\n        \n        guard !rect.isEmpty else {\n            return\n        }\n        \n        let topLeft = NSPoint(x: rect.minX, y: rect.maxY)\n        let topRight = NSPoint(x: rect.maxX, y: rect.maxY)\n        let bottomRight = NSPoint(x: rect.maxX, y: rect.minY)\n        \n        move(to: NSPoint(x: rect.midX, y: rect.maxY))\n        appendArc(from: topLeft, to: rect.origin, radius: radiusTopLeft)\n        appendArc(from: rect.origin, to: bottomRight, radius: radiusBottomLeft)\n        appendArc(from: bottomRight, to: topRight, radius: radiusBottomRight)\n        appendArc(from: topRight, to: topLeft, radius: radiusTopRight)\n        close()\n    }\n    \n    convenience init(roundedRect rect: NSRect, byRoundingCorners corners: RectCorner, radius: CGFloat) {\n        let radiusTopLeft = corners.contains(.topLeft) ? radius : 0\n        let radiusTopRight = corners.contains(.topRight) ? radius : 0\n        let radiusBottomLeft = corners.contains(.bottomLeft) ? radius : 0\n        let radiusBottomRight = corners.contains(.bottomRight) ? radius : 0\n        \n        self.init(roundedRect: rect, topLeftRadius: radiusTopLeft, topRightRadius: radiusTopRight,\n                  bottomLeftRadius: radiusBottomLeft, bottomRightRadius: radiusBottomRight)\n    }\n}\n\nextension KFCrossPlatformImage {\n    // macOS does not support scale. This is just for code compatibility across platforms.\n    convenience init?(data: Data, scale: CGFloat) {\n        self.init(data: data)\n    }\n}\n#endif\n\n#if canImport(UIKit)\nimport UIKit\nextension RectCorner {\n    var uiRectCorner: UIRectCorner {\n        \n        var result: UIRectCorner = []\n        \n        if contains(.topLeft) { result.insert(.topLeft) }\n        if contains(.topRight) { result.insert(.topRight) }\n        if contains(.bottomLeft) { result.insert(.bottomLeft) }\n        if contains(.bottomRight) { result.insert(.bottomRight) }\n        \n        return result\n    }\n}\n#endif\n\nextension Date {\n    var isPast: Bool {\n        return isPast(referenceDate: Date())\n    }\n\n    func isPast(referenceDate: Date) -> Bool {\n        return timeIntervalSince(referenceDate) <= 0\n    }\n\n    // `Date` in memory is a wrap for `TimeInterval`. But in file attribute it can only accept `Int` number.\n    // By default the system will `round` it. But it is not friendly for testing purpose.\n    // So we always `ceil` the value when used for file attributes.\n    var fileAttributeDate: Date {\n        return Date(timeIntervalSince1970: ceil(timeIntervalSince1970))\n    }\n}\n"
  },
  {
    "path": "Sources/Utility/Result.swift",
    "content": "//\n//  Result.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/09/22.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n// These helper methods are not public since we do not want them to be exposed or cause any conflicting.\n// However, they are just wrapper of `ResultUtil` static methods.\nextension Result where Failure: Error {\n\n    /// Evaluates the given transformation closures to create a single output value.\n    ///\n    /// - Parameters:\n    ///   - onSuccess: A closure that transforms the success value.\n    ///   - onFailure: A closure that transforms the error value.\n    /// - Returns: A single `Output` value.\n    func match<Output>(\n        onSuccess: (Success) -> Output,\n        onFailure: (Failure) -> Output) -> Output\n    {\n        switch self {\n        case let .success(value):\n            return onSuccess(value)\n        case let .failure(error):\n            return onFailure(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Utility/Runtime.swift",
    "content": "//\n//  Runtime.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/10/12.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\nfunc getAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer) -> T? {\n    if #available(iOS 14, macOS 11, watchOS 7, tvOS 14, *) { // swift 5.3 fixed this issue (https://github.com/swiftlang/swift/issues/46456)\n        return objc_getAssociatedObject(object, key) as? T\n    } else {\n        return objc_getAssociatedObject(object, key) as AnyObject as? T\n    }\n}\n\nfunc setRetainedAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer, _ value: T) {\n    objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)\n}\n"
  },
  {
    "path": "Sources/Utility/SizeExtensions.swift",
    "content": "//\n//  SizeExtensions.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/09/28.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport CoreGraphics\n\nextension CGSize: KingfisherCompatibleValue {}\nextension KingfisherWrapper where Base == CGSize {\n    \n    /// Returns a size by resizing the `base` size to a target size under a given content mode.\n    ///\n    /// - Parameters:\n    ///   - size: The target size to resize to.\n    ///   - contentMode: The content mode of the target size when resizing.\n    /// - Returns: The resized size under the given ``ContentMode``.\n    public func resize(to size: CGSize, for contentMode: ContentMode) -> CGSize {\n        switch contentMode {\n        case .aspectFit:\n            return constrained(size)\n        case .aspectFill:\n            return filling(size)\n        case .none:\n            return size\n        }\n    }\n    \n    /// Returns a size by resizing the `base` size to make it aspect-fit the given `size`.\n    ///\n    /// - Parameter size: The size in which the `base` should fit.\n    /// - Returns: The size that fits the `base` within the input `size`, while keeping the `base` aspect.\n    public func constrained(_ size: CGSize) -> CGSize {\n        let aspectWidth = round(aspectRatio * size.height)\n        let aspectHeight = round(size.width / aspectRatio)\n        \n        return aspectWidth > size.width ?\n            CGSize(width: size.width, height: aspectHeight) :\n            CGSize(width: aspectWidth, height: size.height)\n    }\n    \n    /// Returns a size by resizing the `base` size to make it aspect-fill the given `size`.\n    ///\n    /// - Parameter size: The size that the `base` should fill.\n    /// - Returns: The size filled by the input `size`, while keeping the `base` aspect.\n    public func filling(_ size: CGSize) -> CGSize {\n        let aspectWidth = round(aspectRatio * size.height)\n        let aspectHeight = round(size.width / aspectRatio)\n        \n        return aspectWidth < size.width ?\n            CGSize(width: size.width, height: aspectHeight) :\n            CGSize(width: aspectWidth, height: size.height)\n    }\n    \n    /// Returns a `CGRect` in which the `base` size is constrained to fit within a specified `size`, anchored at a \n    /// particular `anchor` point.\n    ///\n    /// - Parameters:\n    ///   - size: The size to which the `base` should be constrained.\n    ///   - anchor: The anchor point where the size constraint is applied.\n    /// - Returns: A `CGRect` that results from the constraint operation.\n    public func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect {\n        \n        let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0),\n                                    y: anchor.y.clamped(to: 0.0...1.0))\n        \n        let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width\n        let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height\n        let r = CGRect(x: x, y: y, width: size.width, height: size.height)\n        \n        let ori = CGRect(origin: .zero, size: base)\n        return ori.intersection(r)\n    }\n    \n    private var aspectRatio: CGFloat {\n        return base.height == 0.0 ? 1.0 : base.width / base.height\n    }\n}\n\nextension CGRect {\n    func scaled(_ scale: CGFloat) -> CGRect {\n        return CGRect(x: origin.x * scale, y: origin.y * scale,\n                      width: size.width * scale, height: size.height * scale)\n    }\n}\n\nextension Comparable {\n    func clamped(to limits: ClosedRange<Self>) -> Self {\n        return min(max(self, limits.lowerBound), limits.upperBound)\n    }\n}\n"
  },
  {
    "path": "Sources/Utility/String+SHA256.swift",
    "content": "//\n//  String+SHA256.swift\n//  Kingfisher\n//\n//  Created by kaimaschke on 28.07.23.\n//\n//  Copyright (c) 2023 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\nimport CryptoKit\nimport CommonCrypto\n\nextension String: KingfisherCompatibleValue { }\nextension KingfisherWrapper where Base == String {\n    var sha256: String {\n        guard let data = base.data(using: .utf8) else { return base }\n        if #available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, macCatalyst 13.0, *) {\n            let hashed = SHA256.hash(data: data)\n            return hashed.compactMap { String(format: \"%02x\", $0) }.joined()\n        } else {\n            var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))\n            data.withUnsafeBytes { bytes in\n                _ = CC_SHA256(bytes.baseAddress, UInt32(data.count), &digest)\n            }\n            return digest.makeIterator().compactMap { String(format: \"%02x\", $0) }.joined()\n        }\n    }\n\n    var ext: String? {\n        guard let firstSeg = base.split(separator: \"@\").first else {\n            return nil\n        }\n\n        var ext = \"\"\n        if let index = firstSeg.lastIndex(of: \".\") {\n            let extRange = firstSeg.index(index, offsetBy: 1)..<firstSeg.endIndex\n            ext = String(firstSeg[extRange])\n        }\n        return ext.count > 0 ? ext : nil\n    }\n}\n"
  },
  {
    "path": "Sources/Views/AnimatedImageView.swift",
    "content": "//\n//  AnimatedImageView.swift\n//  Kingfisher\n//\n//  Created by bl4ckra1sond3tre on 4/22/16.\n//\n//  The AnimatedImageView, AnimatedFrame and Animator is a modified version of \n//  some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu)\n//\n//  The MIT License (MIT)\n//\n//  Copyright (c) 2019 Reda Lemeden.\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy of\n//  this software and associated documentation files (the \"Software\"), to deal in\n//  the Software without restriction, including without limitation the rights to\n//  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n//  the Software, and to permit persons to whom the Software is furnished to do so,\n//  subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in all\n//  copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n//  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n//  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n//  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n//  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n//\n//  The name and characters used in the demo of this software are property of their\n//  respective owners.\n\n#if !os(watchOS)\n#if canImport(UIKit)\nimport UIKit\nimport ImageIO\ntypealias KFCrossPlatformContentMode = UIView.ContentMode\n#elseif canImport(AppKit)\nimport AppKit\ntypealias KFCrossPlatformContentMode = NSImageScaling\n#endif\n\n/// Delegate of the ``AnimatedImageView``.\n///\n/// It reports back some events of the animated image view.\npublic protocol AnimatedImageViewDelegate: AnyObject {\n\n    /// Called after the ``AnimatedImageView`` has finished each animation loop.\n    ///\n    /// - Parameters:\n    ///   - imageView: The ``AnimatedImageView`` that is being animated.\n    ///   - count: The loop count.\n    func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt)\n\n    /// Called after the ``AnimatedImageView`` has reached the maximum repeat count.\n    ///\n    /// - Parameter imageView: The ``AnimatedImageView`` that is being animated.\n    func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView)\n}\n\nextension AnimatedImageViewDelegate {\n    public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {}\n    public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {}\n}\n\nlet KFRunLoopModeCommon = RunLoop.Mode.common\n\n/// Represents a subclass of `UIImageView` for displaying animated images.\n///\n/// Different from showing an animated image in a normal `UIImageView` (which loads all frames at one time),\n/// ``AnimatedImageView`` only tries to load several frames (defined by ``AnimatedImageView/framePreloadCount``) to\n/// reduce memory usage. It provides a tradeoff between memory usage and CPU time. If you have a memory issue when\n/// using a normal image view to load GIF data, you could give this class a try.\n///\n/// Kingfisher supports setting GIF animated data to either `UIImageView` or ``AnimatedImageView`` out of the box. So\n/// it would be fairly easy to switch between them.\nopen class AnimatedImageView: KFCrossPlatformImageView {\n    /// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`.\n    class TargetProxy {\n        private weak var target: AnimatedImageView?\n        \n        init(target: AnimatedImageView) {\n            self.target = target\n        }\n        \n        @MainActor @objc func onScreenUpdate() {\n            target?.updateFrameIfNeeded()\n        }\n    }\n\n    /// An enumeration that specifies the repeat count of a GIF.\n    public enum RepeatCount: Equatable {\n        /// The animated image should be only played once.\n        case once\n        /// The animated image should be played by a finite times defined in the associated value.\n        case finite(count: UInt)\n        /// The animated image should be played infinitely.\n        case infinite\n\n        public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool {\n            switch (lhs, rhs) {\n            case let (.finite(l), .finite(r)):\n                return l == r\n            case (.once, .once),\n                 (.infinite, .infinite):\n                return true\n            case (.once, .finite(let count)),\n                 (.finite(let count), .once):\n                return count == 1\n            case (.once, _),\n                 (.infinite, _),\n                 (.finite, _):\n                return false\n            }\n        }\n    }\n    \n    // MARK: - Public property\n    /// Whether to automatically play the animation when the view becomes visible. \n    ///\n    /// The default is `true`.\n    public var autoPlayAnimatedImage = true\n    \n    /// The count of frames that should be preloaded before being shown.\n    public var framePreloadCount = 10\n    \n    /// Specifies whether the GIF frames should be pre-scaled to the image view's size or not.\n    ///\n    /// If the downloaded image is larger than the image view's size, it will help reduce some memory usage.\n    ///\n    /// The default is `true`.\n    public var needsPrescaling = true\n\n    /// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen\n    /// rendering to extract pixel information in background. This can reduce the main thread CPU usage.\n    ///\n    @available(*, deprecated, message: \"\"\"\n        This property does not perform as declared and may lead to performance degradation.\n        It is currently obsolete and scheduled for removal in a future version.\n    \"\"\")\n    public var backgroundDecode = true\n\n    /// The animation timer's run loop mode. The default is `RunLoop.Mode.common`.\n    ///\n    /// Setting this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling.\n    public var runLoopMode = KFRunLoopModeCommon {\n        willSet {\n            guard runLoopMode != newValue else { return }\n            stopAnimating()\n            displayLink.remove(from: .main, forMode: runLoopMode)\n            displayLink.add(to: .main, forMode: newValue)\n            startAnimating()\n        }\n    }\n    \n    /// The repeat count. The animated image will keep animating until the loop count reaches this value.\n    ///\n    /// Setting this value to another one will reset the current animation.\n    ///\n    /// The default is ``RepeatCount/infinite``, which means the animation will last forever.\n    public var repeatCount = RepeatCount.infinite {\n        didSet {\n            if oldValue != repeatCount {\n                reset()\n                #if os(macOS)\n                needsDisplay = true\n                layer?.setNeedsDisplay()\n                #else\n                setNeedsDisplay()\n                layer.setNeedsDisplay()\n                #endif\n            }\n        }\n    }\n\n    /// The delegate of this `AnimatedImageView` object. \n    ///\n    /// See the ``AnimatedImageViewDelegate`` protocol for more information.\n    public weak var delegate: (any AnimatedImageViewDelegate)?\n\n    /// The ``Animator`` instance that holds the frames of a specific image in memory.\n    public private(set) var animator: Animator?\n\n    /// Purges decoded frame images from the internal buffer.\n    ///\n    /// By default, this keeps the current frame (to avoid a potential blink) while removing other preloaded frames.\n    /// You can call this manually on any platform.\n    public func purgeFrames(keepCurrentFrame: Bool = true) {\n        animator?.purgeFrames(keepCurrentFrame: keepCurrentFrame)\n    }\n\n#if os(iOS)\n    /// Whether the animated frame buffer should be purged when the app enters background.\n    ///\n    /// This is an opt-in behavior to reduce memory footprint when your app is in background. When enabled,\n    /// `AnimatedImageView` stops animating and purges preloaded frames on\n    /// `UIApplication.didEnterBackgroundNotification`. If the view was animating before entering background, it will\n    /// prepare frames and resume animation on `UIApplication.willEnterForegroundNotification`.\n    ///\n    /// Default is `false`.\n    public var purgeFramesOnBackground: Bool = false {\n        didSet { updateBackgroundFramePurgeObserversIfNeeded() }\n    }\n\n    private var isBackgroundFramePurgeObserversAdded: Bool = false\n    private var shouldResumeAnimationAfterForeground: Bool = false\n#endif\n\n    // MARK: - Private property\n    // Dispatch queue used for preloading images.\n    private lazy var preloadQueue: DispatchQueue = {\n        return DispatchQueue(label: \"com.onevcat.Kingfisher.Animator.preloadQueue\")\n    }()\n    \n    // A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy.\n    private var isDisplayLinkInitialized: Bool = false\n    \n    // A display link that keeps calling the `updateFrame` method on every screen refresh.\n    private lazy var displayLink: any DisplayLinkCompatible = {\n        isDisplayLinkInitialized = true\n        let displayLink = self.compatibleDisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))\n        displayLink.add(to: .main, forMode: runLoopMode)\n        displayLink.isPaused = true\n        return displayLink\n    }()\n    \n    // MARK: - Override\n    @MainActor\n    override open var image: KFCrossPlatformImage? {\n        didSet {\n            if image != oldValue {\n                reset()\n            }\n            #if os(macOS)\n            needsDisplay = true\n            layer?.setNeedsDisplay()\n            #else\n            setNeedsDisplay()\n            layer.setNeedsDisplay()\n            #endif\n        }\n    }\n    \n    open override var isHighlighted: Bool {\n        get {\n            super.isHighlighted\n        }\n        set {\n            // Highlighted image is unsupported for animated images.\n            // See https://github.com/onevcat/Kingfisher/issues/1679\n            if displayLink.isPaused {\n                super.isHighlighted = newValue\n            }\n        }\n    }\n\n// Workaround for Apple xcframework creating issue on Apple TV in Swift 5.8.\n// https://github.com/swiftlang/swift/issues/66015\n#if os(tvOS)\n    public override init(image: UIImage?, highlightedImage: UIImage?) {\n        super.init(image: image, highlightedImage: highlightedImage)\n    }\n    \n    required public init?(coder: NSCoder) {\n        super.init(coder: coder)\n    }\n    \n    init() {\n        super.init(frame: .zero)\n    }\n#endif\n    \n    deinit {\n        // `@MainActor deinit` requires isolated deinit support and broke older Swift 6 toolchains.\n        // Keep a single code path that assumes UIKit/AppKit deallocation on the main thread.\n        MainActor.runUnsafely {\n            #if os(iOS)\n            removeBackgroundFramePurgeObservers()\n            #endif\n\n            if isDisplayLinkInitialized {\n                displayLink.invalidate()\n            }\n        }\n    }\n    \n#if os(macOS)\n    public override init(frame frameRect: NSRect) {\n        super.init(frame: frameRect)\n        commonInit()\n    }\n    \n    public required init?(coder: NSCoder) {\n        super.init(coder: coder)\n        commonInit()\n    }\n    \n    private func commonInit() {\n        super.animates = false\n        wantsLayer = true\n    }\n    \n    open override var animates: Bool {\n        get {\n            if isDisplayLinkInitialized {\n                return !displayLink.isPaused\n            } else {\n                return super.animates\n            }\n        }\n        set {\n            if newValue {\n                startAnimating()\n            } else {\n                stopAnimating()\n            }\n        }\n    }\n    \n    open func startAnimating() {\n        guard let animator = animator else { return }\n        guard !animator.isReachMaxRepeatCount else { return }\n\n        displayLink.isPaused = false\n    }\n    \n    open func stopAnimating() {\n        if isDisplayLinkInitialized {\n            displayLink.isPaused = true\n        }\n    }\n    \n    open override var wantsUpdateLayer: Bool {\n        return true\n    }\n    \n    open override func updateLayer() {\n        if let frame = animator?.currentFrameImage ?? currentFrame, let layer = layer {\n            layer.contents = frame.kf.cgImage\n            layer.contentsScale = frame.kf.scale\n            layer.contentsGravity = determineContentsGravity(for: frame)\n            currentFrame = frame\n        }\n    }\n    \n    private func determineContentsGravity(for image: NSImage) -> CALayerContentsGravity {\n        switch imageScaling {\n            case .scaleProportionallyDown:\n                if image.size.width > bounds.width || image.size.height > bounds.height {\n                    return .resizeAspect\n                } else {\n                    return .center\n                }\n            case .scaleProportionallyUpOrDown:\n                return .resizeAspect\n            case .scaleAxesIndependently:\n                return .resize\n            case .scaleNone:\n                return .center\n            default:\n                return .resizeAspect\n        }\n    }\n    \n    open override func viewDidMoveToWindow() {\n        super.viewDidMoveToWindow()\n        didMove()\n    }\n    \n    open override func viewDidMoveToSuperview() {\n        super.viewDidMoveToSuperview()\n        didMove()\n    }\n#else\n    override open var isAnimating: Bool {\n        if isDisplayLinkInitialized {\n            return !displayLink.isPaused\n        } else {\n            return super.isAnimating\n        }\n    }\n    \n    override open func startAnimating() {\n        guard !isAnimating else { return }\n        guard let animator = animator else { return }\n        guard !animator.isReachMaxRepeatCount else { return }\n\n        displayLink.isPaused = false\n    }\n    \n    override open func stopAnimating() {\n        super.stopAnimating()\n        if isDisplayLinkInitialized {\n            displayLink.isPaused = true\n        }\n    }\n    \n    override open func display(_ layer: CALayer) {\n        layer.contents = animator?.currentFrameImage?.cgImage ?? image?.cgImage\n    }\n    \n    override open func didMoveToWindow() {\n        super.didMoveToWindow()\n        didMove()\n    }\n    \n    override open func didMoveToSuperview() {\n        super.didMoveToSuperview()\n        didMove()\n    }\n#endif\n\n    // This is for back compatibility that using regular `UIImageView` to show animated image.\n    override func shouldPreloadAllAnimation() -> Bool {\n        return false\n    }\n\n    // Reset the animator.\n    private func reset() {\n        animator = nil\n        currentFrame = nil\n        if let image = image, let frameSource = image.kf.frameSource {\n            #if os(visionOS)\n            let scale = UITraitCollection.current.displayScale\n            #elseif os(macOS)\n            let scale = image.recommendedLayerContentsScale(window?.backingScaleFactor ?? 0.0)\n            let contentMode = imageScaling\n            #else\n            var scale: CGFloat = 0\n            if #available(iOS 13.0, tvOS 13.0, *) {\n                scale = UITraitCollection.current.displayScale\n            } else {\n                scale = UIScreen.main.scale\n            }\n            #endif\n            currentFrame = image\n            let targetSize = bounds.scaled(scale).size\n            let animator = Animator(\n                frameSource: frameSource,\n                contentMode: contentMode,\n                size: targetSize,\n                imageSize: image.kf.size,\n                imageScale: image.kf.scale,\n                framePreloadCount: framePreloadCount,\n                repeatCount: repeatCount,\n                preloadQueue: preloadQueue)\n            animator.delegate = self\n            animator.needsPrescaling = needsPrescaling\n            animator.prepareFramesAsynchronously()\n            self.animator = animator\n        }\n        didMove()\n    }\n    \n    private func didMove() {\n        if autoPlayAnimatedImage && animator != nil {\n            if let _ = superview, let _ = window {\n                startAnimating()\n            } else {\n                stopAnimating()\n            }\n        }\n    }\n\n#if os(iOS)\n    private func updateBackgroundFramePurgeObserversIfNeeded() {\n        if purgeFramesOnBackground {\n            guard !isBackgroundFramePurgeObserversAdded else { return }\n            isBackgroundFramePurgeObserversAdded = true\n\n            let center = NotificationCenter.default\n            center.addObserver(\n                self,\n                selector: #selector(didEnterBackgroundNotification(_:)),\n                name: UIApplication.didEnterBackgroundNotification,\n                object: nil\n            )\n            center.addObserver(\n                self,\n                selector: #selector(willEnterForegroundNotification(_:)),\n                name: UIApplication.willEnterForegroundNotification,\n                object: nil\n            )\n        } else {\n            removeBackgroundFramePurgeObservers()\n        }\n    }\n\n    private func removeBackgroundFramePurgeObservers() {\n        guard isBackgroundFramePurgeObserversAdded else { return }\n        let center = NotificationCenter.default\n        center.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)\n        center.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)\n        isBackgroundFramePurgeObserversAdded = false\n    }\n\n    @objc private func didEnterBackgroundNotification(_ notification: Notification) {\n        handleDidEnterBackground()\n    }\n\n    @objc private func willEnterForegroundNotification(_ notification: Notification) {\n        handleWillEnterForeground()\n    }\n\n    private func handleDidEnterBackground() {\n        guard purgeFramesOnBackground else { return }\n        shouldResumeAnimationAfterForeground = isAnimating\n        stopAnimating()\n        purgeFrames(keepCurrentFrame: true)\n    }\n\n    private func handleWillEnterForeground() {\n        guard purgeFramesOnBackground else { return }\n        guard shouldResumeAnimationAfterForeground else { return }\n        shouldResumeAnimationAfterForeground = false\n        guard animator != nil, superview != nil, window != nil else { return }\n        animator?.prepareFramesAsynchronously()\n        startAnimating()\n    }\n#endif\n    \n    /// If the Animator cannot prepare the next frame in time, `animator.currentFrameImage` will return nil.\n    /// To prevent unexpected blinking in the ImageView, we maintain a cache of the currently displayed frame\n    /// to use as a fallback in such scenarios.\n    private var currentFrame: KFCrossPlatformImage?\n    \n    /// Update the current frame with the displayLink duration.\n    @MainActor\n    private func updateFrameIfNeeded() {\n        guard let animator = animator else {\n            return\n        }\n\n        guard !animator.isFinished else {\n            stopAnimating()\n            delegate?.animatedImageViewDidFinishAnimating(self)\n            return\n        }\n\n        let duration: CFTimeInterval\n\n        // CA based display link is opt-out from ProMotion by default.\n        // So the duration and its FPS might not match. \n        // See [#718](https://github.com/onevcat/Kingfisher/issues/718)\n        // By setting CADisableMinimumFrameDuration to YES in Info.plist may\n        // cause the preferredFramesPerSecond being 0\n        let preferredFramesPerSecond = displayLink.preferredFramesPerSecond\n        if preferredFramesPerSecond == 0 {\n            duration = displayLink.duration\n        } else {\n            // Some devices (like iPad Pro 10.5) will have a different FPS.\n            duration = 1.0 / TimeInterval(preferredFramesPerSecond)\n        }\n        \n        if animator.shouldChangeFrame(with: duration) {\n            #if os(macOS)\n            layer?.setNeedsDisplay()\n            #else\n            layer.setNeedsDisplay()\n            #endif\n        }\n    }\n}\n\n@MainActor\nprotocol AnimatorDelegate: AnyObject {\n    func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt)\n}\n\nextension AnimatedImageView: AnimatorDelegate {\n    func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) {\n        delegate?.animatedImageView(self, didPlayAnimationLoops: count)\n    }\n}\n\nextension AnimatedImageView {\n\n    // Represents a single frame in a GIF.\n    struct AnimatedFrame {\n\n        // The image to display for this frame. Its value is nil when the frame is removed from the buffer.\n        let image: KFCrossPlatformImage?\n\n        // The duration that this frame should remain active.\n        let duration: TimeInterval\n\n        // A placeholder frame with no image assigned.\n        // Used to replace frames that are no longer needed in the animation.\n        var placeholderFrame: AnimatedFrame {\n            return AnimatedFrame(image: nil, duration: duration)\n        }\n\n        // Whether this frame instance contains an image or not.\n        var isPlaceholder: Bool {\n            return image == nil\n        }\n\n        // Returns a new instance from an optional image.\n        //\n        // - parameter image: An optional `UIImage` instance to be assigned to the new frame.\n        // - returns: An `AnimatedFrame` instance.\n        func makeAnimatedFrame(image: KFCrossPlatformImage?) -> AnimatedFrame {\n            return AnimatedFrame(image: image, duration: duration)\n        }\n    }\n}\n\nextension AnimatedImageView {\n\n    // MARK: - Animator\n\n    // TODO: Check the thread-safety of `Animator` for Sendable again.\n    /// An animator which is used to drive the data behind ``AnimatedImageView``.\n    public class Animator: @unchecked Sendable {\n        private let size: CGSize\n\n        private let imageSize: CGSize\n        private let imageScale: CGFloat\n\n        /// The maximum count of image frames that need to be preloaded.\n        public let maxFrameCount: Int\n\n        private let frameSource: any ImageFrameSource\n        private let maxRepeatCount: RepeatCount\n\n        private let maxTimeStep: TimeInterval = 1.0\n        private let animatedFrames = SafeArray<AnimatedFrame>()\n        private var frameCount = 0\n        private var timeSinceLastFrameChange: TimeInterval = 0.0\n        private var currentRepeatCount: UInt = 0\n\n        var isFinished: Bool = false\n\n        var needsPrescaling = true\n\n        weak var delegate: (any AnimatorDelegate)?\n\n        // Total duration of one animation loop\n        var loopDuration: TimeInterval = 0\n\n        /// The image of the current frame.\n        public var currentFrameImage: KFCrossPlatformImage? {\n            return frame(at: currentFrameIndex)\n        }\n\n        /// The duration of the current active frame.\n        public var currentFrameDuration: TimeInterval {\n            return duration(at: currentFrameIndex)\n        }\n\n        /// The index of the current animation frame.\n        public internal(set) var currentFrameIndex = 0 {\n            didSet {\n                previousFrameIndex = oldValue\n            }\n        }\n\n        var previousFrameIndex = 0 {\n            didSet {\n                preloadQueue.async {\n                    self.updatePreloadedFrames()\n                }\n            }\n        }\n\n        var isReachMaxRepeatCount: Bool {\n            switch maxRepeatCount {\n            case .once:\n                return currentRepeatCount >= 1\n            case .finite(let maxCount):\n                return currentRepeatCount >= maxCount\n            case .infinite:\n                return false\n            }\n        }\n\n        /// Whether the current frame is the last frame or not in the animation sequence.\n        public var isLastFrame: Bool {\n            return currentFrameIndex == frameCount - 1\n        }\n\n        var preloadingIsNeeded: Bool {\n            return maxFrameCount < frameCount - 1\n        }\n\n        #if os(macOS)\n        var contentMode = NSImageScaling.scaleAxesIndependently\n        #else\n        var contentMode = UIView.ContentMode.scaleToFill\n        #endif\n\n        private lazy var preloadQueue: DispatchQueue = {\n            return DispatchQueue(label: \"com.onevcat.Kingfisher.Animator.preloadQueue\")\n        }()\n\n        /// Creates an animator with image source reference.\n        ///\n        /// - Parameters:\n        ///   - source: The reference of animated image.\n        ///   - mode: Content mode of the `AnimatedImageView`.\n        ///   - size: Size of the `AnimatedImageView`.\n        ///   - imageSize: Size of the `KingfisherWrapper`.\n        ///   - imageScale: Scale of the `KingfisherWrapper`.\n        ///   - count: Count of frames needed to be preloaded.\n        ///   - repeatCount: The repeat count should this animator uses.\n        ///   - preloadQueue: Dispatch queue used for preloading images.\n        convenience init(imageSource source: CGImageSource,\n                         contentMode mode: KFCrossPlatformContentMode,\n                         size: CGSize,\n                         imageSize: CGSize,\n                         imageScale: CGFloat,\n                         framePreloadCount count: Int,\n                         repeatCount: RepeatCount,\n                         preloadQueue: DispatchQueue) {\n            let frameSource = CGImageFrameSource(data: nil, imageSource: source, options: nil)\n            self.init(frameSource: frameSource,\n                      contentMode: mode,\n                      size: size,\n                      imageSize: imageSize,\n                      imageScale: imageScale,\n                      framePreloadCount: count,\n                      repeatCount: repeatCount,\n                      preloadQueue: preloadQueue)\n        }\n        \n        /// Creates an animator with a custom image frame source.\n        ///\n        /// - Parameters:\n        ///   - frameSource: The reference of animated image.\n        ///   - mode: Content mode of the `AnimatedImageView`.\n        ///   - size: Size of the `AnimatedImageView`.\n        ///   - imageSize: Size of the `KingfisherWrapper`.\n        ///   - imageScale: Scale of the `KingfisherWrapper`.\n        ///   - count: Count of frames needed to be preloaded.\n        ///   - repeatCount: The repeat count should this animator uses.\n        ///   - preloadQueue: Dispatch queue used for preloading images.\n        init(frameSource source: any ImageFrameSource,\n             contentMode mode: KFCrossPlatformContentMode,\n             size: CGSize,\n             imageSize: CGSize,\n             imageScale: CGFloat,\n             framePreloadCount count: Int,\n             repeatCount: RepeatCount,\n             preloadQueue: DispatchQueue) {\n            self.frameSource = source.copy()\n            self.contentMode = mode\n            self.size = size\n            self.imageSize = imageSize\n            self.imageScale = imageScale\n            self.maxFrameCount = count\n            self.maxRepeatCount = repeatCount\n            self.preloadQueue = preloadQueue\n        }\n\n        /// Gets the image frame of a given index.\n        /// - Parameter index: The index of the desired image.\n        /// - Returns: The decoded image at the frame. `nil` if the index is out of bounds or the image is not yet loaded.\n        public func frame(at index: Int) -> KFCrossPlatformImage? {\n            return animatedFrames[index]?.image\n        }\n\n        /// Gets the duration of an image for the given frame index.\n        /// - Parameter index: The index of the desired image.\n        /// - Returns: The duration of that frame.\n        public func duration(at index: Int) -> TimeInterval {\n            return animatedFrames[index]?.duration  ?? .infinity\n        }\n\n        func prepareFramesAsynchronously() {\n            frameCount = frameSource.frameCount\n            animatedFrames.reserveCapacity(frameCount)\n            preloadQueue.async { [weak self] in\n                self?.setupAnimatedFrames()\n            }\n        }\n\n        func purgeFrames(keepCurrentFrame: Bool = true) {\n            preloadQueue.async { [weak self] in\n                guard let self else { return }\n                guard self.frameCount > 0, self.animatedFrames.count > 0 else { return }\n\n                let keepIndex = keepCurrentFrame ? self.currentFrameIndex : nil\n                var imagesToRelease: [KFCrossPlatformImage] = []\n\n                for index in 0..<self.frameCount {\n                    if let keepIndex, index == keepIndex { continue }\n                    guard let frame = self.animatedFrames[index], let image = frame.image else { continue }\n                    imagesToRelease.append(image)\n                    self.animatedFrames[index] = frame.placeholderFrame\n                }\n\n                if !imagesToRelease.isEmpty {\n                    // Ensure the image dealloc in main thread.\n                    let imagesToReleaseCopy = imagesToRelease\n                    DispatchQueue.main.async { _ = imagesToReleaseCopy }\n                }\n            }\n        }\n\n        @MainActor \n        func shouldChangeFrame(with duration: CFTimeInterval) -> Bool {\n            incrementTimeSinceLastFrameChange(with: duration)\n\n            if currentFrameDuration > timeSinceLastFrameChange {\n                return false\n            } else {\n                resetTimeSinceLastFrameChange()\n                incrementCurrentFrameIndex()\n                return true\n            }\n        }\n\n        private func setupAnimatedFrames() {\n            resetAnimatedFrames()\n\n            var duration: TimeInterval = 0\n\n            (0..<frameCount).forEach { index in\n                let frameDuration = frameSource.duration(at: index)\n                duration += min(frameDuration, maxTimeStep)\n                animatedFrames.append(AnimatedFrame(image: nil, duration: frameDuration))\n\n                if index > maxFrameCount { return }\n                animatedFrames[index] = animatedFrames[index]?.makeAnimatedFrame(image: loadFrame(at: index))\n            }\n\n            self.loopDuration = duration\n        }\n\n        private func resetAnimatedFrames() {\n            animatedFrames.removeAll()\n        }\n\n        private func loadFrame(at index: Int) -> KFCrossPlatformImage? {\n            let resize = needsPrescaling && size != .zero\n            let maxSize = resize ? size : nil\n            guard let cgImage = frameSource.frame(at: index, maxSize: maxSize) else {\n                return nil\n            }\n            \n            #if os(macOS)\n            return KFCrossPlatformImage(cgImage: cgImage, size: .zero)\n            #else\n            if #available(iOS 15, tvOS 15, *) {\n                // From iOS 15, a plain image loading causes iOS calling `-[_UIImageCGImageContent initWithCGImage:scale:]`\n                // in ImageIO, which holds the image ref on the creating thread.\n                // To get a workaround, create another image ref and use that to create the final image. This leads to\n                // some performance loss, but there is little we can do.\n                // https://github.com/onevcat/Kingfisher/issues/1844\n                // https://github.com/onevcat/Kingfisher/pulls/2194\n                guard let unretainedImage = CGImage.create(ref: cgImage) else {\n                    return KFCrossPlatformImage(cgImage: cgImage)\n                }\n                \n                return KFCrossPlatformImage(cgImage: unretainedImage).preparingForDisplay()\n            } else {\n                return KFCrossPlatformImage(cgImage: cgImage)\n            }\n            #endif\n        }\n        \n        private func updatePreloadedFrames() {\n            guard preloadingIsNeeded else {\n                return\n            }\n\n            let previousFrame = animatedFrames[previousFrameIndex]\n            animatedFrames[previousFrameIndex] = previousFrame?.placeholderFrame\n            // ensure the image dealloc in main thread\n            defer {\n                if let image = previousFrame?.image {\n                    DispatchQueue.main.async {\n                        _ = image\n                    }\n                }\n            }\n\n            preloadIndexes(start: currentFrameIndex).forEach { index in\n                guard let currentAnimatedFrame = animatedFrames[index] else { return }\n                if !currentAnimatedFrame.isPlaceholder { return }\n                animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index))\n            }\n        }\n\n        @MainActor private func incrementCurrentFrameIndex() {\n            let wasLastFrame = isLastFrame\n            currentFrameIndex = increment(frameIndex: currentFrameIndex)\n            if isLastFrame {\n                currentRepeatCount += 1\n                if isReachMaxRepeatCount {\n                    isFinished = true\n\n                    // Notify the delegate here because the animation is stopping.\n                    delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)\n                }\n            } else if wasLastFrame {\n\n                // Notify the delegate that the loop completed\n                delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)\n            }\n        }\n\n        private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) {\n            timeSinceLastFrameChange += min(maxTimeStep, duration)\n        }\n\n        private func resetTimeSinceLastFrameChange() {\n            timeSinceLastFrameChange -= currentFrameDuration\n        }\n\n        private func increment(frameIndex: Int, by value: Int = 1) -> Int {\n            return (frameIndex + value) % frameCount\n        }\n\n        private func preloadIndexes(start index: Int) -> [Int] {\n            let nextIndex = increment(frameIndex: index)\n            let lastIndex = increment(frameIndex: index, by: maxFrameCount)\n\n            if lastIndex >= nextIndex {\n                return [Int](nextIndex...lastIndex)\n            } else {\n                return [Int](nextIndex..<frameCount) + [Int](0...lastIndex)\n            }\n        }\n    }\n}\n\nclass SafeArray<Element> {\n    private var array: Array<Element> = []\n    private let lock = NSLock()\n    \n    subscript(index: Int) -> Element? {\n        get {\n            lock.lock()\n            defer { lock.unlock() }\n            return array.indices ~= index ? array[index] : nil\n        }\n        \n        set {\n            lock.lock()\n            defer { lock.unlock() }\n            if let newValue = newValue, array.indices ~= index {\n                array[index] = newValue\n            }\n        }\n    }\n    \n    var count : Int {\n        lock.lock()\n        defer { lock.unlock() }\n        return array.count\n    }\n    \n    func reserveCapacity(_ count: Int) {\n        lock.lock()\n        defer { lock.unlock() }\n        array.reserveCapacity(count)\n    }\n    \n    func append(_ element: Element) {\n        lock.lock()\n        defer { lock.unlock() }\n        array += [element]\n    }\n    \n    func removeAll() {\n        lock.lock()\n        defer { lock.unlock() }\n        array = []\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Views/Indicator.swift",
    "content": "//\n//  Indicator.swift\n//  Kingfisher\n//\n//  Created by João D. Moreira on 30/08/16.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if !os(watchOS)\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\npublic typealias IndicatorView = NSView\n#else\nimport UIKit\npublic typealias IndicatorView = UIView\n#endif\n\n/// Represents the activity indicator type that should be added to an image view when an image is being downloaded.\npublic enum IndicatorType {\n    \n    /// No indicator.\n    case none\n    \n    /// Uses the system activity indicator.\n    case activity\n    \n    /// Uses an image as an indicator. GIF is supported.\n    case image(imageData: Data)\n    \n    /// Uses a custom indicator.\n    ///\n    /// The type of the associated value should conform to the ``Indicator`` protocol.\n    case custom(indicator: any Indicator)\n}\n\n/// An indicator type which can be used to show that the download task is in progress.\n@MainActor \npublic protocol Indicator: Sendable {\n    \n    /// Called when the indicator should start animating.\n    func startAnimatingView()\n    \n    /// Called when the indicator should stop animating.\n    func stopAnimatingView()\n\n    /// Center offset of the indicator.\n    ///\n    /// Kingfisher will use this value to determine the position of the indicator in the superview.\n    var centerOffset: CGPoint { get }\n    \n    /// The indicator view which would be added to the superview.\n    var view: IndicatorView { get }\n\n    /// The size strategy used when adding the indicator to the image view.\n    /// - Parameter imageView: The superview of the indicator.\n    /// - Returns: An ``IndicatorSizeStrategy`` that determines how the indicator should be sized.\n    func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy\n}\n\n/// The idicator size strategy used when sizing the indicator in the image view.\npublic enum IndicatorSizeStrategy {\n    /// Uses the intrinsic size of the indicator.\n    case intrinsicSize\n    /// Match the size of the super view of the indicator.\n    case full\n    /// Uses the associated `CGSize` to set the indicator size.\n    case size(CGSize)\n}\n\nextension Indicator {\n    \n    /// Default implementation of ``Indicator/centerOffset-7jxdw`` of the ``Indicator``.\n    ///\n    /// The default value is `.zero`, which means that there is no offset for the indicator view.\n    public var centerOffset: CGPoint {\n        .zero\n    }\n\n    /// Default implementation of ``Indicator/sizeStrategy(in:)-5x0b4`` of the ``Indicator``.\n    ///\n    /// The default value is ``IndicatorSizeStrategy/full``, means that the indicator will pin to the same height and\n    /// width as the image view.\n    /// - Parameter imageView: The image view which holds the indicator.\n    /// - Returns: The desired ``IndicatorSizeStrategy``\n    public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy {\n        .full\n    }\n}\n\n// Displays a NSProgressIndicator / UIActivityIndicatorView\n@MainActor\nfinal class ActivityIndicator: Indicator {\n\n    #if os(macOS)\n    private let activityIndicatorView: NSProgressIndicator\n    #else\n    private let activityIndicatorView: UIActivityIndicatorView\n    #endif\n    private var animatingCount = 0\n\n    var view: IndicatorView {\n        return activityIndicatorView\n    }\n\n    func startAnimatingView() {\n        if animatingCount == 0 {\n            #if os(macOS)\n            activityIndicatorView.startAnimation(nil)\n            #else\n            activityIndicatorView.startAnimating()\n            #endif\n            activityIndicatorView.isHidden = false\n        }\n        animatingCount += 1\n    }\n\n    func stopAnimatingView() {\n        animatingCount = max(animatingCount - 1, 0)\n        if animatingCount == 0 {\n            #if os(macOS)\n                activityIndicatorView.stopAnimation(nil)\n            #else\n                activityIndicatorView.stopAnimating()\n            #endif\n            activityIndicatorView.isHidden = true\n        }\n    }\n\n    func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy {\n        return .intrinsicSize\n    }\n\n    init() {\n        #if os(macOS)\n            activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16))\n            activityIndicatorView.controlSize = .regular\n            activityIndicatorView.style = .spinning\n        #else\n            let indicatorStyle: UIActivityIndicatorView.Style\n\n            #if os(tvOS)\n            if #available(tvOS 13.0, *) {\n                indicatorStyle = UIActivityIndicatorView.Style.large\n            } else {\n                indicatorStyle = UIActivityIndicatorView.Style.white\n            }\n            #elseif os(visionOS)\n            indicatorStyle = UIActivityIndicatorView.Style.medium\n            #else\n            if #available(iOS 13.0, * ) {\n                indicatorStyle = UIActivityIndicatorView.Style.medium\n            } else {\n                indicatorStyle = UIActivityIndicatorView.Style.gray\n            }\n            #endif\n\n            activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle)\n        #endif\n    }\n}\n\n#if canImport(UIKit)\nextension UIActivityIndicatorView.Style {\n    #if compiler(>=5.1)\n    #else\n    static let large = UIActivityIndicatorView.Style.white\n    #if !os(tvOS)\n    static let medium = UIActivityIndicatorView.Style.gray\n    #endif\n    #endif\n}\n#endif\n\n// MARK: - ImageIndicator\n// Displays an ImageView. Supports gif\nfinal class ImageIndicator: Indicator {\n    private let animatedImageIndicatorView: KFCrossPlatformImageView\n\n    var view: IndicatorView {\n        return animatedImageIndicatorView\n    }\n\n    init?(\n        imageData data: Data,\n        processor: any ImageProcessor = DefaultImageProcessor.default,\n        options: KingfisherParsedOptionsInfo? = nil)\n    {\n        var options = options ?? KingfisherParsedOptionsInfo(nil)\n        // Use normal image view to show animations, so we need to preload all animation data.\n        if !options.preloadAllAnimationData {\n            options.preloadAllAnimationData = true\n        }\n        \n        guard let image = processor.process(item: .data(data), options: options) else {\n            return nil\n        }\n\n        animatedImageIndicatorView = KFCrossPlatformImageView()\n        animatedImageIndicatorView.image = image\n        \n        #if os(macOS)\n            // Need for gif to animate on macOS\n            animatedImageIndicatorView.imageScaling = .scaleNone\n            animatedImageIndicatorView.canDrawSubviewsIntoLayer = true\n        #else\n            animatedImageIndicatorView.contentMode = .center\n        #endif\n    }\n\n    func startAnimatingView() {\n        #if os(macOS)\n            animatedImageIndicatorView.animates = true\n        #else\n            animatedImageIndicatorView.startAnimating()\n        #endif\n        animatedImageIndicatorView.isHidden = false\n    }\n\n    func stopAnimatingView() {\n        #if os(macOS)\n            animatedImageIndicatorView.animates = false\n        #else\n            animatedImageIndicatorView.stopAnimating()\n        #endif\n        animatedImageIndicatorView.isHidden = true\n    }\n}\n\n#endif\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/LICENSE",
    "content": "Copyright (c) 2012 Luis Solano Bonet\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Categories/NSData+Nocilla.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSHTTPBody.h\"\n\n@interface NSData (Nocilla) <LSHTTPBody>\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Categories/NSData+Nocilla.m",
    "content": "#import \"NSData+Nocilla.h\"\n\n@implementation NSData (Nocilla)\n\n- (NSData *)data {\n    return self;\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Categories/NSString+Nocilla.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSHTTPBody.h\"\n\n@interface NSString (Nocilla) <LSHTTPBody>\n\n- (NSRegularExpression *)regex;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Categories/NSString+Nocilla.m",
    "content": "#import \"NSString+Nocilla.h\"\n\n@implementation NSString (Nocilla)\n\n- (NSRegularExpression *)regex {\n    NSError *error = nil;\n    NSRegularExpression *regex =  [[NSRegularExpression alloc] initWithPattern:self options:0 error:&error];\n    if (error) {\n        [NSException raise:NSInvalidArgumentException format:@\"Invalid regex pattern: %@\\nError: %@\", self, error];\n    }\n    return regex;\n}\n\n- (NSData *)data {\n    return [self dataUsingEncoding:NSUTF8StringEncoding];\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/DSL/LSHTTPRequestDSLRepresentation.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSHTTPRequest.h\"\n\n@interface LSHTTPRequestDSLRepresentation : NSObject\n- (id)initWithRequest:(id<LSHTTPRequest>)request;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/DSL/LSHTTPRequestDSLRepresentation.m",
    "content": "#import \"LSHTTPRequestDSLRepresentation.h\"\n\n@interface LSHTTPRequestDSLRepresentation ()\n@property (nonatomic, strong) id<LSHTTPRequest> request;\n@end\n\n@implementation LSHTTPRequestDSLRepresentation\n- (id)initWithRequest:(id<LSHTTPRequest>)request {\n    self = [super init];\n    if (self) {\n        _request = request;\n    }\n    return self;\n}\n\n- (NSString *)description {\n    NSMutableString *result = [NSMutableString stringWithFormat:@\"stubRequest(@\\\"%@\\\", @\\\"%@\\\")\", self.request.method, [self.request.url absoluteString]];\n    if (self.request.headers.count) {\n        [result appendString:@\".\\nwithHeaders(@{ \"];\n        NSMutableArray *headerElements = [NSMutableArray arrayWithCapacity:self.request.headers.count];\n        \n        NSArray *descriptors = @[[NSSortDescriptor sortDescriptorWithKey:@\"\" ascending:YES]];\n        NSArray * sortedHeaders = [[self.request.headers allKeys] sortedArrayUsingDescriptors:descriptors];\n        \n        for (NSString * header in sortedHeaders) {\n            NSString *value = [self.request.headers objectForKey:header];\n            [headerElements addObject:[NSString stringWithFormat:@\"@\\\"%@\\\": @\\\"%@\\\"\", header, value]];\n        }\n        [result appendString:[headerElements componentsJoinedByString:@\", \"]];\n        [result appendString:@\" })\"];\n    }\n    if (self.request.body.length) {\n        NSString *escapedBody = [[NSString alloc] initWithData:self.request.body encoding:NSUTF8StringEncoding];\n        escapedBody = [escapedBody stringByReplacingOccurrencesOfString:@\"\\\"\" withString:@\"\\\\\\\"\"];\n        [result appendFormat:@\".\\nwithBody(@\\\"%@\\\")\", escapedBody];\n    }\n    return [NSString stringWithFormat:@\"%@;\", result];\n}\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/DSL/LSStubRequestDSL.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"NSString+Matcheable.h\"\n#import \"NSRegularExpression+Matcheable.h\"\n#import \"NSData+Matcheable.h\"\n\n@class LSStubRequestDSL;\n@class LSStubResponseDSL;\n@class LSStubRequest;\n\n@protocol LSHTTPBody;\n\ntypedef LSStubRequestDSL *(^WithHeaderMethod)(NSString *, NSString *);\ntypedef LSStubRequestDSL *(^WithHeadersMethod)(NSDictionary *);\ntypedef LSStubRequestDSL *(^AndBodyMethod)(id<LSMatcheable>);\ntypedef LSStubResponseDSL *(^AndReturnMethod)(NSInteger);\ntypedef LSStubResponseDSL *(^AndReturnRawResponseMethod)(NSData *rawResponseData);\ntypedef void (^AndFailWithErrorMethod)(NSError *error);\n\n@interface LSStubRequestDSL : NSObject\n- (id)initWithRequest:(LSStubRequest *)request;\n\n@property (nonatomic, strong, readonly) WithHeaderMethod withHeader;\n@property (nonatomic, strong, readonly) WithHeadersMethod withHeaders;\n@property (nonatomic, strong, readonly) AndBodyMethod withBody;\n@property (nonatomic, strong, readonly) AndReturnMethod andReturn;\n@property (nonatomic, strong, readonly) AndReturnRawResponseMethod andReturnRawResponse;\n@property (nonatomic, strong, readonly) AndFailWithErrorMethod andFailWithError;\n\n@end\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n    \nLSStubRequestDSL * stubRequest(NSString *method, id<LSMatcheable> url);\n    \n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/DSL/LSStubRequestDSL.m",
    "content": "#import \"LSStubRequestDSL.h\"\n#import \"LSStubResponseDSL.h\"\n#import \"LSStubRequest.h\"\n#import \"LSNocilla.h\"\n\n@interface LSStubRequestDSL ()\n@property (nonatomic, strong) LSStubRequest *request;\n@end\n\n@implementation LSStubRequestDSL\n\n- (id)initWithRequest:(LSStubRequest *)request {\n    self = [super init];\n    if (self) {\n        _request = request;\n    }\n    return self;\n}\n- (WithHeadersMethod)withHeaders {\n    return ^(NSDictionary *headers) {\n        for (NSString *header in headers) {\n            NSString *value = [headers objectForKey:header];\n            [self.request setHeader:header value:value];\n        }\n        return self;\n    };\n}\n\n- (WithHeaderMethod)withHeader {\n    return ^(NSString * header, NSString * value) {\n        [self.request setHeader:header value:value];\n        return self;\n    };\n}\n\n- (AndBodyMethod)withBody {\n    return ^(id<LSMatcheable> body) {\n        self.request.body = body.matcher;\n        return self;\n    };\n}\n\n- (AndReturnMethod)andReturn {\n    return ^(NSInteger statusCode) {\n        self.request.response = [[LSStubResponse alloc] initWithStatusCode:statusCode];\n        LSStubResponseDSL *responseDSL = [[LSStubResponseDSL alloc] initWithResponse:self.request.response];\n        return responseDSL;\n    };\n}\n\n- (AndReturnRawResponseMethod)andReturnRawResponse {\n    return ^(NSData *rawResponseData) {\n        self.request.response = [[LSStubResponse alloc] initWithRawResponse:rawResponseData];\n        LSStubResponseDSL *responseDSL = [[LSStubResponseDSL alloc] initWithResponse:self.request.response];\n        return responseDSL;\n    };\n}\n\n- (AndFailWithErrorMethod)andFailWithError {\n    return ^(NSError *error) {\n        self.request.response = [[LSStubResponse alloc] initWithError:error];\n    };\n}\n\n@end\n\nLSStubRequestDSL * stubRequest(NSString *method, id<LSMatcheable> url) {\n    LSStubRequest *request = [[LSStubRequest alloc] initWithMethod:method urlMatcher:url.matcher];\n    LSStubRequestDSL *dsl = [[LSStubRequestDSL alloc] initWithRequest:request];\n    [[LSNocilla sharedInstance] addStubbedRequest:request];\n    return dsl;\n}\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/DSL/LSStubResponseDSL.h",
    "content": "#import <Foundation/Foundation.h>\n\n@class LSStubResponse;\n@class LSStubResponseDSL;\n\n@protocol LSHTTPBody;\n\ntypedef LSStubResponseDSL *(^ResponseWithBodyMethod)(id<LSHTTPBody>);\ntypedef LSStubResponseDSL *(^ResponseWithHeaderMethod)(NSString *, NSString *);\ntypedef LSStubResponseDSL *(^ResponseWithHeadersMethod)(NSDictionary *);\ntypedef LSStubResponseDSL *(^ResponseVoidMethod)(void);\n\n@interface LSStubResponseDSL : NSObject\n- (id)initWithResponse:(LSStubResponse *)response;\n\n@property (nonatomic, strong, readonly) ResponseWithHeaderMethod withHeader;\n@property (nonatomic, strong, readonly) ResponseWithHeadersMethod withHeaders;\n@property (nonatomic, strong, readonly) ResponseWithBodyMethod withBody;\n\n@property (nonatomic, strong, readonly) ResponseVoidMethod delay;\n@property (nonatomic, strong, readonly) ResponseVoidMethod go;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/DSL/LSStubResponseDSL.m",
    "content": "#import \"LSStubResponseDSL.h\"\n#import \"LSStubResponse.h\"\n#import \"LSHTTPBody.h\"\n\n@interface LSStubResponseDSL ()\n@property (nonatomic, strong) LSStubResponse *response;\n@end\n\n@implementation LSStubResponseDSL\n- (id)initWithResponse:(LSStubResponse *)response {\n    self = [super init];\n    if (self) {\n        _response = response;\n    }\n    return self;\n}\n- (ResponseWithHeaderMethod)withHeader {\n    return ^(NSString * header, NSString * value) {\n        [self.response setHeader:header value:value];\n        return self;\n    };\n}\n\n- (ResponseWithHeadersMethod)withHeaders; {\n    return ^(NSDictionary *headers) {\n        for (NSString *header in headers) {\n            NSString *value = [headers objectForKey:header];\n            [self.response setHeader:header value:value];\n        }\n        return self;\n    };\n}\n\n- (ResponseWithBodyMethod)withBody {\n    return ^(id<LSHTTPBody> body) {\n        self.response.body = [body data];\n        return self;\n    };\n}\n\n- (ResponseVoidMethod)delay {\n    return ^{\n        [self.response delay];\n        return self;\n    };\n}\n\n- (ResponseVoidMethod)go {\n    return ^{\n        [self.response go];\n        return self;\n    };\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Diff/LSHTTPRequestDiff.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSHTTPRequest.h\"\n\n@interface LSHTTPRequestDiff : NSObject\n@property (nonatomic, assign, readonly, getter = isEmpty) BOOL empty;\n\n- (id)initWithRequest:(id<LSHTTPRequest>)oneRequest andRequest:(id<LSHTTPRequest>)anotherRequest;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Diff/LSHTTPRequestDiff.m",
    "content": "#import \"LSHTTPRequestDiff.h\"\n\n@interface LSHTTPRequestDiff ()\n@property (nonatomic, strong) id<LSHTTPRequest>oneRequest;\n@property (nonatomic, strong) id<LSHTTPRequest>anotherRequest;\n\n- (BOOL)isMethodDifferent;\n- (BOOL)isUrlDifferent;\n- (BOOL)areHeadersDifferent;\n- (BOOL)isBodyDifferent;\n\n- (void)appendMethodDiff:(NSMutableString *)diff;\n- (void)appendUrlDiff:(NSMutableString *)diff;\n- (void)appendHeadersDiff:(NSMutableString *)diff;\n- (void)appendBodyDiff:(NSMutableString *)diff;\n@end\n\n@implementation LSHTTPRequestDiff\n- (id)initWithRequest:(id<LSHTTPRequest>)oneRequest andRequest:(id<LSHTTPRequest>)anotherRequest {\n    self = [super init];\n    if (self) {\n        _oneRequest = oneRequest;\n        _anotherRequest = anotherRequest;\n    }\n    return self;\n}\n\n- (BOOL)isEmpty {\n    if ([self isMethodDifferent] ||\n        [self isUrlDifferent] ||\n        [self areHeadersDifferent] ||\n        [self isBodyDifferent]) {\n        return NO;\n    }\n    return YES;\n}\n\n- (NSString *)description {\n    NSMutableString *diff = [@\"\" mutableCopy];\n    if ([self isMethodDifferent]) {\n        [self appendMethodDiff:diff];\n    }\n    if ([self isUrlDifferent]) {\n        [self appendUrlDiff:diff];\n    }\n    if([self areHeadersDifferent]) {\n        [self appendHeadersDiff:diff];\n    }\n    if([self isBodyDifferent]) {\n        [self appendBodyDiff:diff];\n    }\n    return [NSString stringWithString:diff];\n}\n\n#pragma mark - Private Methods\n- (BOOL)isMethodDifferent {\n    return ![self.oneRequest.method isEqualToString:self.anotherRequest.method];\n}\n\n- (BOOL)isUrlDifferent {\n    return ![self.oneRequest.url isEqual:self.anotherRequest.url];\n}\n\n- (BOOL)areHeadersDifferent {\n    return ![self.oneRequest.headers isEqual:self.anotherRequest.headers];\n}\n\n- (BOOL)isBodyDifferent {\n    return (((self.oneRequest.body) && (![self.oneRequest.body isEqual:self.anotherRequest.body])) ||\n            ((self.anotherRequest.body) && (![self.anotherRequest.body isEqual:self.oneRequest.body])));\n}\n\n- (void)appendMethodDiff:(NSMutableString *)diff {\n    [diff appendFormat:@\"- Method: %@\\n+ Method: %@\\n\", self.oneRequest.method, self.anotherRequest.method];\n}\n\n- (void)appendUrlDiff:(NSMutableString *)diff {\n    [diff appendFormat:@\"- URL: %@\\n+ URL: %@\\n\", [self.oneRequest.url absoluteString], [self.anotherRequest.url absoluteString]];\n}\n\n- (void)appendHeadersDiff:(NSMutableString *)diff {\n    [diff appendString:@\"  Headers:\\n\"];\n    NSSet *headersInOneButNotInTheOther = [self.oneRequest.headers keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {\n        return ![self.anotherRequest.headers objectForKey:key] || ![obj isEqual:[self.anotherRequest.headers objectForKey:key]];\n    }];\n    NSSet *headersInTheOtherButNotInOne = [self.anotherRequest.headers keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {\n        return ![self.oneRequest.headers objectForKey:key] || ![obj isEqual:[self.oneRequest.headers objectForKey:key]];\n    }];\n    \n    NSArray *descriptors = @[[NSSortDescriptor sortDescriptorWithKey:@\"\" ascending:YES]];\n    NSArray * sortedHeadersInOneButNotInTheOther = [headersInOneButNotInTheOther sortedArrayUsingDescriptors:descriptors];\n    NSArray * sortedHeadersInTheOtherButNotInOne = [headersInTheOtherButNotInOne sortedArrayUsingDescriptors:descriptors];\n    for (NSString *header in sortedHeadersInOneButNotInTheOther) {\n        NSString *value = [self.oneRequest.headers objectForKey:header];\n        [diff appendFormat:@\"-\\t\\\"%@\\\": \\\"%@\\\"\\n\", header, value];\n        \n    }\n    for (NSString *header in sortedHeadersInTheOtherButNotInOne) {\n        NSString *value = [self.anotherRequest.headers objectForKey:header];\n        [diff appendFormat:@\"+\\t\\\"%@\\\": \\\"%@\\\"\\n\", header, value];\n    }\n}\n\n- (void)appendBodyDiff:(NSMutableString *)diff {\n    NSString *oneBody = [[NSString alloc] initWithData:self.oneRequest.body encoding:NSUTF8StringEncoding];\n    if (oneBody.length) {\n        [diff appendFormat:@\"- Body: \\\"%@\\\"\\n\", oneBody];\n    }\n    NSString *anotherBody = [[NSString alloc] initWithData:self.anotherRequest.body encoding:NSUTF8StringEncoding];\n    if (anotherBody.length) {\n        [diff appendFormat:@\"+ Body: \\\"%@\\\"\\n\", anotherBody];\n    }\n}\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/ASIHTTPRequestStub.h",
    "content": "#import <Foundation/Foundation.h>\n\n@interface ASIHTTPRequestStub : NSObject\n- (int)stub_responseStatusCode;\n- (NSData *)stub_responseData;\n- (NSDictionary *)stub_responseHeaders;\n- (void)stub_startRequest;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/ASIHTTPRequestStub.m",
    "content": "#import \"ASIHTTPRequestStub.h\"\n#import \"LSStubResponse.h\"\n#import \"LSNocilla.h\"\n#import \"LSASIHTTPRequestAdapter.h\"\n#import <objc/runtime.h>\n\n@interface ASIHTTPRequestStub ()\n@property (nonatomic, strong) LSStubResponse *stubResponse;\n@end\n\n@interface ASIHTTPRequestStub (Private)\n- (void)failWithError:(NSError *)error;\n- (void)requestFinished;\n- (void)markAsFinished;\n@end\n\nstatic void const * ASIHTTPRequestStubResponseKey = &ASIHTTPRequestStubResponseKey;\n\n@implementation ASIHTTPRequestStub\n\n- (void)setStubResponse:(LSStubResponse *)stubResponse {\n    objc_setAssociatedObject(self, ASIHTTPRequestStubResponseKey, stubResponse, OBJC_ASSOCIATION_RETAIN);\n}\n\n- (LSStubResponse *)stubResponse {\n    return objc_getAssociatedObject(self, ASIHTTPRequestStubResponseKey);\n}\n\n- (int)stub_responseStatusCode {\n    return (int)self.stubResponse.statusCode;\n}\n\n- (NSData *)stub_responseData {\n    return self.stubResponse.body;\n}\n\n- (NSDictionary *)stub_responseHeaders {\n    return self.stubResponse.headers;\n}\n\n- (void)stub_startRequest {\n    self.stubResponse = [[LSNocilla sharedInstance] responseForRequest:[[LSASIHTTPRequestAdapter alloc] initWithASIHTTPRequest:(id)self]];\n\n    if (self.stubResponse.shouldFail) {\n        [self failWithError:self.stubResponse.error];\n    } else {\n        [self requestFinished];\n    }\n    [self markAsFinished];\n}\n\n@end"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/LSASIHTTPRequestAdapter.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSHTTPRequest.h\"\n\n@class ASIHTTPRequest;\n\n@interface LSASIHTTPRequestAdapter : NSObject<LSHTTPRequest>\n\n- (instancetype)initWithASIHTTPRequest:(ASIHTTPRequest *)request;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/LSASIHTTPRequestAdapter.m",
    "content": "#import \"LSASIHTTPRequestAdapter.h\"\n\n@interface ASIHTTPRequest\n\n@property (nonatomic, strong, readonly) NSURL *url;\n@property (nonatomic, strong, readonly) NSString *requestMethod;\n@property (nonatomic, strong, readonly) NSDictionary *requestHeaders;\n@property (nonatomic, strong, readonly) NSData *postBody;\n\n@end\n\n@interface LSASIHTTPRequestAdapter ()\n@property (nonatomic, strong) ASIHTTPRequest *request;\n@end\n\n@implementation LSASIHTTPRequestAdapter\n\n- (instancetype)initWithASIHTTPRequest:(ASIHTTPRequest *)request {\n    self = [super init];\n    if (self) {\n        _request = request;\n    }\n    return self;\n}\n\n- (NSURL *)url {\n    return self.request.url;\n}\n\n- (NSString *)method {\n    return self.request.requestMethod;\n}\n\n- (NSDictionary *)headers {\n    return self.request.requestHeaders;\n}\n\n- (NSData *)body {\n    return self.request.postBody;\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/LSASIHTTPRequestHook.h",
    "content": "#import \"LSHTTPClientHook.h\"\n\n@interface LSASIHTTPRequestHook : LSHTTPClientHook\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/LSASIHTTPRequestHook.m",
    "content": "#import \"LSASIHTTPRequestHook.h\"\n#import \"ASIHTTPRequestStub.h\"\n#import <objc/runtime.h>\n\n@implementation LSASIHTTPRequestHook\n\n- (void)load {\n    if (!NSClassFromString(@\"ASIHTTPRequest\")) return;\n    [self swizzleASIHTTPRequest];\n}\n\n- (void)unload {\n    if (!NSClassFromString(@\"ASIHTTPRequest\")) return;\n    [self swizzleASIHTTPRequest];\n}\n\n#pragma mark - Internal Methods\n\n- (void)swizzleASIHTTPRequest {\n    [self swizzleASIHTTPSelector:NSSelectorFromString(@\"responseStatusCode\") withSelector:@selector(stub_responseStatusCode)];\n    [self swizzleASIHTTPSelector:NSSelectorFromString(@\"responseData\") withSelector:@selector(stub_responseData)];\n    [self swizzleASIHTTPSelector:NSSelectorFromString(@\"responseHeaders\") withSelector:@selector(stub_responseHeaders)];\n    [self swizzleASIHTTPSelector:NSSelectorFromString(@\"startRequest\") withSelector:@selector(stub_startRequest)];\n    [self addMethodToASIHTTPRequest:NSSelectorFromString(@\"stubResponse\")];\n    [self addMethodToASIHTTPRequest:NSSelectorFromString(@\"setStubResponse:\")];\n}\n\n- (void)swizzleASIHTTPSelector:(SEL)original withSelector:(SEL)stub {\n    Class asiHttpRequest = NSClassFromString(@\"ASIHTTPRequest\");\n    Method originalMethod = class_getInstanceMethod(asiHttpRequest, original);\n    Method stubMethod = class_getInstanceMethod([ASIHTTPRequestStub class], stub);\n    if (!originalMethod || !stubMethod) {\n        [self fail];\n    }\n    method_exchangeImplementations(originalMethod, stubMethod);\n}\n\n- (void)addMethodToASIHTTPRequest:(SEL)newMethod {\n    Method method = class_getInstanceMethod([ASIHTTPRequestStub class], newMethod);\n    const char *types = method_getTypeEncoding(method);\n    class_addMethod(NSClassFromString(@\"ASIHTTPRequest\"), newMethod, class_getMethodImplementation([ASIHTTPRequestStub class], newMethod), types);\n}\n\n- (void)fail {\n    [NSException raise:NSInternalInconsistencyException format:@\"Couldn't load ASIHTTPRequest hook.\"];\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/LSHTTPClientHook.h",
    "content": "#import <Foundation/Foundation.h>\n\n@interface LSHTTPClientHook : NSObject\n- (void)load;\n- (void)unload;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/LSHTTPClientHook.m",
    "content": "#import \"LSHTTPClientHook.h\"\n\n@implementation LSHTTPClientHook\n- (void)load {\n    [NSException raise:NSInternalInconsistencyException\n                format:@\"Method '%@' not implemented. Subclass '%@' and override it\", NSStringFromSelector(_cmd), NSStringFromClass([self class])];\n}\n\n- (void)unload {\n    [NSException raise:NSInternalInconsistencyException\n                format:@\"Method '%@' not implemented. Subclass '%@' and override it\", NSStringFromSelector(_cmd), NSStringFromClass([self class])];\n}\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/LSHTTPStubURLProtocol.h",
    "content": "#import <Foundation/Foundation.h>\n\n@interface LSHTTPStubURLProtocol : NSURLProtocol\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/LSHTTPStubURLProtocol.m",
    "content": "#import \"LSHTTPStubURLProtocol.h\"\n#import \"LSNocilla.h\"\n#import \"NSURLRequest+LSHTTPRequest.h\"\n#import \"LSStubRequest.h\"\n#import \"NSURLRequest+DSL.h\"\n\n@interface NSHTTPURLResponse(UndocumentedInitializer)\n- (id)initWithURL:(NSURL*)URL statusCode:(NSInteger)statusCode headerFields:(NSDictionary*)headerFields requestTime:(double)requestTime;\n@end\n\n@implementation LSHTTPStubURLProtocol\n\n+ (BOOL)canInitWithRequest:(NSURLRequest *)request {\n    return [@[ @\"http\", @\"https\" ] containsObject:request.URL.scheme];\n}\n\n+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {\n\treturn request;\n}\n+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {\n    return NO;\n}\n\n- (void)startLoading {\n    NSURLRequest* request = [self request];\n\tid<NSURLProtocolClient> client = [self client];\n\n    LSStubResponse* stubbedResponse = [[LSNocilla sharedInstance] responseForRequest:request];\n\n    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];\n    [cookieStorage setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:stubbedResponse.headers forURL:request.url]\n                       forURL:request.URL mainDocumentURL:request.URL];\n\n    [stubbedResponse waitForGo];\n\n    if (stubbedResponse.shouldFail) {\n        [client URLProtocol:self didFailWithError:stubbedResponse.error];\n    } else {\n        NSHTTPURLResponse* urlResponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL\n                                                  statusCode:stubbedResponse.statusCode\n                                                headerFields:stubbedResponse.headers\n                                                 requestTime:0];\n\n        if (stubbedResponse.statusCode < 300 || stubbedResponse.statusCode > 399\n            || stubbedResponse.statusCode == 304 || stubbedResponse.statusCode == 305 ) {\n            NSData *body = stubbedResponse.body;\n\n            [client URLProtocol:self didReceiveResponse:urlResponse\n             cacheStoragePolicy:NSURLCacheStorageNotAllowed];\n            [client URLProtocol:self didLoadData:body];\n            [client URLProtocolDidFinishLoading:self];\n        } else {\n\n            NSURL *newURL = [NSURL URLWithString:[stubbedResponse.headers objectForKey:@\"Location\"] relativeToURL:request.URL];\n            NSMutableURLRequest *redirectRequest = [NSMutableURLRequest requestWithURL:newURL];\n\n            [redirectRequest setAllHTTPHeaderFields:[NSHTTPCookie requestHeaderFieldsWithCookies:[cookieStorage cookiesForURL:newURL]]];\n\n            [client URLProtocol:self\n         wasRedirectedToRequest:redirectRequest\n               redirectResponse:urlResponse];\n            // According to: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Listings/CustomHTTPProtocol_Core_Code_CustomHTTPProtocol_m.html\n            // needs to abort the original request\n            [client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];\n\n        }\n    }\n}\n\n- (void)stopLoading {\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/LSNSURLHook.h",
    "content": "#import \"LSHTTPClientHook.h\"\n\n@interface LSNSURLHook : LSHTTPClientHook\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/LSNSURLHook.m",
    "content": "#import \"LSNSURLHook.h\"\n#import \"LSHTTPStubURLProtocol.h\"\n\n@implementation LSNSURLHook\n\n- (void)load {\n    [NSURLProtocol registerClass:[LSHTTPStubURLProtocol class]];\n}\n\n- (void)unload {\n    [NSURLProtocol unregisterClass:[LSHTTPStubURLProtocol class]];\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/NSURLRequest+DSL.h",
    "content": "#import <Foundation/Foundation.h>\n\n@interface NSURLRequest (DSL)\n- (NSString *)toNocillaDSL;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/NSURLRequest+DSL.m",
    "content": "#import \"NSURLRequest+DSL.h\"\n#import \"LSHTTPRequestDSLRepresentation.h\"\n#import \"NSURLRequest+LSHTTPRequest.h\"\n\n@implementation NSURLRequest (DSL)\n- (NSString *)toNocillaDSL {\n    return [[[LSHTTPRequestDSLRepresentation alloc] initWithRequest:self] description];\n}\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/NSURLRequest+LSHTTPRequest.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSHTTPRequest.h\"\n\n@interface NSURLRequest (LSHTTPRequest)<LSHTTPRequest>\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/NSURLRequest+LSHTTPRequest.m",
    "content": "#import \"NSURLRequest+LSHTTPRequest.h\"\n\n@implementation NSURLRequest (LSHTTPRequest)\n\n- (NSURL*)url {\n    return self.URL;\n}\n\n- (NSString *)method {\n    return self.HTTPMethod;\n}\n\n- (NSDictionary *)headers {\n    return self.allHTTPHeaderFields;\n}\n\n- (NSData *)body {\n    if (self.HTTPBodyStream) {\n        NSInputStream *stream = self.HTTPBodyStream;\n        NSMutableData *data = [NSMutableData data];\n        [stream open];\n        size_t bufferSize = 4096;\n        uint8_t *buffer = malloc(bufferSize);\n        if (buffer == NULL) {\n            [NSException raise:@\"NocillaMallocFailure\" format:@\"Could not allocate %zu bytes to read HTTPBodyStream\", bufferSize];\n        }\n        while ([stream hasBytesAvailable]) {\n            NSInteger bytesRead = [stream read:buffer maxLength:bufferSize];\n            if (bytesRead > 0) {\n                NSData *readData = [NSData dataWithBytes:buffer length:bytesRead];\n                [data appendData:readData];\n            } else if (bytesRead < 0) {\n                [NSException raise:@\"NocillaStreamReadError\" format:@\"An error occurred while reading HTTPBodyStream (%ld)\", (long)bytesRead];\n            } else if (bytesRead == 0) {\n                break;\n            }\n        }\n        free(buffer);\n        [stream close];\n\n        return data;\n    }\n\n    return self.HTTPBody;\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLSession/LSNSURLSessionHook.h",
    "content": "//\n//  LSNSURLSessionHook.h\n//  Nocilla\n//\n//  Created by Luis Solano Bonet on 08/01/14.\n//  Copyright (c) 2014 Luis Solano Bonet. All rights reserved.\n//\n\n#import \"Nocilla.h\"\n\n#import \"LSHTTPClientHook.h\"\n\n@interface LSNSURLSessionHook : LSHTTPClientHook\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLSession/LSNSURLSessionHook.m",
    "content": "//\n//  LSNSURLSessionHook.m\n//  Nocilla\n//\n//  Created by Luis Solano Bonet on 08/01/14.\n//  Copyright (c) 2014 Luis Solano Bonet. All rights reserved.\n//\n\n#import \"LSNSURLSessionHook.h\"\n#import \"LSHTTPStubURLProtocol.h\"\n#import <objc/runtime.h>\n\n@implementation LSNSURLSessionHook\n\n- (void)load {\n    Class cls = NSClassFromString(@\"__NSCFURLSessionConfiguration\") ?: NSClassFromString(@\"NSURLSessionConfiguration\");\n    [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];\n}\n\n- (void)unload {\n    Class cls = NSClassFromString(@\"__NSCFURLSessionConfiguration\") ?: NSClassFromString(@\"NSURLSessionConfiguration\");\n    [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];\n}\n\n- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub {\n\n    Method originalMethod = class_getInstanceMethod(original, selector);\n    Method stubMethod = class_getInstanceMethod(stub, selector);\n    if (!originalMethod || !stubMethod) {\n        [NSException raise:NSInternalInconsistencyException format:@\"Couldn't load NSURLSession hook.\"];\n    }\n    method_exchangeImplementations(originalMethod, stubMethod);\n}\n\n- (NSArray *)protocolClasses {\n    return @[[LSHTTPStubURLProtocol class]];\n}\n\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/LSNocilla.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"Nocilla.h\"\n\n@class LSStubRequest;\n@class LSStubResponse;\n@class LSHTTPClientHook;\n@protocol LSHTTPRequest;\n\nextern NSString * const LSUnexpectedRequest;\n\n@interface LSNocilla : NSObject\n+ (LSNocilla *)sharedInstance;\n\n@property (nonatomic, strong, readonly) NSArray *stubbedRequests;\n@property (nonatomic, assign, readonly, getter = isStarted) BOOL started;\n\n- (void)start;\n- (void)stop;\n- (void)addStubbedRequest:(LSStubRequest *)request;\n- (void)clearStubs;\n\n- (void)registerHook:(LSHTTPClientHook *)hook;\n\n- (LSStubResponse *)responseForRequest:(id<LSHTTPRequest>)request;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/LSNocilla.m",
    "content": "#import \"LSNocilla.h\"\n#import \"LSNSURLHook.h\"\n#import \"LSStubRequest.h\"\n#import \"LSHTTPRequestDSLRepresentation.h\"\n#import \"LSASIHTTPRequestHook.h\"\n#import \"LSNSURLSessionHook.h\"\n#import \"LSASIHTTPRequestHook.h\"\n\nNSString * const LSUnexpectedRequest = @\"Unexpected Request\";\n\n@interface LSNocilla ()\n@property (nonatomic, strong) NSMutableArray *mutableRequests;\n@property (nonatomic, strong) NSMutableArray *hooks;\n@property (nonatomic, assign, getter = isStarted) BOOL started;\n\n- (void)loadHooks;\n- (void)unloadHooks;\n@end\n\nstatic LSNocilla *sharedInstace = nil;\n\n@implementation LSNocilla\n\n+ (LSNocilla *)sharedInstance {\n    static dispatch_once_t onceToken;\n    dispatch_once(&onceToken, ^{\n        sharedInstace = [[self alloc] init];\n    });\n    return sharedInstace;\n}\n\n- (id)init {\n    self = [super init];\n    if (self) {\n        _mutableRequests = [NSMutableArray array];\n        _hooks = [NSMutableArray array];\n        [self registerHook:[[LSNSURLHook alloc] init]];\n        if (NSClassFromString(@\"NSURLSession\") != nil) {\n            [self registerHook:[[LSNSURLSessionHook alloc] init]];\n        }\n        [self registerHook:[[LSASIHTTPRequestHook alloc] init]];\n    }\n    return self;\n}\n\n- (NSArray *)stubbedRequests {\n    return [NSArray arrayWithArray:self.mutableRequests];\n}\n\n- (void)start {\n    if (!self.isStarted){\n        [self loadHooks];\n        self.started = YES;\n    }\n}\n\n- (void)stop {\n    [self unloadHooks];\n    [self clearStubs];\n    self.started = NO;\n}\n\n- (void)addStubbedRequest:(LSStubRequest *)request {\n    NSUInteger index = [self.mutableRequests indexOfObject:request];\n\n    if (index == NSNotFound) {\n        [self.mutableRequests addObject:request];\n        return;\n    }\n\n    [self.mutableRequests replaceObjectAtIndex:index withObject:request];\n}\n\n- (void)clearStubs {\n    [self.mutableRequests removeAllObjects];\n}\n\n- (LSStubResponse *)responseForRequest:(id<LSHTTPRequest>)actualRequest {\n    NSArray* requests = [LSNocilla sharedInstance].stubbedRequests;\n\n    for(LSStubRequest *someStubbedRequest in requests) {\n        if ([someStubbedRequest matchesRequest:actualRequest]) {\n            return someStubbedRequest.response;\n        }\n    }\n    \n    // Stop raise this due to it causes problem on CI.\n//    [NSException raise:@\"NocillaUnexpectedRequest\" format:@\"An unexpected HTTP request was fired.\\n\\nUse this snippet to stub the request:\\n%@\\n\", [[[LSHTTPRequestDSLRepresentation alloc] initWithRequest:actualRequest] description]];\n\n    return nil;\n}\n\n- (void)registerHook:(LSHTTPClientHook *)hook {\n    if (![self hookWasRegistered:hook]) {\n        [[self hooks] addObject:hook];\n    }\n}\n\n- (BOOL)hookWasRegistered:(LSHTTPClientHook *)aHook {\n    for (LSHTTPClientHook *hook in self.hooks) {\n        if ([hook isMemberOfClass: [aHook class]]) {\n            return YES;\n        }\n    }\n    return NO;\n}\n#pragma mark - Private\n- (void)loadHooks {\n    for (LSHTTPClientHook *hook in self.hooks) {\n        [hook load];\n    }\n}\n\n- (void)unloadHooks {\n    for (LSHTTPClientHook *hook in self.hooks) {\n        [hook unload];\n    }\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSDataMatcher.h",
    "content": "//\n//  LSDataMatcher.h\n//  Nocilla\n//\n//  Created by Luis Solano Bonet on 09/11/14.\n//  Copyright (c) 2014 Luis Solano Bonet. All rights reserved.\n//\n\n#import <Foundation/Foundation.h>\n#import \"LSMatcher.h\"\n\n@interface LSDataMatcher : LSMatcher\n\n- (instancetype)initWithData:(NSData *)data;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSDataMatcher.m",
    "content": "//\n//  LSDataMatcher.m\n//  Nocilla\n//\n//  Created by Luis Solano Bonet on 09/11/14.\n//  Copyright (c) 2014 Luis Solano Bonet. All rights reserved.\n//\n\n#import \"LSDataMatcher.h\"\n\n@interface LSDataMatcher ()\n\n@property (nonatomic, copy) NSData *data;\n\n@end\n\n@implementation LSDataMatcher\n\n- (instancetype)initWithData:(NSData *)data {\n    self = [super init];\n\n    if (self) {\n        _data = data;\n    }\n    return self;\n}\n\n- (BOOL)matchesData:(NSData *)data {\n    return [self.data isEqualToData:data];\n}\n\n\n#pragma mark - Equality\n\n- (BOOL)isEqual:(id)object {\n    if (self == object) {\n        return YES;\n    }\n\n    if (![object isKindOfClass:[LSDataMatcher class]]) {\n        return NO;\n    }\n\n    return [self.data isEqual:((LSDataMatcher *)object).data];\n}\n\n- (NSUInteger)hash {\n    return self.data.hash;\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSMatcheable.h",
    "content": "#import <Foundation/Foundation.h>\n\n@class LSMatcher;\n\n@protocol LSMatcheable <NSObject>\n\n- (LSMatcher *)matcher;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSMatcher.h",
    "content": "#import <Foundation/Foundation.h>\n\n@interface LSMatcher : NSObject\n\n- (BOOL)matches:(NSString *)string;\n\n- (BOOL)matchesData:(NSData *)data;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSMatcher.m",
    "content": "#import \"LSMatcher.h\"\n\n@implementation LSMatcher\n\n- (BOOL)matches:(NSString *)string {\n    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@\"[LSMatcher matches:] is an abstract method\" userInfo:nil];\n}\n\n- (BOOL)matchesData:(NSData *)data {\n    return [self matches:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]];\n}\n\n\n#pragma mark - Equality\n\n- (BOOL)isEqual:(id)object {\n    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@\"[LSMatcher isEqual:] is an abstract method\" userInfo:nil];\n}\n\n- (NSUInteger)hash {\n    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@\"[LSMatcher hash] an abstract method\" userInfo:nil];\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSRegexMatcher.h",
    "content": "#import \"LSMatcher.h\"\n\n@interface LSRegexMatcher : LSMatcher\n\n- (instancetype)initWithRegex:(NSRegularExpression *)regex;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSRegexMatcher.m",
    "content": "#import \"LSRegexMatcher.h\"\n\n@interface LSRegexMatcher ()\n@property (nonatomic, strong) NSRegularExpression *regex;\n@end\n\n@implementation LSRegexMatcher\n\n- (instancetype)initWithRegex:(NSRegularExpression *)regex {\n    self = [super init];\n    if (self) {\n        _regex = regex;\n    }\n    return self;\n}\n\n- (BOOL)matches:(NSString *)string {\n    return [self.regex numberOfMatchesInString:string options:0 range:NSMakeRange(0, string.length)] > 0;\n}\n\n\n#pragma mark - Equality\n\n- (BOOL)isEqual:(id)object {\n    if (self == object) {\n        return YES;\n    }\n\n    if (![object isKindOfClass:[LSRegexMatcher class]]) {\n        return NO;\n    }\n\n    return [self.regex isEqual:((LSRegexMatcher *)object).regex];\n}\n\n- (NSUInteger)hash {\n    return self.regex.hash;\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSStringMatcher.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSMatcher.h\"\n\n@interface LSStringMatcher : LSMatcher\n\n- (instancetype)initWithString:(NSString *)string;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/LSStringMatcher.m",
    "content": "#import \"LSStringMatcher.h\"\n\n@interface LSStringMatcher ()\n\n@property (nonatomic, copy) NSString *string;\n\n@end\n\n@implementation LSStringMatcher\n\n- (instancetype)initWithString:(NSString *)string {\n    self = [super init];\n    if (self) {\n        _string = string;\n    }\n    return self;\n}\n\n- (BOOL)matches:(NSString *)string {\n    return [self.string isEqualToString:string];\n}\n\n\n#pragma mark - Equality\n\n- (BOOL)isEqual:(id)object {\n    if (self == object) {\n        return YES;\n    }\n\n    if (![object isKindOfClass:[LSStringMatcher class]]) {\n        return NO;\n    }\n\n    return [self.string isEqualToString:((LSStringMatcher *)object).string];\n}\n\n- (NSUInteger)hash {\n    return self.string.hash;\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/NSData+Matcheable.h",
    "content": "//\n//  NSData+Matcheable.h\n//  Nocilla\n//\n//  Created by Luis Solano Bonet on 09/11/14.\n//  Copyright (c) 2014 Luis Solano Bonet. All rights reserved.\n//\n\n#import <Foundation/Foundation.h>\n#import \"LSMatcheable.h\"\n\n@interface NSData (Matcheable) <LSMatcheable>\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/NSData+Matcheable.m",
    "content": "//\n//  NSData+Matcheable.m\n//  Nocilla\n//\n//  Created by Luis Solano Bonet on 09/11/14.\n//  Copyright (c) 2014 Luis Solano Bonet. All rights reserved.\n//\n\n#import \"NSData+Matcheable.h\"\n#import \"LSDataMatcher.h\"\n\n@implementation NSData (Matcheable)\n\n- (LSMatcher *)matcher {\n    return [[LSDataMatcher alloc] initWithData:self];\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/NSRegularExpression+Matcheable.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSMatcheable.h\"\n\n@interface NSRegularExpression (Matcheable) <LSMatcheable>\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/NSRegularExpression+Matcheable.m",
    "content": "#import \"NSRegularExpression+Matcheable.h\"\n#import \"LSRegexMatcher.h\"\n\n@implementation NSRegularExpression (Matcheable)\n\n- (LSMatcher *)matcher {\n    return [[LSRegexMatcher alloc] initWithRegex:self];\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/NSString+Matcheable.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSMatcheable.h\"\n\n@interface NSString (Matcheable) <LSMatcheable>\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Matchers/NSString+Matcheable.m",
    "content": "#import \"NSString+Matcheable.h\"\n#import \"LSStringMatcher.h\"\n\n@implementation NSString (Matcheable)\n\n- (LSMatcher *)matcher {\n    return [[LSStringMatcher alloc] initWithString:self];\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Model/LSHTTPBody.h",
    "content": "#import <Foundation/Foundation.h>\n\n@protocol LSHTTPBody <NSObject>\n- (NSData *)data;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Model/LSHTTPRequest.h",
    "content": "#import <Foundation/Foundation.h>\n\n@protocol LSHTTPRequest <NSObject>\n\n@property (nonatomic, strong, readonly) NSURL *url;\n@property (nonatomic, strong, readonly) NSString *method;\n@property (nonatomic, strong, readonly) NSDictionary *headers;\n@property (nonatomic, strong, readonly) NSData *body;\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Model/LSHTTPResponse.h",
    "content": "#import <Foundation/Foundation.h>\n\n@protocol LSHTTPResponse <NSObject>\n@property (nonatomic, assign, readonly) NSInteger statusCode;\n@property (nonatomic, strong, readonly) NSDictionary *headers;\n@property (nonatomic, strong, readonly) NSData *body;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Nocilla.h",
    "content": "//\n//  Nocilla.h\n//  Nocilla\n//\n//  Created by Robert Böhnke on 26/03/15.\n//  Copyright (c) 2015 Luis Solano Bonet. All rights reserved.\n//\n\n#import <Foundation/Foundation.h>\n\n//! Project version number for Nocilla.\nFOUNDATION_EXPORT double NocillaVersionNumber;\n\n//! Project version string for Nocilla.\nFOUNDATION_EXPORT const unsigned char NocillaVersionString[];\n\n#import \"LSHTTPBody.h\"\n#import \"LSMatcheable.h\"\n#import \"LSNocilla.h\"\n#import \"LSStubRequestDSL.h\"\n#import \"LSStubResponseDSL.h\"\n#import \"NSData+Matcheable.h\"\n#import \"NSData+Nocilla.h\"\n#import \"NSRegularExpression+Matcheable.h\"\n#import \"NSString+Matcheable.h\"\n#import \"NSString+Nocilla.h\"\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Stubs/LSStubRequest.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSStubResponse.h\"\n#import \"LSHTTPRequest.h\"\n\n\n@class LSMatcher;\n@class LSStubRequest;\n@class LSStubResponse;\n\n@interface LSStubRequest : NSObject\n@property (nonatomic, strong, readonly) NSString *method;\n@property (nonatomic, strong, readonly) LSMatcher *urlMatcher;\n@property (nonatomic, strong, readonly) NSDictionary *headers;\n@property (nonatomic, strong, readwrite) LSMatcher *body;\n\n@property (nonatomic, strong) LSStubResponse *response;\n\n- (instancetype)initWithMethod:(NSString *)method url:(NSString *)url;\n- (instancetype)initWithMethod:(NSString *)method urlMatcher:(LSMatcher *)urlMatcher;\n\n- (void)setHeader:(NSString *)header value:(NSString *)value;\n\n- (BOOL)matchesRequest:(id<LSHTTPRequest>)request;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Stubs/LSStubRequest.m",
    "content": "#import \"LSStubRequest.h\"\n#import \"LSMatcher.h\"\n#import \"NSString+Matcheable.h\"\n\n@interface LSStubRequest ()\n@property (nonatomic, strong, readwrite) NSString *method;\n@property (nonatomic, strong, readwrite) LSMatcher *urlMatcher;\n@property (nonatomic, strong, readwrite) NSMutableDictionary *mutableHeaders;\n\n-(BOOL)matchesMethod:(id<LSHTTPRequest>)request;\n-(BOOL)matchesURL:(id<LSHTTPRequest>)request;\n-(BOOL)matchesHeaders:(id<LSHTTPRequest>)request;\n-(BOOL)matchesBody:(id<LSHTTPRequest>)request;\n@end\n\n@implementation LSStubRequest\n\n- (instancetype)initWithMethod:(NSString *)method url:(NSString *)url {\n    return [self initWithMethod:method urlMatcher:[url matcher]];\n}\n\n- (instancetype)initWithMethod:(NSString *)method urlMatcher:(LSMatcher *)urlMatcher; {\n    self = [super init];\n    if (self) {\n        self.method = method;\n        self.urlMatcher = urlMatcher;\n        self.mutableHeaders = [NSMutableDictionary dictionary];\n    }\n    return self;\n}\n\n- (void)setHeader:(NSString *)header value:(NSString *)value {\n    [self.mutableHeaders setValue:value forKey:header];\n}\n\n- (NSDictionary *)headers {\n    return [NSDictionary dictionaryWithDictionary:self.mutableHeaders];;\n}\n\n- (NSString *)description {\n    return [NSString stringWithFormat:@\"StubRequest:\\nMethod: %@\\nURL: %@\\nHeaders: %@\\nBody: %@\\nResponse: %@\",\n            self.method,\n            self.urlMatcher,\n            self.headers,\n            self.body,\n            self.response];\n}\n\n- (LSStubResponse *)response {\n    if (!_response) {\n        _response = [[LSStubResponse alloc] initDefaultResponse];\n    }\n    return _response;\n    \n}\n\n- (BOOL)matchesRequest:(id<LSHTTPRequest>)request {\n    if ([self matchesMethod:request]\n        && [self matchesURL:request]\n        && [self matchesHeaders:request]\n        && [self matchesBody:request]\n        ) {\n        return YES;\n    }\n    return NO;\n}\n\n-(BOOL)matchesMethod:(id<LSHTTPRequest>)request {\n    if (!self.method || [self.method isEqualToString:request.method]) {\n        return YES;\n    }\n    return NO;\n}\n\n-(BOOL)matchesURL:(id<LSHTTPRequest>)request {\n    return [self.urlMatcher matches:[request.url absoluteString]];\n}\n\n-(BOOL)matchesHeaders:(id<LSHTTPRequest>)request {\n    for (NSString *header in self.headers) {\n        if (![[request.headers objectForKey:header] isEqualToString:[self.headers objectForKey:header]]) {\n            return NO;\n        }\n    }\n    return YES;\n}\n\n-(BOOL)matchesBody:(id<LSHTTPRequest>)request {\n    NSData *reqBody = request.body;\n    if (!self.body || [self.body matchesData:reqBody]) {\n        return YES;\n    }\n    return NO;\n}\n\n\n#pragma mark - Equality\n\n- (BOOL)isEqual:(id)object {\n    if (self == object) {\n        return YES;\n    }\n\n    if (![object isKindOfClass:[LSStubRequest class]]) {\n        return NO;\n    }\n\n    return [self isEqualToStubRequest:object];\n}\n\n- (BOOL)isEqualToStubRequest:(LSStubRequest *)stubRequest {\n    if (!stubRequest) {\n        return NO;\n    }\n\n    BOOL methodEqual = [self.method isEqualToString:stubRequest.method];\n    BOOL urlMatcherEqual = [self.urlMatcher isEqual:stubRequest.urlMatcher];\n    BOOL headersEqual = [self.headers isEqual:stubRequest.headers];\n    BOOL bodyEqual = (self.body == nil && stubRequest.body == nil) || [self.body isEqual:stubRequest.body];\n\n    return methodEqual && urlMatcherEqual && headersEqual && bodyEqual;\n}\n\n- (NSUInteger)hash {\n    return self.method.hash ^ self.urlMatcher.hash ^ self.headers.hash ^ self.body.hash;\n}\n\n@end\n\n\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Stubs/LSStubResponse.h",
    "content": "#import <Foundation/Foundation.h>\n#import \"LSHTTPResponse.h\"\n\n@interface LSStubResponse : NSObject<LSHTTPResponse>\n\n@property (nonatomic, assign, readonly) NSInteger statusCode;\n@property (nonatomic, strong) NSData *body;\n@property (nonatomic, strong, readonly) NSDictionary *headers;\n\n@property (nonatomic, assign, readonly) BOOL shouldFail;\n@property (nonatomic, strong, readonly) NSError *error;\n\n- (id)initWithError:(NSError *)error;\n- (id)initWithStatusCode:(NSInteger)statusCode;\n- (id)initWithRawResponse:(NSData *)rawResponseData;\n- (id)initDefaultResponse;\n- (void)setHeader:(NSString *)header value:(NSString *)value;\n\n- (void)delay;\n- (void)go;\n- (void)waitForGo;\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/Nocilla/Stubs/LSStubResponse.m",
    "content": "#import \"LSStubResponse.h\"\n\n@interface LSStubResponse () {\n    NSCondition *_delayLock;\n}\n@property (nonatomic, assign, readwrite) NSInteger statusCode;\n@property (nonatomic, strong) NSMutableDictionary *mutableHeaders;\n@property (nonatomic, assign) UInt64 offset;\n@property (nonatomic, assign, getter = isDone) BOOL done;\n@property (nonatomic, assign) BOOL shouldFail;\n@property (nonatomic, strong) NSError *error;\n@end\n\n@implementation LSStubResponse\n\n#pragma Initializers\n- (id)initDefaultResponse {\n    self = [super init];\n    if (self) {\n        self.shouldFail = NO;\n\n        self.statusCode = 200;\n        self.mutableHeaders = [NSMutableDictionary dictionary];\n        self.body = [@\"\" dataUsingEncoding:NSUTF8StringEncoding];\n    }\n    return self;\n}\n\n\n- (id)initWithError:(NSError *)error {\n    self = [super init];\n    if (self) {\n        self.shouldFail = YES;\n        self.error = error;\n    }\n    return self;\n}\n\n-(id)initWithStatusCode:(NSInteger)statusCode {\n    self = [super init];\n    if (self) {\n        self.shouldFail = NO;\n        self.statusCode = statusCode;\n        self.mutableHeaders = [NSMutableDictionary dictionary];\n        self.body = [@\"\" dataUsingEncoding:NSUTF8StringEncoding];\n    }\n    return self;\n}\n\n- (id)initWithRawResponse:(NSData *)rawResponseData {\n    self = [self initDefaultResponse];\n    if (self) {\n        CFHTTPMessageRef httpMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, FALSE);\n        if (httpMessage) {\n            CFHTTPMessageAppendBytes(httpMessage, [rawResponseData bytes], [rawResponseData length]);\n            \n            self.body = rawResponseData; // By default\n            \n            if (CFHTTPMessageIsHeaderComplete(httpMessage)) {\n                self.statusCode = (NSInteger)CFHTTPMessageGetResponseStatusCode(httpMessage);\n                self.mutableHeaders = [NSMutableDictionary dictionaryWithDictionary:(__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(httpMessage)];\n                self.body = (__bridge_transfer NSData *)CFHTTPMessageCopyBody(httpMessage);\n            }\n            CFRelease(httpMessage);\n        }\n    }\n    return self;\n}\n\n- (void)setHeader:(NSString *)header value:(NSString *)value {\n    [self.mutableHeaders setValue:value forKey:header];\n}\n- (NSDictionary *)headers {\n    return [NSDictionary dictionaryWithDictionary:self.mutableHeaders];\n}\n\n- (NSString *)description {\n    return [NSString stringWithFormat:@\"StubRequest:\\nStatus Code: %ld\\nHeaders: %@\\nBody: %@\",\n            (long)self.statusCode,\n            self.mutableHeaders,\n            self.body];\n}\n\n- (NSCondition*)delayLock {\n    @synchronized(self) {\n        return _delayLock;\n    }\n}\n\n- (void)delay {\n    @synchronized(self) {\n        if(!_delayLock)\n            _delayLock = [[NSCondition alloc] init];\n    }\n}\n\n- (void)go {\n    NSCondition *condition = self.delayLock;\n    @synchronized(self) {\n        _delayLock = nil;\n    }\n    [condition lock];\n    [condition broadcast];\n    [condition unlock];\n}\n\n- (void)waitForGo {\n    NSCondition *condition = self.delayLock;\n    [condition lock];\n    [condition wait];\n    [condition unlock];\n}\n\n@end\n"
  },
  {
    "path": "Tests/Dependency/Nocilla/README.md",
    "content": "# Nocilla [![CI Status](http://img.shields.io/travis/luisobo/Nocilla.svg?style=flat&branch=master)](https://travis-ci.org/luisobo/Nocilla)[![Version](https://img.shields.io/cocoapods/v/Nocilla.svg?style=flat)](http://cocoadocs.org/docsets/Nocilla)[![License](https://img.shields.io/cocoapods/l/Nocilla.svg?style=flat)](http://cocoadocs.org/docsets/Nocilla)[![Platform](https://img.shields.io/cocoapods/p/Nocilla.svg?style=flat)](http://cocoadocs.org/docsets/Nocilla)\n\nStunning HTTP stubbing for iOS and OS X. Testing HTTP requests has never been easier.\n\nThis library was inspired by [WebMock](https://github.com/bblimke/webmock) and it's using [this approach](http://www.infinite-loop.dk/blog/2011/09/using-nsurlprotocol-for-injecting-test-data/) to stub the requests.\n\n## Features\n* Stub HTTP and HTTPS requests in your unit tests.\n* Supports NSURLConnection, NSURLSession and ASIHTTPRequest.\n* Awesome DSL that will improve the readability and maintainability of your tests.\n* Match requests with regular expressions.\n* Stub requests with errors.\n* Tested.\n* Fast.\n* Extendable to support more HTTP libraries.\n\n## Installation\n### As a [CocoaPod](http://cocoapods.org/)\nJust add this to your Podfile\n```ruby\npod 'Nocilla'\n```\n\n### Other approaches\n* You should be able to add Nocilla to you source tree. If you are using git, consider using a `git submodule`\n\n## Usage\n_Yes, the following code is valid Objective-C, or at least, it should be_\n\nThe following examples are described using [Kiwi](https://github.com/kiwi-bdd/Kiwi)\n\n### Common parts\nUntil Nocilla can hook directly into Kiwi, you will have to include the following snippet in the specs you want to use Nocilla:\n\n```objc\n#import \"Kiwi.h\"\n#import \"Nocilla.h\"\nSPEC_BEGIN(ExampleSpec)\nbeforeAll(^{\n  [[LSNocilla sharedInstance] start];\n});\nafterAll(^{\n  [[LSNocilla sharedInstance] stop];\n});\nafterEach(^{\n  [[LSNocilla sharedInstance] clearStubs];\n});\n\nit(@\"should do something\", ^{\n  // Stub here!\n});\nSPEC_END\n```\n\n### Stubbing requests\n#### Stubbing a simple request\nIt will return the default response, which is a 200 and an empty body.\n\n```objc\nstubRequest(@\"GET\", @\"http://www.google.com\");\n```\n\n#### Stubbing requests with regular expressions\n```objc\nstubRequest(@\"GET\", @\"^http://(.*?)\\\\.example\\\\.com/v1/dogs\\\\.json\".regex);\n```\n\n\n#### Stubbing a request with a particular header\n\n```objc\nstubRequest(@\"GET\", @\"https://api.example.com\").\nwithHeader(@\"Accept\", @\"application/json\");\n```\n\n#### Stubbing a request with multiple headers\n\nUsing the `withHeaders` method makes sense with the Objective-C literals, but it accepts an NSDictionary.\n\n```objc\nstubRequest(@\"GET\", @\"https://api.example.com/dogs.json\").\nwithHeaders(@{@\"Accept\": @\"application/json\", @\"X-CUSTOM-HEADER\": @\"abcf2fbc6abgf\"});\n```\n\n#### Stubbing a request with a particular body\n\n```objc\nstubRequest(@\"POST\", @\"https://api.example.com/dogs.json\").\nwithHeaders(@{@\"Accept\": @\"application/json\", @\"X-CUSTOM-HEADER\": @\"abcf2fbc6abgf\"}).\nwithBody(@\"{\\\"name\\\":\\\"foo\\\"}\");\n```\n\nYou can also use `NSData` for the request body:\n\n```objc\nstubRequest(@\"POST\", @\"https://api.example.com/dogs.json\").\nwithHeaders(@{@\"Accept\": @\"application/json\", @\"X-CUSTOM-HEADER\": @\"abcf2fbc6abgf\"}).\nwithBody([@\"foo\" dataUsingEncoding:NSUTF8StringEncoding]);\n```\n\nIt even works with regular expressions!\n\n```objc\nstubRequest(@\"POST\", @\"https://api.example.com/dogs.json\").\nwithHeaders(@{@\"Accept\": @\"application/json\", @\"X-CUSTOM-HEADER\": @\"abcf2fbc6abgf\"}).\nwithBody(@\"^The body start with this\".regex);\n```\n\n#### Returning a specific status code\n```objc\nstubRequest(@\"GET\", @\"http://www.google.com\").andReturn(404);\n```\n\n#### Returning a specific status code and header\nThe same approach here, you can use `withHeader` or `withHeaders`\n\n```objc\nstubRequest(@\"POST\", @\"https://api.example.com/dogs.json\").\nandReturn(201).\nwithHeaders(@{@\"Content-Type\": @\"application/json\"});\n```\n\n#### Returning a specific status code, headers and body\n```objc\nstubRequest(@\"GET\", @\"https://api.example.com/dogs.json\").\nandReturn(201).\nwithHeaders(@{@\"Content-Type\": @\"application/json\"}).\nwithBody(@\"{\\\"ok\\\":true}\");\n```\n\nYou can also use `NSData` for the response body:\n\n```objc\nstubRequest(@\"GET\", @\"https://api.example.com/dogs.json\").\nandReturn(201).\nwithHeaders(@{@\"Content-Type\": @\"application/json\"}).\nwithBody([@\"bar\" dataUsingEncoding:NSUTF8StringEncoding]);\n```\n\n#### Returning raw responses recorded with `curl -is`\n`curl -is http://api.example.com/dogs.json > /tmp/example_curl_-is_output.txt`\n\n```objc\nstubRequest(@\"GET\", @\"https://api.example.com/dogs.json\").\nandReturnRawResponse([NSData dataWithContentsOfFile:@\"/tmp/example_curl_-is_output.txt\"]);\n```\n\n#### All together\n```objc\nstubRequest(@\"POST\", @\"https://api.example.com/dogs.json\").\nwithHeaders(@{@\"Accept\": @\"application/json\", @\"X-CUSTOM-HEADER\": @\"abcf2fbc6abgf\"}).\nwithBody(@\"{\\\"name\\\":\\\"foo\\\"}\").\nandReturn(201).\nwithHeaders(@{@\"Content-Type\": @\"application/json\"}).\nwithBody(@\"{\\\"ok\\\":true}\");\n```\n\n#### Making a request fail\nThis will call the failure handler (callback, delegate... whatever your HTTP client uses) with the specified error.\n\n```objc\nstubRequest(@\"POST\", @\"https://api.example.com/dogs.json\").\nwithHeaders(@{@\"Accept\": @\"application/json\", @\"X-CUSTOM-HEADER\": @\"abcf2fbc6abgf\"}).\nwithBody(@\"{\\\"name\\\":\\\"foo\\\"}\").\nandFailWithError([NSError errorWithDomain:@\"foo\" code:123 userInfo:nil]);\n```\n\n#### Replacing a request stub\n\nIf you need to change the response of a single request, simply re-stub the request:\n\n```objc\nstubRequest(@\"POST\", @\"https://api.example.com/authorize/\").\nandReturn(401);\n\n// Some test expectation...\n\nstubRequest(@\"POST\", @\"https://api.example.com/authorize/\").\nandReturn(200);\n```\n\n### Unexpected requests\nIf some request is made but it wasn't stubbed, Nocilla won't let that request hit the real world. In that case your test should fail.\nAt this moment Nocilla will raise an exception with a meaningful message about the error and how to solve it, including a snippet of code on how to stub the unexpected request.\n\n### Testing asynchronous requests\nWhen testing asynchronous requests your request will be sent on a different thread from the one on which your test is executed. It is important to keep this in mind, and design your test in such a way that is has enough time to finish. For instance  ```tearDown()``` when using ```XCTest``` and ```afterEach()``` when using [Quick](https://github.com/Quick/Quick) and [Nimble](https://github.com/Quick/Nimble) will cause the request never to complete.\n\n\n## Who uses Nocilla.\n\n### Submit a PR to add your company here!\n\n- [MessageBird](https://www.messagebird.com)\n- [Groupon](http://www.groupon.com)\n- [Pixable](http://www.pixable.com)\n- [Jackthreads](https://www.jackthreads.com)\n- [ShopKeep](http://www.shopkeep.com)\n- [Venmo](https://www.venmo.com)\n- [Lighthouse](http://www.lighthouselabs.co.uk)\n\n## Other alternatives\n* [ILTesting](https://github.com/InfiniteLoopDK/ILTesting)\n* [OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs)\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch\n3. Commit your changes\n4. Push to the branch\n5. Create new Pull Request\n"
  },
  {
    "path": "Tests/KingfisherTests/DataReceivingSideEffectTests.swift",
    "content": "//\n//  DataReceivingSideEffectTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2019/05/15.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass DataReceivingSideEffectTests: XCTestCase {\n\n    var manager: KingfisherManager!\n\n    override class func setUp() {\n        super.setUp()\n        LSNocilla.sharedInstance().start()\n    }\n\n    override class func tearDown() {\n        LSNocilla.sharedInstance().stop()\n        super.tearDown()\n    }\n\n    override func setUp() {\n        super.setUp()\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n        let uuid = UUID()\n        let downloader = ImageDownloader(name: \"test.manager.\\(uuid.uuidString)\")\n        let cache = ImageCache(name: \"test.cache.\\(uuid.uuidString)\")\n\n        manager = KingfisherManager(downloader: downloader, cache: cache)\n    }\n\n    override func tearDown() {\n        LSNocilla.sharedInstance().clearStubs()\n        clearCaches([manager.cache])\n        cleanDefaultCache()\n        manager = nil\n        super.tearDown()\n    }\n\n    func xtestDataReceivingSideEffectBlockCanBeCalled() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n\n        let receiver = DataReceivingStub()\n\n        let options: KingfisherOptionsInfo = [/*.onDataReceived([receiver]),*/ .waitForCache]\n        KingfisherManager.shared.retrieveImage(with: url, options: options) {\n            result in\n            XCTAssertTrue(receiver.called.value)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func xtestDataReceivingSideEffectBlockCanBeCalledButNotApply() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n\n        let receiver = DataReceivingNotApplyStub()\n\n        let options: KingfisherOptionsInfo = [/*.onDataReceived([receiver]),*/ .waitForCache]\n        KingfisherManager.shared.retrieveImage(with: url, options: options) {\n            result in\n            XCTAssertTrue(receiver.called.value)\n            XCTAssertFalse(receiver.applied.value)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n}\n\nclass DataReceivingStub: DataReceivingSideEffect, @unchecked Sendable {\n    var called = LockIsolated(false)\n    var onShouldApply: () -> Bool = { return true }\n    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {\n        self.called.setValue(true)\n    }\n}\n\nclass DataReceivingNotApplyStub: DataReceivingSideEffect, @unchecked Sendable {\n\n    var called = LockIsolated(false)\n    var applied = LockIsolated(false)\n\n    var onShouldApply: () -> Bool = { return false }\n\n    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {\n        called.setValue(true)\n        if onShouldApply() {\n            applied.setValue(true)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/DiskStorageTests.swift",
    "content": "//\n//  DiskStorageTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/11/12.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\n#if compiler(>=6)\nextension String: @retroactive DataTransformable { }\n#else\nextension String: DataTransformable { }\n#endif\nextension String {\n    public func toData() throws -> Data {\n        return data(using: .utf8)!\n    }\n    public static func fromData(_ data: Data) throws -> String {\n        return String(data: data, encoding: .utf8)!\n    }\n    public static var empty: String { return \"\" }\n}\n\nclass DiskStorageTests: XCTestCase {\n\n    var storage: DiskStorage.Backend<String>!\n\n    override func setUp() {\n        super.setUp()\n\n        let uuid = UUID().uuidString\n        let config = DiskStorage.Config(name: \"test-\\(uuid)\", sizeLimit: 5)\n        storage = try! DiskStorage.Backend<String>(config: config)\n    }\n\n    override func tearDown() {\n        try! storage.removeAll(skipCreatingDirectory: true)\n        super.tearDown()\n    }\n\n    func testStoreAndGet() {\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        try! storage.store(value: \"1\", forKey: \"1\")\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n        let value = try! storage.value(forKey: \"1\")\n        XCTAssertEqual(value, \"1\")\n    }\n\n    func testRemove() {\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        try! storage.store(value: \"1\", forKey: \"1\")\n        try! storage.remove(forKey: \"1\")\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n    }\n\n    func testRemoveAll() {\n        try! storage.store(value: \"1\", forKey: \"1\")\n        try! storage.store(value: \"2\", forKey: \"2\")\n        try! storage.store(value: \"3\", forKey: \"3\")\n\n        try! storage.removeAll()\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        XCTAssertFalse(storage.isCached(forKey: \"2\"))\n        XCTAssertFalse(storage.isCached(forKey: \"3\"))\n    }\n\n    func testTotalSize() {\n        var size = try! storage.totalSize()\n        XCTAssertEqual(size, 0)\n\n        try! storage.store(value: \"1\", forKey: \"1\")\n\n        size = try! storage.totalSize()\n        XCTAssertEqual(size, 1)\n    }\n\n    func testSetExpiration() {\n        let now = Date()\n\n        try! storage.store(value: \"1\", forKey: \"1\", expiration: .seconds(1))\n\n        XCTAssertTrue(storage.isCached(forKey: \"1\", referenceDate: now))\n        XCTAssertFalse(storage.isCached(forKey: \"1\", referenceDate: now.addingTimeInterval(5)))\n    }\n\n    func testConfigExpiration() {\n\n        let now = Date()\n\n        storage.config.expiration = .seconds(1)\n        try! storage.store(value: \"1\", forKey: \"1\")\n        XCTAssertTrue(storage.isCached(forKey: \"1\", referenceDate: now))\n        XCTAssertFalse(storage.isCached(forKey: \"1\", referenceDate: now.addingTimeInterval(5)))\n    }\n\n    func testExtendExpirationByAccessing() {\n\n        let exp = expectation(description: #function)\n        let now = Date()\n        try! storage.store(value: \"1\", forKey: \"1\", expiration: .seconds(2))\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n        XCTAssertFalse(storage.isCached(forKey: \"1\", referenceDate: now.addingTimeInterval(5)))\n\n        delay(1) {\n            let v = try! self.storage.value(forKey: \"1\")\n            XCTAssertNotNil(v)\n            // The meta extending happens on its own queue.\n            self.storage.metaChangingQueue.async {\n                XCTAssertTrue(self.storage.isCached(forKey: \"1\", referenceDate: now.addingTimeInterval(3)))\n                XCTAssertFalse(self.storage.isCached(forKey: \"1\", referenceDate: now.addingTimeInterval(10)))\n                exp.fulfill()\n            }\n        }\n\n        waitForExpectations(timeout: 2, handler: nil)\n    }\n\n    func testNotExtendExpirationByAccessing() {\n\n        let exp = expectation(description: #function)\n        let now = Date()\n        try! storage.store(value: \"1\", forKey: \"1\", expiration: .seconds(2))\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n        XCTAssertFalse(storage.isCached(forKey: \"1\", referenceDate: now.addingTimeInterval(3)))\n\n        delay(1) {\n            let v = try! self.storage.value(forKey: \"1\", extendingExpiration: .none)\n            XCTAssertNotNil(v)\n            // The meta extending happens on its own queue.\n            self.storage.metaChangingQueue.async {\n                XCTAssertFalse(self.storage.isCached(forKey: \"1\", referenceDate: now.addingTimeInterval(3)))\n                XCTAssertFalse(self.storage.isCached(forKey: \"1\", referenceDate: now.addingTimeInterval(10)))\n                exp.fulfill()\n            }\n        }\n\n        waitForExpectations(timeout: 2, handler: nil)\n    }\n\n    func testRemoveExpired() {\n\n        let expiration = StorageExpiration.seconds(1)\n        try! storage.store(value: \"1\", forKey: \"1\", expiration: expiration)\n        try! storage.store(value: \"2\", forKey: \"2\", expiration: expiration)\n        try! storage.store(value: \"3\", forKey: \"3\")\n\n        let urls = try! self.storage.removeExpiredValues(referenceDate: Date().addingTimeInterval(2))\n        XCTAssertEqual(urls.count, 2)\n\n        XCTAssertTrue(self.storage.isCached(forKey: \"3\"))\n    }\n\n    func testRemoveSizeExceeded() {\n        let count = 10\n        for i in 0..<count {\n            let s = String(i)\n            try! storage.store(value: s, forKey: s)\n        }\n\n        let urls = try! storage.removeSizeExceededValues()\n        XCTAssertTrue(urls.count < count)\n        XCTAssertTrue(urls.count > 0)\n    }\n\n    func testConfigUsesHashedFileName() {\n        let key = \"test\"\n\n        // hashed fileName\n        storage.config.usesHashedFileName = true\n        let hashedFileName = storage.cacheFileName(forKey: key)\n        XCTAssertNotEqual(hashedFileName, key)\n        // validation sha256 hash of the key\n        XCTAssertEqual(hashedFileName, key.kf.sha256)\n\n        // fileName without hash\n        storage.config.usesHashedFileName = false\n        let originalFileName = storage.cacheFileName(forKey: key)\n        XCTAssertEqual(originalFileName, key)\n    }\n\n    func testConfigUsesHashedFileNameWithAutoExt() {\n        let key = \"test.gif\"\n\n        // hashed fileName\n        storage.config.usesHashedFileName = true\n        storage.config.autoExtAfterHashedFileName = true\n        let hashedFileName = storage.cacheFileName(forKey: key)\n        XCTAssertNotEqual(hashedFileName, key)\n        // validation sha256 hash of the key\n        XCTAssertEqual(hashedFileName, key.kf.sha256 + \".gif\")\n\n        // fileName without hash\n        storage.config.usesHashedFileName = false\n        let originalFileName = storage.cacheFileName(forKey: key)\n        XCTAssertEqual(originalFileName, key)\n    }\n    \n    func testConfigUsesHashedFileNameWithAutoExtAndProcessor() {\n        // The key of an image with processor will be as this format.\n        let key = \"test.jpeg@com.onevcat.Kingfisher.DownsamplingImageProcessor\"\n        \n        // hashed fileName\n        storage.config.usesHashedFileName = true\n        storage.config.autoExtAfterHashedFileName = true\n        let hashedFileName = storage.cacheFileName(forKey: key)\n        XCTAssertNotEqual(hashedFileName, key)\n        // validation sha256 hash of the key\n        XCTAssertEqual(hashedFileName, key.kf.sha256 + \".jpeg\")\n\n        // fileName without hash\n        storage.config.usesHashedFileName = false\n        let originalFileName = storage.cacheFileName(forKey: key)\n        XCTAssertEqual(originalFileName, key)\n    }\n\n    func testFileMetaOrder() {\n        let urls = [URL(string: \"test1\")!, URL(string: \"test2\")!, URL(string: \"test3\")!]\n\n        let now = Date()\n\n        let file1 = DiskStorage.FileMeta(\n            fileURL: urls[0],\n            lastAccessDate: now,\n            estimatedExpirationDate: now.addingTimeInterval(1),\n            isDirectory: false,\n            fileSize: 1)\n        let file2 = DiskStorage.FileMeta(\n            fileURL: urls[1],\n            lastAccessDate: now.addingTimeInterval(1),\n            estimatedExpirationDate: now.addingTimeInterval(2),\n            isDirectory: false,\n            fileSize: 1)\n        let file3 = DiskStorage.FileMeta(\n            fileURL: urls[2],\n            lastAccessDate: now.addingTimeInterval(2),\n            estimatedExpirationDate: now.addingTimeInterval(3),\n            isDirectory: false,\n            fileSize: 1)\n\n        let ordered = [file2, file1, file3].sorted(by: DiskStorage.FileMeta.lastAccessDate)\n        XCTAssertTrue(ordered[0].lastAccessDate! > ordered[1].lastAccessDate!)\n        XCTAssertTrue(ordered[1].lastAccessDate! > ordered[2].lastAccessDate!)\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/ImageCacheTests.swift",
    "content": "//\n//  ImageCacheTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/10.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass ImageCacheTests: XCTestCase {\n\n    var cache: ImageCache!\n    var observer: NSObjectProtocol!\n    \n    override func setUp() {\n        super.setUp()\n\n        let uuid = UUID().uuidString\n        let cacheName = \"test-\\(uuid)\"\n        cache = ImageCache(name: cacheName)\n    }\n    \n    override func tearDown() {\n        clearCaches([cache])\n        cache = nil\n        if let o = observer {\n            NotificationCenter.default.removeObserver(o)\n            observer = nil\n        }\n\n        super.tearDown()\n    }\n    \n    func testInvalidCustomCachePath() {\n        let customPath = \"/path/to/image/cache\"\n        let url = URL(fileURLWithPath: customPath)\n        XCTAssertThrowsError(try ImageCache(name: \"test\", cacheDirectoryURL: url)) { error in\n            guard case KingfisherError.cacheError(reason: .cannotCreateDirectory(let path, _)) = error else {\n                XCTFail(\"Should be KingfisherError with cacheError reason.\")\n                return\n            }\n            XCTAssertEqual(path, customPath + \"/com.onevcat.Kingfisher.ImageCache.test\")\n        }\n    }\n\n    func testCustomCachePath() {\n        let cacheURL = try! FileManager.default.url(\n            for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)\n        let subFolder = cacheURL.appendingPathComponent(\"temp\")\n\n        let customPath = subFolder.path\n        let cache = try! ImageCache(name: \"test\", cacheDirectoryURL: subFolder)\n        XCTAssertEqual(\n            cache.diskStorage.directoryURL.path,\n            (customPath as NSString).appendingPathComponent(\"com.onevcat.Kingfisher.ImageCache.test\"))\n        clearCaches([cache])\n    }\n    \n    func testCustomCachePathByBlock() {\n        let cache = try! ImageCache(name: \"test\", cacheDirectoryURL: nil, diskCachePathClosure: { (url, path) -> URL in\n            let modifiedPath = path + \"-modified\"\n            return url.appendingPathComponent(modifiedPath, isDirectory: true)\n        })\n        let cacheURL = try! FileManager.default.url(\n            for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)\n        XCTAssertEqual(\n            cache.diskStorage.directoryURL.path,\n            (cacheURL.path as NSString).appendingPathComponent(\"com.onevcat.Kingfisher.ImageCache.test-modified\"))\n        clearCaches([cache])\n    }\n    \n    func testMaxCachePeriodInSecond() {\n        cache.diskStorage.config.expiration = .seconds(1)\n        XCTAssertEqual(cache.diskStorage.config.expiration.timeInterval, 1)\n    }\n    \n    func testMaxMemorySize() {\n        cache.memoryStorage.config.totalCostLimit = 1\n        XCTAssert(cache.memoryStorage.config.totalCostLimit == 1, \"maxMemoryCost should be able to be set.\")\n    }\n    \n    func testMaxDiskCacheSize() {\n        cache.diskStorage.config.sizeLimit = 1\n        XCTAssert(cache.diskStorage.config.sizeLimit == 1, \"maxDiskCacheSize should be able to be set.\")\n    }\n    \n    func testClearDiskCache() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in\n            self.cache.clearMemoryCache()\n            let cacheResult = self.cache.imageCachedType(forKey: key)\n            XCTAssertTrue(cacheResult.cached)\n            XCTAssertEqual(cacheResult, .disk)\n        \n            self.cache.clearDiskCache {\n                let cacheResult = self.cache.imageCachedType(forKey: key)\n                XCTAssertFalse(cacheResult.cached)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler:nil)\n    }\n    \n    func testClearDiskCacheAsync() async throws {\n        let key = testKeys[0]\n        try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true)\n        cache.clearMemoryCache()\n        var cacheResult = self.cache.imageCachedType(forKey: key)\n        XCTAssertTrue(cacheResult.cached)\n        XCTAssertEqual(cacheResult, .disk)\n        \n        await cache.clearDiskCache()\n        cacheResult = cache.imageCachedType(forKey: key)\n        XCTAssertFalse(cacheResult.cached)\n    }\n    \n    func testClearMemoryCache() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in\n            self.cache.clearMemoryCache()\n            self.cache.retrieveImage(forKey: key) { result in\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value?.cacheType, .disk)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testClearMemoryCacheAsync() async throws {\n        let key = testKeys[0]\n        try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true)\n        cache.clearMemoryCache()\n        let result = try await cache.retrieveImage(forKey: key)\n        XCTAssertNotNil(result.image)\n        XCTAssertEqual(result.cacheType, .disk)\n    }\n    \n    func testNoImageFound() {\n        let exp = expectation(description: #function)\n        cache.retrieveImage(forKey: testKeys[0]) { result in\n            XCTAssertNotNil(result.value)\n            XCTAssertNil(result.value!.image)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testNoImageFoundAsync() async throws {\n        let result = try await cache.retrieveImage(forKey: testKeys[0])\n        XCTAssertNil(result.image)\n    }\n\n    func testCachedFileDoesNotExist() {\n        let URLString = testKeys[0]\n        let url = URL(string: URLString)!\n\n        let exists = cache.imageCachedType(forKey: url.cacheKey).cached\n        XCTAssertFalse(exists)\n    }\n    \n    func testStoreImageInMemory() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        cache.store(testImage, forKey: key, toDisk: false) { _ in\n            self.cache.retrieveImage(forKey: key) { result in\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value?.cacheType, .memory)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testStoreImageInMemoryAsync() async throws {\n        let key = testKeys[0]\n        try await cache.store(testImage, forKey: key, toDisk: false)\n        let result = try await cache.retrieveImage(forKey: key)\n        XCTAssertNotNil(result.image)\n        XCTAssertEqual(result.cacheType, .memory)\n    }\n\n    func testStoreGIFToDiskWithNilOriginalShouldPreserveGIFFormat() {\n        struct TestProcessor: ImageProcessor {\n            let identifier: String = \"com.onevcat.KingfisherTests.TestProcessor\"\n            func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n                switch item {\n                case .image(let image): return image\n                case .data(let data): return DefaultImageProcessor.default.process(item: .data(data), options: options)\n                }\n            }\n        }\n\n        let exp = expectation(description: #function)\n        let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: .init())!\n        XCTAssertEqual(image.kf.gifRepresentation()?.kf.imageFormat, .GIF)\n\n        let options = KingfisherParsedOptionsInfo([.processor(TestProcessor())])\n        let key = \"test-gif\"\n        cache.store(image, original: nil, forKey: key, options: options, toDisk: true) { _ in\n            do {\n                let storedKey = key.computedKey(with: TestProcessor().identifier)\n                let storedData = try self.cache.diskStorage.value(forKey: storedKey)\n                XCTAssertEqual(storedData?.kf.imageFormat, .GIF)\n            } catch {\n                XCTFail(\"Unexpected error: \\(error)\")\n            }\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testCopyKingfisherStateShouldKeepEmbeddedGIFDataForDiskCache() {\n        struct TestProcessor: ImageProcessor {\n            let identifier: String = \"com.onevcat.KingfisherTests.TestProcessor.CopyState\"\n            func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n                switch item {\n                case .image(let image):\n                    #if os(macOS)\n                    guard let cgImage = image.kf.cgImage else { return image }\n                    let newImage = KFCrossPlatformImage(cgImage: cgImage, size: image.kf.size)\n                    image.kf.copyKingfisherState(to: newImage)\n                    return newImage\n                    #else\n                    guard let cgImage = image.cgImage else { return image }\n                    let newImage = KFCrossPlatformImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)\n                    image.kf.copyKingfisherState(to: newImage)\n                    return newImage\n                    #endif\n                case .data(let data):\n                    return DefaultImageProcessor.default.process(item: .data(data), options: options)\n                }\n            }\n        }\n\n        let exp = expectation(description: #function)\n        let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: .init())!\n        XCTAssertEqual(image.kf.gifRepresentation()?.kf.imageFormat, .GIF)\n\n        let options = KingfisherParsedOptionsInfo([.processor(TestProcessor())])\n        let key = \"test-gif-copy-state\"\n        cache.store(image, original: nil, forKey: key, options: options, toDisk: true) { _ in\n            do {\n                let storedKey = key.computedKey(with: TestProcessor().identifier)\n                let storedData = try self.cache.diskStorage.value(forKey: storedKey)\n                XCTAssertEqual(storedData?.kf.imageFormat, .GIF)\n            } catch {\n                XCTFail(\"Unexpected error: \\(error)\")\n            }\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testStoreMultipleImages() {\n        let exp = expectation(description: #function)\n        storeMultipleImages {\n            let diskCachePath = self.cache.diskStorage.directoryURL.path\n            var files: [String] = []\n            do {\n                files = try FileManager.default.contentsOfDirectory(atPath: diskCachePath)\n            } catch _ {\n                XCTFail()\n            }\n            XCTAssertEqual(files.count, testKeys.count)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testStoreMultipleImagesAsync() async throws {\n        await storeMultipleImages()\n    \n        let diskCachePath = cache.diskStorage.directoryURL.path\n        let files = try FileManager.default.contentsOfDirectory(atPath: diskCachePath)\n        XCTAssertEqual(files.count, testKeys.count)\n    }\n    \n    func testCachedFileExists() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        let url = URL(string: key)!\n        \n        let exists = cache.imageCachedType(forKey: url.cacheKey).cached\n        XCTAssertFalse(exists)\n        \n        cache.retrieveImage(forKey: key) { result in\n            switch result {\n            case .success(let value):\n                XCTAssertNil(value.image)\n                XCTAssertEqual(value.cacheType, .none)\n            case .failure:\n                XCTFail()\n                return\n            }\n\n            self.cache.store(testImage, forKey: key, toDisk: true) { _ in\n                self.cache.retrieveImage(forKey: key) { result in\n\n                    XCTAssertNotNil(result.value?.image)\n                    XCTAssertEqual(result.value?.cacheType, .memory)\n\n                    self.cache.clearMemoryCache()\n                    self.cache.retrieveImage(forKey: key) { result in\n                        XCTAssertNotNil(result.value?.image)\n                        XCTAssertEqual(result.value?.cacheType, .disk)\n\n                        exp.fulfill()\n                    }\n                }\n            }\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCachedFileExistsAsync() async throws {\n        let key = testKeys[0]\n        let url = URL(string: key)!\n        \n        let exists = cache.imageCachedType(forKey: url.cacheKey).cached\n        XCTAssertFalse(exists)\n        \n        var result = try await cache.retrieveImage(forKey: key)\n        XCTAssertNil(result.image)\n        XCTAssertEqual(result.cacheType, .none)\n        \n        try await cache.store(testImage, forKey: key, toDisk: true)\n        \n        result = try await cache.retrieveImage(forKey: key)\n        XCTAssertNotNil(result.image)\n        XCTAssertEqual(result.cacheType, .memory)\n        \n        cache.clearMemoryCache()\n        \n        result = try await cache.retrieveImage(forKey: key)\n        XCTAssertNotNil(result.image)\n        XCTAssertEqual(result.cacheType, .disk)\n    }\n\n    func testCachedFileWithCustomPathExtensionExists() {\n        cache.diskStorage.config.pathExtension = \"jpg\"\n        let exp = expectation(description: #function)\n        \n        let key = testKeys[0]\n        let url = URL(string: key)!\n\n        cache.store(testImage, forKey: key, toDisk: true) { _ in\n            let cachePath = self.cache.cachePath(forKey: url.cacheKey)\n            XCTAssertTrue(cachePath.hasSuffix(\".jpg\"))\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testCachedFileWithCustomPathExtensionExistsAsync() async throws {\n        cache.diskStorage.config.pathExtension = \"jpg\"\n        let key = testKeys[0]\n        let url = URL(string: key)!\n        try await cache.store(testImage, forKey: key, toDisk: true)\n        let cachePath = self.cache.cachePath(forKey: url.cacheKey)\n        XCTAssertTrue(cachePath.hasSuffix(\".jpg\"))\n    }\n  \n    @MainActor func testCachedImageIsFetchedSynchronouslyFromTheMemoryCache() {\n        cache.store(testImage, forKey: testKeys[0], toDisk: false)\n        var image: KFCrossPlatformImage? = nil\n        cache.retrieveImage(forKey: testKeys[0]) { result in\n            MainActor.assumeIsolated {\n                image = try? result.get().image\n            }\n        }\n        XCTAssertEqual(testImage, image)\n    }\n    \n    func testCachedImageIsFetchedSynchronouslyFromTheMemoryCacheAsync() async throws {\n        try await cache.store(testImage, forKey: testKeys[0], toDisk: false)\n        let result = try await cache.retrieveImage(forKey: testKeys[0])\n        XCTAssertEqual(testImage, result.image)\n    }\n\n    func testIsImageCachedForKey() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        XCTAssertFalse(cache.imageCachedType(forKey: key).cached)\n        cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in\n            XCTAssertTrue(self.cache.imageCachedType(forKey: key).cached)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testIsImageCachedForKeyAsync() async throws {\n        let key = testKeys[0]\n        XCTAssertFalse(cache.imageCachedType(forKey: key).cached)\n        try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true)\n        XCTAssertTrue(cache.imageCachedType(forKey: key).cached)\n    }\n    \n    func testCleanDiskCacheNotification() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n\n        cache.diskStorage.config.expiration = .seconds(0.1)\n\n        let selfCache = self.cache\n        cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in\n            self.observer = NotificationCenter.default.addObserver(\n                forName: .KingfisherDidCleanDiskCache,\n                object: self.cache,\n                queue: .main\n            ) { noti in\n                let receivedCache = noti.object as? ImageCache\n                XCTAssertNotNil(receivedCache)\n                XCTAssertTrue(receivedCache === selfCache)\n            \n                guard let hashes = noti.userInfo?[KingfisherDiskCacheCleanedHashKey] as? [String] else {\n                    XCTFail(\"Notification should contains Strings in key 'KingfisherDiskCacheCleanedHashKey'\")\n                    exp.fulfill()\n                    return\n                }\n            \n                XCTAssertEqual(hashes.count, 1)\n                XCTAssertEqual(hashes.first!, selfCache!.hash(forKey: key))\n                exp.fulfill()\n            }\n\n            delay(2) { // File writing in disk cache has an approximate (round) creating time. 1 second is not enough.\n                self.cache.cleanExpiredDiskCache()\n            }\n        }\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    func testCannotRetrieveCacheWithProcessorIdentifier() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        let p = RoundCornerImageProcessor(cornerRadius: 40)\n        cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in\n            self.cache.retrieveImage(forKey: key, options: [.processor(p)]) { result in\n                XCTAssertNotNil(result.value)\n                XCTAssertNil(result.value!.image)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCannotRetrieveCacheWithProcessorIdentifierAsync() async throws {\n        let key = testKeys[0]\n        let p = RoundCornerImageProcessor(cornerRadius: 40)\n        try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true)\n        let result = try await cache.retrieveImage(forKey: key, options: [.processor(p)])\n        XCTAssertNotNil(result)\n        XCTAssertNil(result.image)\n    }\n\n    func testRetrieveCacheWithProcessorIdentifier() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        let p = RoundCornerImageProcessor(cornerRadius: 40)\n        cache.store(\n            testImage,\n            original: testImageData,\n            forKey: key,\n            processorIdentifier: p.identifier,\n            toDisk: true)\n        {\n            _ in\n            self.cache.retrieveImage(forKey: key, options: [.processor(p)]) { result in\n                XCTAssertNotNil(result.value?.image)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testRetrieveCacheWithProcessorIdentifierAsync() async throws {\n        let key = testKeys[0]\n        let p = RoundCornerImageProcessor(cornerRadius: 40)\n        try await cache.store(\n            testImage,\n            original: testImageData,\n            forKey: key,\n            processorIdentifier: p.identifier,\n            toDisk: true\n        )\n        let result = try await cache.retrieveImage(forKey: key, options: [.processor(p)])\n        XCTAssertNotNil(result.image)\n    }\n\n    func testDefaultCache() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        let cache = ImageCache.default\n        cache.store(testImage, forKey: key) { _ in\n            XCTAssertTrue(cache.memoryStorage.isCached(forKey: key))\n            XCTAssertTrue(cache.diskStorage.isCached(forKey: key))\n            cleanDefaultCache()\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDefaultCacheAsync() async throws {\n        let key = testKeys[0]\n        let cache = ImageCache.default\n        try await cache.store(testImage, forKey: key)\n        XCTAssertTrue(cache.memoryStorage.isCached(forKey: key))\n        XCTAssertTrue(cache.diskStorage.isCached(forKey: key))\n        cleanDefaultCache()\n    }\n    \n    func testRetrieveDiskCacheSynchronously() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        cache.store(testImage, forKey: key, toDisk: true) { _ in\n            var cacheType = self.cache.imageCachedType(forKey: key)\n            XCTAssertEqual(cacheType, .memory)\n            \n            self.cache.memoryStorage.remove(forKey: key)\n            cacheType = self.cache.imageCachedType(forKey: key)\n            XCTAssertEqual(cacheType, .disk)\n            \n            let dispatched = LockIsolated(false)\n            self.cache.retrieveImageInDiskCache(forKey: key, options:  [.loadDiskFileSynchronously]) {\n                result in\n                XCTAssertFalse(dispatched.value)\n                exp.fulfill()\n            }\n            // This should be called after the completion handler above.\n            dispatched.setValue(true)\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testRetrieveDiskCacheAsynchronously() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        cache.store(testImage, forKey: key, toDisk: true) { _ in\n            var cacheType = self.cache.imageCachedType(forKey: key)\n            XCTAssertEqual(cacheType, .memory)\n            \n            self.cache.memoryStorage.remove(forKey: key)\n            cacheType = self.cache.imageCachedType(forKey: key)\n            XCTAssertEqual(cacheType, .disk)\n            \n            let dispatched = LockIsolated(false)\n            self.cache.retrieveImageInDiskCache(forKey: key, options: nil) {\n                result in\n                XCTAssertTrue(dispatched.value)\n                exp.fulfill()\n            }\n            // This should be called before the completion handler above.\n            dispatched.setValue(true)\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n\t#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)\n\t    func testModifierShouldOnlyApplyForFinalResultWhenMemoryLoad() {\n\t        let exp = expectation(description: #function)\n\t        let key = testKeys[0]\n\t        \n\t        let modifierCalled = LockIsolated(false)\n\t        let modifier = AnyImageModifier { image in\n\t            modifierCalled.setValue(true)\n\t            return image.withRenderingMode(.alwaysTemplate)\n\t        }\n\t        \n\t        cache.store(testImage, original: testImageData, forKey: key) { _ in\n\t            self.cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)]) { result in\n\t                XCTAssertEqual(result.value?.image?.renderingMode, .automatic)\n\t                XCTAssertFalse(modifierCalled.value)\n\t                exp.fulfill()\n\t            }\n\t        }\n\t        waitForExpectations(timeout: 3, handler: nil)\n\t    }\n\t    \n\t    func testModifierShouldOnlyApplyForFinalResultWhenMemoryLoadAsync() async throws {\n\t        let key = testKeys[0]\n\n\t        let modifierCalled = LockIsolated(false)\n\t        let modifier = AnyImageModifier { image in\n\t            modifierCalled.setValue(true)\n\t            return image.withRenderingMode(.alwaysTemplate)\n\t        }\n\n\t        try await cache.store(testImage, original: testImageData, forKey: key)\n\t        let result = try await cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)])\n\t        XCTAssertFalse(modifierCalled.value)\n\t        XCTAssertEqual(result.image?.renderingMode, .automatic)\n\t    }\n\n\t    func testModifierShouldOnlyApplyForFinalResultWhenDiskLoad() {\n\t        let exp = expectation(description: #function)\n\t        let key = testKeys[0]\n\n\t        let modifierCalled = LockIsolated(false)\n\t        let modifier = AnyImageModifier { image in\n\t            modifierCalled.setValue(true)\n\t            return image.withRenderingMode(.alwaysTemplate)\n\t        }\n\n\t        cache.store(testImage, original: testImageData, forKey: key) { _ in\n\t            self.cache.clearMemoryCache()\n\t            self.cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)]) { result in\n\t                XCTAssertEqual(result.value?.image?.renderingMode, .automatic)\n\t                XCTAssertFalse(modifierCalled.value)\n\t                exp.fulfill()\n\t            }\n\t        }\n\t        waitForExpectations(timeout: 3, handler: nil)\n\t    }\n\t    \n\t    func testModifierShouldOnlyApplyForFinalResultWhenDiskLoadAsync() async throws {\n\t        let key = testKeys[0]\n\t        let modifierCalled = LockIsolated(false)\n\t        let modifier = AnyImageModifier { image in\n\t            modifierCalled.setValue(true)\n\t            return image.withRenderingMode(.alwaysTemplate)\n\t        }\n\t        \n\t        try await cache.store(testImage, original: testImageData, forKey: key)\n\t        cache.clearMemoryCache()\n\t        let result = try await cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)])\n\t        XCTAssertFalse(modifierCalled.value)\n\t        // The renderingMode is expected to be the default value `.automatic`. The image modifier should only apply to\n\t        // the image manager result.\n\t        XCTAssertEqual(result.image?.renderingMode, .automatic)\n\t    }\n\t#endif\n    \n    func testStoreToMemoryWithExpiration() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        cache.store(\n            testImage,\n            original: testImageData,\n            forKey: key,\n            options: KingfisherParsedOptionsInfo([.memoryCacheExpiration(.seconds(0.5))]),\n            toDisk: true)\n        {\n            _ in\n            XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)\n            delay(1) {\n                XCTAssertEqual(self.cache.imageCachedType(forKey: key), .disk)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    func testStoreToMemoryWithExpirationAsync() async throws {\n        let key = testKeys[0]\n        try await cache.store(\n            testImage,\n            original: testImageData,\n            forKey: key,\n            options: KingfisherParsedOptionsInfo([.memoryCacheExpiration(.seconds(0.5))]),\n            toDisk: true\n        )\n        XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)\n        // After 1 sec, the cache only remains on disk.\n        try await Task.sleep(nanoseconds: NSEC_PER_SEC)\n        XCTAssertEqual(self.cache.imageCachedType(forKey: key), .disk)\n    }\n    \n    func testStoreToDiskWithExpiration() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        cache.store(\n            testImage,\n            original: testImageData,\n            forKey: key,\n            options: KingfisherParsedOptionsInfo([.diskCacheExpiration(.expired)]),\n            toDisk: true)\n        {\n            _ in\n            XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)\n            self.cache.clearMemoryCache()\n            XCTAssertEqual(self.cache.imageCachedType(forKey: key), .none)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testStoreToDiskWithExpirationAsync() async throws {\n        let key = testKeys[0]\n        try await cache.store(\n            testImage,\n            original: testImageData,\n            forKey: key,\n            options: KingfisherParsedOptionsInfo([.diskCacheExpiration(.expired)]),\n            toDisk: true\n        )\n        \n        XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)\n        self.cache.clearMemoryCache()\n        XCTAssertEqual(self.cache.imageCachedType(forKey: key), .none)\n    }\n\n    func testCalculateDiskStorageSize() {\n        let exp = expectation(description: #function)\n        cache.calculateDiskStorageSize { result in\n            switch result {\n            case .success(let size):\n                XCTAssertEqual(size, 0)\n                self.storeMultipleImages {\n                    self.cache.calculateDiskStorageSize { result in\n                        switch result {\n                        case .success(let size):\n                            XCTAssertEqual(size, UInt(testImagePNGData.count * testKeys.count))\n                        case .failure:\n                            XCTAssert(false)\n                        }\n                        exp.fulfill()\n                    }\n                }\n            case .failure:\n                XCTAssert(false)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDiskCacheStillWorkWhenFolderDeletedExternally() {\n        let exp = expectation(description: #function)\n        let key = testKeys[0]\n        let url = URL(string: key)!\n        \n        let exists = cache.imageCachedType(forKey: url.cacheKey)\n        XCTAssertEqual(exists, .none)\n        \n        cache.store(testImage, forKey: key, toDisk: true) { _ in\n            self.cache.retrieveImage(forKey: key) { result in\n\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value?.cacheType, .memory)\n\n                self.cache.clearMemoryCache()\n                self.cache.retrieveImage(forKey: key) { result in\n                    XCTAssertNotNil(result.value?.image)\n                    XCTAssertEqual(result.value?.cacheType, .disk)\n                    self.cache.clearMemoryCache()\n                    \n                    try! FileManager.default.removeItem(at: self.cache.diskStorage.directoryURL)\n                    \n                    let exists = self.cache.imageCachedType(forKey: url.cacheKey)\n                    XCTAssertEqual(exists, .none)\n                    \n                    self.cache.store(testImage, forKey: key, toDisk: true) { _ in\n                        self.cache.clearMemoryCache()\n                        let cacheType = self.cache.imageCachedType(forKey: url.cacheKey)\n                        XCTAssertEqual(cacheType, .disk)\n                        exp.fulfill()\n                    }\n                }\n            }\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDiskCacheCalculateSizeWhenFolderDeletedExternally() {\n        let exp = expectation(description: #function)\n        \n        let key = testKeys[0]\n        \n        cache.calculateDiskStorageSize { result in\n            XCTAssertEqual(result.value, 0)\n            \n            self.cache.store(testImage, forKey: key, toDisk: true) { _ in\n                self.cache.calculateDiskStorageSize { result in\n                    XCTAssertEqual(result.value, UInt(testImagePNGData.count))\n                    \n                    try! FileManager.default.removeItem(at: self.cache.diskStorage.directoryURL)\n                    self.cache.calculateDiskStorageSize { result in\n                        XCTAssertEqual(result.value, 0)\n                        exp.fulfill()\n                    }\n                    \n                }\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testCalculateDiskStorageSizeAsync() async throws {\n        let size = try await cache.diskStorageSize\n        XCTAssertEqual(size, 0)\n        await storeMultipleImages()\n        let newSize = try await cache.diskStorageSize\n        XCTAssertEqual(newSize, UInt(testImagePNGData.count * testKeys.count))\n    }\n    \n    func testStoreFileWithForcedExtension() async throws {\n        let key = testKeys[0]\n        try await cache.store(testImage, forKey: key, forcedExtension: \"jpg\", toDisk: true)\n    \n        let pathWithoutExtension = cache.cachePath(forKey: key)\n        XCTAssertFalse(FileManager.default.fileExists(atPath: pathWithoutExtension))\n        \n        let pathWithExtension = cache.cachePath(forKey: key, forcedExtension: \"jpg\")\n        XCTAssertTrue(FileManager.default.fileExists(atPath: pathWithExtension))\n        \n        XCTAssertEqual(cache.imageCachedType(forKey: key), .memory)\n        XCTAssertEqual(cache.imageCachedType(forKey: key, forcedExtension: \"jpg\"), .memory)\n        \n        cache.clearMemoryCache()\n        XCTAssertEqual(cache.imageCachedType(forKey: key), .none)\n        XCTAssertEqual(cache.imageCachedType(forKey: key, forcedExtension: \"jpg\"), .disk)\n    }\n    \n    func testPossibleCacheFileURLIfOnDiskNotCached() {\n        let url = URL(string: \"https://example.com/photo\")!\n        let resource = LivePhotoResource(downloadURL: url)\n        \n        let fileURL = cache.possibleCacheFileURLIfOnDisk(\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            referenceFileType: .heic\n        )\n            \n        // Not cached\n        XCTAssertNil(fileURL)\n    }\n    \n    func testPossibleCacheFileURLIfOnDiskCachedWithWrongFileType() async throws {\n        let url = URL(string: \"https://example.com/photo\")!\n        let resource = LivePhotoResource(downloadURL: url, fileType: .heic)\n        \n        // Cache without a file type extension\n        try await cache.storeToDisk(\n            testImageData,\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier\n        )\n        \n        let fileURL = cache.possibleCacheFileURLIfOnDisk(\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            referenceFileType: .heic\n        )\n            \n        // Not cached\n        XCTAssertNil(fileURL)\n    }\n    \n    func testPossibleCacheFileURLIfOnDiskCachedWithExplicitFileType() async throws {\n        let url = URL(string: \"https://example.com/photo\")!\n        let resource = LivePhotoResource(downloadURL: url, fileType: .heic)\n        \n        // Cache without a file type extension\n        try await cache.storeToDisk(\n            testImageData,\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: \"heic\"\n        )\n        \n        let fileURL = cache.possibleCacheFileURLIfOnDisk(\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            referenceFileType: .heic\n        )\n\n        let result = try XCTUnwrap(fileURL)\n        XCTAssertTrue(result.absoluteString.hasSuffix(\".heic\"))\n    }\n    \n    func testPossibleCacheFileURLIfOnDiskCachedGuessingFileTypeNotHit() async throws {\n        let url = URL(string: \"https://example.com/photo\")!\n        let resource = LivePhotoResource(downloadURL: url, fileType: .heic)\n\n        let fileURL = cache.possibleCacheFileURLIfOnDisk(\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            referenceFileType: .other(\"\")\n        )\n\n        XCTAssertNil(fileURL)\n    }\n    \n    func testPossibleCacheFileURLIfOnDiskCachedGuessingFileType() async throws {\n        let url = URL(string: \"https://example.com/photo\")!\n        let resource = LivePhotoResource(downloadURL: url, fileType: .heic)\n        \n        // Cache without a file type extension\n        try await cache.storeToDisk(\n            testImageData,\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: \"heic\"\n        )\n        \n        let fileURL = cache.possibleCacheFileURLIfOnDisk(\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            referenceFileType: .other(\"\")\n        )\n\n        let result = try XCTUnwrap(fileURL)\n        XCTAssertTrue(result.absoluteString.hasSuffix(\".heic\"))\n    }\n    \n    func testPossibleCacheFileURLIfOnDiskCachedArbitraryFileType() async throws {\n        let url = URL(string: \"https://example.com/photo\")!\n        let resource = LivePhotoResource(downloadURL: url, fileType: .heic)\n        \n        // Cache without a file type extension\n        try await cache.storeToDisk(\n            testImageData,\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: \"myExt\"\n        )\n        \n        let fileURL = cache.possibleCacheFileURLIfOnDisk(\n            forKey: resource.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            referenceFileType: .other(\"myExt\")\n        )\n\n        let result = try XCTUnwrap(fileURL)\n        XCTAssertTrue(result.absoluteString.hasSuffix(\".myExt\"))\n    }\n\n#if !os(macOS) && !os(watchOS)\n    func testKingfisherWrapperUIApplicationSharedReturnsNilInUnitTest() {\n        // UIApplication.shared is not available in some Unit Tests contexts.\n        // This tests that accessing it via KingfisherWrapper does not cause a crash.\n        XCTAssertNil(KingfisherWrapper<UIApplication>.shared)\n    }\n#endif\n\n    // MARK: - Helper\n    private func storeMultipleImages(_ completionHandler: @escaping () -> Void) {\n        let group = DispatchGroup()\n        testKeys.forEach {\n            group.enter()\n            cache.store(testImage, original: testImageData, forKey: $0, toDisk: true) { _ in\n                group.leave()\n            }\n        }\n        group.notify(queue: .main, execute: completionHandler)\n    }\n    \n    private func storeMultipleImages() async {\n        await withCheckedContinuation {\n            storeMultipleImages($0.resume)\n        }\n    }\n}\n\n@dynamicMemberLookup\npublic final class LockIsolated<Value>: @unchecked Sendable {\n  private var _value: Value\n  private let lock = NSRecursiveLock()\n\n  /// Initializes lock-isolated state around a value.\n  ///\n  /// - Parameter value: A value to isolate with a lock.\n  public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {\n    self._value = try value()\n  }\n\n  public subscript<Subject: Sendable>(dynamicMember keyPath: KeyPath<Value, Subject>) -> Subject {\n    self.lock.sync {\n      self._value[keyPath: keyPath]\n    }\n  }\n\n  /// Perform an operation with isolated access to the underlying value.\n  ///\n  /// Useful for modifying a value in a single transaction.\n  ///\n  /// ```swift\n  /// // Isolate an integer for concurrent read/write access:\n  /// var count = LockIsolated(0)\n  ///\n  /// func increment() {\n  ///   // Safely increment it:\n  ///   self.count.withValue { $0 += 1 }\n  /// }\n  /// ```\n  ///\n  /// - Parameter operation: An operation to be performed on the the underlying value with a lock.\n  /// - Returns: The result of the operation.\n  public func withValue<T: Sendable>(\n    _ operation: @Sendable (inout Value) throws -> T\n  ) rethrows -> T {\n    try self.lock.sync {\n      var value = self._value\n      defer { self._value = value }\n      return try operation(&value)\n    }\n  }\n\n  /// Overwrite the isolated value with a new value.\n  ///\n  /// ```swift\n  /// // Isolate an integer for concurrent read/write access:\n  /// var count = LockIsolated(0)\n  ///\n  /// func reset() {\n  ///   // Reset it:\n  ///   self.count.setValue(0)\n  /// }\n  /// ```\n  ///\n  /// > Tip: Use ``withValue(_:)`` instead of ``setValue(_:)`` if the value being set is derived\n  /// > from the current value. That is, do this:\n  /// >\n  /// > ```swift\n  /// > self.count.withValue { $0 += 1 }\n  /// > ```\n  /// >\n  /// > ...and not this:\n  /// >\n  /// > ```swift\n  /// > self.count.setValue(self.count + 1)\n  /// > ```\n  /// >\n  /// > ``withValue(_:)`` isolates the entire transaction and avoids data races between reading and\n  /// > writing the value.\n  ///\n  /// - Parameter newValue: The value to replace the current isolated value with.\n  public func setValue(_ newValue: @autoclosure @Sendable () throws -> Value) rethrows {\n    try self.lock.sync {\n      self._value = try newValue()\n    }\n  }\n}\n\nfinal class ImageCacheAsyncCachedTypeTests: XCTestCase {\n\n    var cache: ImageCache!\n\n    override func setUp() {\n        super.setUp()\n\n        let uuid = UUID().uuidString\n        cache = ImageCache(name: \"test-\\(uuid)\")\n    }\n\n    override func tearDown() {\n        clearCaches([cache])\n        cache = nil\n\n        super.tearDown()\n    }\n\n    func testImageCachedTypeAsyncMemoryHit() {\n        let expectation = expectation(description: \"memory hit\")\n        let key = \"memory-hit\"\n        let computedKey = key.computedKey(with: DefaultImageProcessor.default.identifier)\n\n        cache.memoryStorage.store(value: testImage, forKey: computedKey)\n\n        cache.imageCachedTypeAsync(forKey: key, callbackQueue: .untouch) { type in\n            XCTAssertEqual(type, .memory)\n            XCTAssertTrue(Thread.isMainThread)\n            expectation.fulfill()\n        }\n\n        waitForExpectations(timeout: 2)\n    }\n\n    func testImageCachedTypeAsyncDiskHitRunsOffMainThreadWhenCallbackUntouch() {\n        let expectation = expectation(description: \"disk hit\")\n        let key = \"disk-hit\"\n        let computedKey = key.computedKey(with: DefaultImageProcessor.default.identifier)\n\n        try? cache.diskStorage.store(value: testImageData, forKey: computedKey, expiration: .never)\n\n        cache.imageCachedTypeAsync(forKey: key, callbackQueue: .untouch) { type in\n            XCTAssertEqual(type, .disk)\n            XCTAssertFalse(Thread.isMainThread)\n            expectation.fulfill()\n        }\n\n        waitForExpectations(timeout: 2)\n    }\n\n    func testImageCachedTypeAsyncNone() {\n        let expectation = expectation(description: \"none\")\n        let key = \"missing\"\n\n        cache.imageCachedTypeAsync(forKey: key, callbackQueue: .untouch) { type in\n            XCTAssertEqual(type, .none)\n            expectation.fulfill()\n        }\n\n        waitForExpectations(timeout: 2)\n    }\n\n    func testImageCachedTypeAsyncDeliversOnMainQueueWhenRequested() {\n        let expectation = expectation(description: \"main callback\")\n        let key = \"main-callback\"\n        let computedKey = key.computedKey(with: DefaultImageProcessor.default.identifier)\n\n        try? cache.diskStorage.store(value: testImageData, forKey: computedKey, expiration: .never)\n\n        DispatchQueue.global().async {\n            self.cache.imageCachedTypeAsync(forKey: key, callbackQueue: .mainAsync) { type in\n                XCTAssertEqual(type, .disk)\n                XCTAssertTrue(Thread.isMainThread)\n                expectation.fulfill()\n            }\n        }\n\n        waitForExpectations(timeout: 2)\n    }\n\n    func testImageCachedTypeAsyncAwaitReturns() async {\n        let key = \"async-await\"\n        let computedKey = key.computedKey(with: DefaultImageProcessor.default.identifier)\n        try? cache.diskStorage.store(value: testImageData, forKey: computedKey, expiration: .never)\n\n        let type = await cache.imageCachedTypeAsync(forKey: key)\n        XCTAssertEqual(type, .disk)\n    }\n}\n\nextension LockIsolated where Value: Sendable {\n  /// The lock-isolated value.\n  public var value: Value {\n    self.lock.sync {\n      self._value\n    }\n  }\n}\n\nextension NSRecursiveLock {\n  @inlinable @discardableResult\n  @_spi(Internals) public func sync<R>(work: () throws -> R) rethrows -> R {\n    self.lock()\n    defer { self.unlock() }\n    return try work()\n  }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/ImageDataProviderTests.swift",
    "content": "//\n//  ImageDataProviderTests.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/18.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass ImageDataProviderTests: XCTestCase {\n\n    func testCacheKeySelectionForPickerProviders() {\n        #if os(iOS) || os(macOS) || os(visionOS)\n        let uuid: () -> String = { \"uuid\" }\n\n        if #available(iOS 16.0, macOS 13.0, *) {\n            XCTAssertEqual(\n                PhotosPickerItemImageDataProvider._cacheKey(providedCacheKey: \"custom\", itemIdentifier: nil, uuidString: uuid),\n                \"custom\"\n            )\n            XCTAssertEqual(\n                PhotosPickerItemImageDataProvider._cacheKey(providedCacheKey: nil, itemIdentifier: \"item\", uuidString: uuid),\n                \"item\"\n            )\n            XCTAssertEqual(\n                PhotosPickerItemImageDataProvider._cacheKey(providedCacheKey: nil, itemIdentifier: nil, uuidString: uuid),\n                \"uuid\"\n            )\n        }\n\n        if #available(iOS 14.0, macOS 13.0, *) {\n            XCTAssertEqual(\n                PHPickerResultImageDataProvider._cacheKey(\n                    providedCacheKey: \"custom\",\n                    assetIdentifier: nil,\n                    contentTypeIdentifier: \"public.image\",\n                    uuidString: uuid\n                ),\n                \"custom\"\n            )\n            XCTAssertEqual(\n                PHPickerResultImageDataProvider._cacheKey(\n                    providedCacheKey: nil,\n                    assetIdentifier: \"asset\",\n                    contentTypeIdentifier: \"public.image\",\n                    uuidString: uuid\n                ),\n                \"asset_public.image\"\n            )\n            XCTAssertEqual(\n                PHPickerResultImageDataProvider._cacheKey(\n                    providedCacheKey: nil,\n                    assetIdentifier: nil,\n                    contentTypeIdentifier: \"public.image\",\n                    uuidString: uuid\n                ),\n                \"uuid_public.image\"\n            )\n        }\n        #endif\n    }\n    \n    func testLocalFileImageDataProvider() {\n        let document = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)\n        let fileURL = document.appendingPathComponent(\"test\")\n        try! testImageData.write(to: fileURL)\n        \n        let provider = LocalFileImageDataProvider(fileURL: fileURL)\n        XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey)\n        XCTAssertEqual(fileURL.cacheKey, fileURL.localFileCacheKey)\n        \n        XCTAssertEqual(provider.fileURL, fileURL)\n        \n        let exp = expectation(description: #function)\n        provider.data { result in\n            XCTAssertEqual(result.value, testImageData)\n            try! FileManager.default.removeItem(at: fileURL)\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testLocalFileImageDataProviderAsync() async {\n        let fm = FileManager.default\n        let document = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)\n        let fileURL = document.appendingPathComponent(\"test\")\n        try! testImageData.write(to: fileURL)\n        \n        let provider = LocalFileImageDataProvider(fileURL: fileURL)\n        XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey)\n        XCTAssertEqual(fileURL.cacheKey, fileURL.localFileCacheKey)\n        \n        XCTAssertEqual(provider.fileURL, fileURL)\n        \n        let value = try? await provider.data\n        XCTAssertEqual(value, testImageData)\n        try! fm.removeItem(at: fileURL)\n    }\n\n    func testLocalFileImageDataProviderMainQueue() {\n        let fm = FileManager.default\n        let document = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)\n        let fileURL = document.appendingPathComponent(\"test\")\n        try! testImageData.write(to: fileURL)\n        \n        let provider = LocalFileImageDataProvider(fileURL: fileURL, loadingQueue: .mainCurrentOrAsync)\n        XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey)\n        XCTAssertEqual(provider.fileURL, fileURL)\n        \n        let called = LockIsolated(false)\n        provider.data { result in\n            XCTAssertEqual(result.value, testImageData)\n            try! FileManager.default.removeItem(at: fileURL)\n            called.setValue(true)\n        }\n\n        XCTAssertTrue(called.value)\n    }\n    \n    func testAVAssetImageDataProviderCacheKeyVariesForRemote() {\n        let remoteURL1 = URL(string: \"https://example.com/1/hello.mp4\")!\n        let remoteURL2 = URL(string: \"https://example.com/2/hello.mp4\")!\n        \n        let provider1 = AVAssetImageDataProvider(assetURL: remoteURL1, seconds: 10)\n        XCTAssertEqual(provider1.cacheKey, \"https://example.com/1/hello.mp4_10.0\")\n        \n        let provider2 = AVAssetImageDataProvider(assetURL: remoteURL2, seconds: 10)\n        XCTAssertNotEqual(provider1.cacheKey, provider2.cacheKey)\n    }\n    \n    // AVAssetImageDataProvider fix for appending to #1825\n    func testAVAssetImageDataProviderCacheKeyConsistForDifferentAppSandbox() {\n        let localURL1 = URL(string: \"file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/DEF/Kingfisher-Demo.app/video/hello.mp4\")!\n        let localURL2 = URL(string: \"file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/XYZ/Kingfisher-Demo.app/video/hello.mp4\")!\n        \n        let provider1 = AVAssetImageDataProvider(assetURL: localURL1, seconds: 10)\n        XCTAssertEqual(provider1.cacheKey, \"\\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/video/hello.mp4_10.0\")\n    \n        let provider2 = AVAssetImageDataProvider(assetURL: localURL2, seconds: 10)\n        XCTAssertEqual(provider1.cacheKey, provider2.cacheKey)\n    }\n    \n\n    func testLocalFileImageDataProviderMainQueueAsync() async {\n        let fm = FileManager.default\n        let document = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)\n        let fileURL = document.appendingPathComponent(\"test\")\n        try! testImageData.write(to: fileURL)\n        \n        let provider = LocalFileImageDataProvider(fileURL: fileURL, loadingQueue: .mainCurrentOrAsync)\n        XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey)\n        XCTAssertEqual(provider.fileURL, fileURL)\n        \n        var called = false\n        let value = try? await provider.data\n        XCTAssertEqual(value, testImageData)\n        try! fm.removeItem(at: fileURL)\n        called = true\n\n        XCTAssertTrue(called)\n    }\n    \n    func testLocalFileCacheKey() {\n        let url1 = URL(string: \"file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/DEF/Kingfisher-Demo.app/images/kingfisher-1.jpg\")!\n        XCTAssertEqual(url1.localFileCacheKey, \"\\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg\")\n    \n        let url2 = URL(string: \"file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.app/images/kingfisher-1.jpg\")!\n        XCTAssertEqual(url2.localFileCacheKey, \"\\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg\")\n        \n        let url3 = URL(string: \"file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.app/images/kingfisher-1.jpg?foo=bar\")!\n        XCTAssertEqual(url3.localFileCacheKey, \"\\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg?foo=bar\")\n        \n        let url4 = URL(string: \"file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.appex/images/kingfisher-1.jpg\")!\n        XCTAssertEqual(url4.localFileCacheKey, \"\\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.appex/images/kingfisher-1.jpg\")\n        \n        let url5 = URL(string: \"file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.other/images/kingfisher-1.jpg\")!\n        XCTAssertEqual(url5.localFileCacheKey, \"\\(URL.localFileCacheKeyPrefix)///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.other/images/kingfisher-1.jpg\")\n    }\n    \n    func testLocalFileExplicitKey() {\n        let url1 = URL(string: \"file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/DEF/Kingfisher-Demo.app/images/kingfisher-1.jpg\")!\n        let imageResource = KF.ImageResource(downloadURL: url1, cacheKey: \"hello\")\n        let source = imageResource.convertToSource()\n        XCTAssertEqual(source.cacheKey, \"hello\")\n    }\n    \n    func testBase64ImageDataProvider() {\n        let base64String = testImageData.base64EncodedString()\n        let provider = Base64ImageDataProvider(base64String: base64String, cacheKey: \"123\")\n        XCTAssertEqual(provider.cacheKey, \"123\")\n        var syncCalled = false\n        provider.data { result in\n            XCTAssertEqual(result.value, testImageData)\n            syncCalled = true\n        }\n        \n        XCTAssertTrue(syncCalled)\n    }\n    \n}\n"
  },
  {
    "path": "Tests/KingfisherTests/ImageDownloaderTests.swift",
    "content": "//\n//  ImageDownloaderTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/10.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n\nimport XCTest\n@testable import Kingfisher\n\nclass ImageDownloaderTests: XCTestCase {\n\n    var downloader: ImageDownloader!\n\n    override class func setUp() {\n        super.setUp()\n        LSNocilla.sharedInstance().start()\n    }\n    \n    override class func tearDown() {\n        LSNocilla.sharedInstance().stop()\n        super.tearDown()\n    }\n    \n    override func setUp() {\n        super.setUp()\n        downloader = ImageDownloader(name: \"test\")\n    }\n    \n    override func tearDown() {\n        LSNocilla.sharedInstance().clearStubs()\n        downloader = nil\n        super.tearDown()\n    }\n    \n    func testDownloadAnImage() {\n        let exp = expectation(description: #function)\n        \n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        downloader.downloadImage(with: url) { result in\n            XCTAssertNotNil(result.value)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadAnImageAsync() async throws {\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        let result = try await downloader.downloadImage(with: url, options: .empty)\n        XCTAssertEqual(result.originalData, testImageData)\n    }\n    \n    func testDownloadMultipleImages() {\n        let exp = expectation(description: #function)\n        let group = DispatchGroup()\n        \n        for url in testURLs {\n            group.enter()\n            stub(url, data: testImageData)\n            downloader.downloadImage(with: url) { result in\n                XCTAssertNotNil(result.value)\n                group.leave()\n            }\n        }\n        \n        group.notify(queue: .main, execute: exp.fulfill)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadMultipleImagesAsync() async throws {\n        try await withThrowingTaskGroup(of: ImageLoadingResult.self) { group in\n            for url in testURLs {\n                stub(url, data: testImageData)\n                group.addTask {\n                    try await self.downloader.downloadImage(with: url)\n                }\n            }\n            \n            for try await result in group {\n                XCTAssertEqual(result.originalData, testImageData)\n            }\n        }\n    }\n    \n    func testDownloadAnImageWithMultipleCallback() {\n        let exp = expectation(description: #function)\n        \n        let group = DispatchGroup()\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        for _ in 0...5 {\n            group.enter()\n            downloader.downloadImage(with: url) { result in\n                XCTAssertNotNil(result.value)\n                group.leave()\n            }\n        }\n\n        group.notify(queue: .main, execute: exp.fulfill)\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    func testDownloadWithModifyingRequest() {\n        let exp = expectation(description: #function)\n\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        let modifier = URLModifier(url: url)\n        \n        let someURL = URL(string: \"some_strange_url\")!\n        let task = downloader.downloadImage(with: someURL, options: [.requestModifier(modifier)]) { result in\n            XCTAssertNotNil(result.value)\n            XCTAssertEqual(result.value?.url, url)\n            exp.fulfill()\n        }\n        XCTAssertTrue(task.isInitialized)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testDownloadWithAsyncModifyingRequest() {\n        let exp = expectation(description: #function)\n        let downloadTaskStarted = LockIsolated(false)\n\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let asyncModifier = AsyncURLModifier(url: url, onDownloadTaskStarted: { task in\n            XCTAssertNotNil(task)\n            downloadTaskStarted.setValue(true)\n        })\n\n        let someURL = URL(string: \"some_strange_url\")!\n        let task = downloader.downloadImage(with: someURL, options: [.requestModifier(asyncModifier)]) { result in\n            XCTAssertNotNil(result.value)\n            XCTAssertEqual(result.value?.url, url)\n            XCTAssertTrue(downloadTaskStarted.value)\n            exp.fulfill()\n        }\n        // The returned task is nil since the download is not starting immediately.\n        XCTAssertFalse(task.isInitialized)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testDownloadWithModifyingRequestToNil() {\n        let nilModifier = AnyModifier { _ in\n            return nil\n        }\n\n        let exp = expectation(description: #function)\n        let someURL = URL(string: \"some_strange_url\")!\n        downloader.downloadImage(with: someURL, options: [.requestModifier(nilModifier)]) { result in\n            XCTAssertNotNil(result.error)\n            guard case .requestError(reason: .emptyRequest) = result.error! else {\n                XCTFail()\n                fatalError()\n            }\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testServerInvalidStatusCode() {\n        let exp = expectation(description: #function)\n        \n        let url = testURLs[0]\n        stub(url, data: testImageData, statusCode: 404)\n        \n        downloader.downloadImage(with: url) { result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isInvalidResponseStatusCode(404))\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadResultErrorAndRetry() {\n        let exp = expectation(description: #function)\n        \n        let url = testURLs[0]\n\n        stub(url, errorCode: -1)\n        downloader.downloadImage(with: url) { result in\n            XCTAssertNotNil(result.error)\n            \n            LSNocilla.sharedInstance().clearStubs()\n\n            stub(url, data: testImageData)\n            // Retry the download\n            self.downloader.downloadImage(with: url) { result in\n                XCTAssertNil(result.error)\n                exp.fulfill()\n            }\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadEmptyURL() {\n        let exp = expectation(description: #function)\n        \n        let modifier = URLModifier(url: nil)\n        \n        let url = URL(string: \"http://onevcat.com\")!\n        downloader.downloadImage(\n            with: url,\n            options: [.requestModifier(modifier)],\n            progressBlock: { received, totalSize in XCTFail(\"The progress block should not be called.\") })\n        {\n            result in\n            XCTAssertNotNil(result.error)\n            if case .requestError(reason: .invalidURL(let request)) = result.error! {\n                XCTAssertNil(request.url)\n            } else {\n                XCTFail()\n            }\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadTaskProperty() {\n        let task = downloader.downloadImage(with: URL(string: \"1234\")!)\n        XCTAssertNotNil(task, \"The task should exist.\")\n    }\n    \n    func testCancelDownloadTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData, length: 123)\n        \n        let task = downloader.downloadImage(\n            with: url,\n            progressBlock: { _, _ in XCTFail() })\n        {\n            result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            delay(0.1) { exp.fulfill() }\n        }\n        \n        XCTAssertTrue(task.isInitialized)\n        task.cancel()\n\n        _ = stub.go()\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCancelDownloadTaskAsync() async throws {\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData, length: 123)\n        \n        let checker = CallingChecker()\n        try await checker.checkCancelBehavior(stub: stub) {\n            _ = try await self.downloader.downloadImage(with: url)\n        }\n    }\n\n    func testCancelOneDownloadTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n\n        let group = DispatchGroup()\n\n        group.enter()\n        let task1 = downloader.downloadImage(with: url) { result in\n            XCTAssertNotNil(result.error)\n            group.leave()\n        }\n\n        group.enter()\n        _ = downloader.downloadImage(with: url) { result in\n            XCTAssertNotNil(result.value?.image)\n            group.leave()\n        }\n\n        task1.cancel()\n        delay(0.1) { _ = stub.go() }\n        group.notify(queue: .main) {\n            delay(0.1) { exp.fulfill() }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCancelAllDownloadTasks() {\n        let exp = expectation(description: #function)\n\n        let url1 = testURLs[0]\n        let stub1 = delayedStub(url1, data: testImageData)\n\n        let url2 = testURLs[1]\n        let stub2 = delayedStub(url2, data: testImageData)\n\n        let group = DispatchGroup()\n\n        let urls = [url1, url1, url2]\n        urls.forEach {\n            group.enter()\n            downloader.downloadImage(with: $0) { result in\n                XCTAssertNotNil(result.error)\n                XCTAssertTrue(result.error!.isTaskCancelled)\n                group.leave()\n            }\n        }\n\n        delay(0.1) {\n            self.downloader.cancelAll()\n            _ = stub1.go()\n            _ = stub2.go()\n        }\n        group.notify(queue: .main) {\n            delay(0.1) { exp.fulfill() }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCancelDownloadTaskForURL() {\n        let exp = expectation(description: #function)\n        \n        let url1 = testURLs[0]\n        let stub1 = delayedStub(url1, data: testImageData)\n        \n        let url2 = testURLs[1]\n        let stub2 = delayedStub(url2, data: testImageData)\n        \n        let group = DispatchGroup()\n        \n        group.enter()\n        downloader.downloadImage(with: url1) { result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            group.leave()\n        }\n        \n        group.enter()\n        downloader.downloadImage(with: url1) { result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            group.leave()\n        }\n        \n        group.enter()\n        downloader.downloadImage(with: url2) { result in\n            XCTAssertNotNil(result.value)\n            group.leave()\n        }\n        \n        delay(0.1) {\n            self.downloader.cancel(url: url1)\n            _ = stub1.go()\n            _ = stub2.go()\n        }\n        \n        group.notify(queue: .main) {\n            delay(0.1) { exp.fulfill() }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    // Issue 532 https://github.com/onevcat/Kingfisher/issues/532#issuecomment-305644311\n    func testCancelThenRestartSameDownload() {\n        let exp = expectation(description: #function)\n        \n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData, length: 123)\n\n        let group = DispatchGroup()\n        \n        group.enter()\n        let downloadTask = downloader.downloadImage(\n            with: url,\n            progressBlock: { _, _ in XCTFail()})\n        {\n            result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            group.leave()\n        }\n        \n        XCTAssertTrue(downloadTask.isInitialized)\n        downloadTask.cancel()\n        _ = stub.go()\n        \n        group.enter()\n        downloader.downloadImage(with: url) {\n            result in\n            XCTAssertNotNil(result.value)\n            if let error = result.error {\n                print(error)\n            }\n            group.leave()\n        }\n        \n        group.notify(queue: .main) {\n            delay(0.1) { exp.fulfill() }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadTaskNilWithNilURL() {\n        let modifier = URLModifier(url: nil)\n        let downloadTask = downloader.downloadImage(with: URL(string: \"url\")!, options: [.requestModifier(modifier)])\n        XCTAssertFalse(downloadTask.isInitialized)\n    }\n    \n    func testDownloadWithProcessor() {\n        let exp = expectation(description: #function)\n        \n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let p = RoundCornerImageProcessor(cornerRadius: 40)\n        let roundCornered = testImage.kf.image(withRoundRadius: 40, fit: testImage.kf.size)\n        \n        downloader.downloadImage(with: url, options: [.processor(p)]) { result in\n            XCTAssertNotNil(result.value)\n            let image = result.value!.image\n            XCTAssertFalse(image.renderEqual(to: testImage))\n            XCTAssertTrue(image.renderEqual(to: roundCornered))\n            XCTAssertEqual(result.value!.originalData, testImageData)\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadWithDifferentProcessors() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n\n        let p1 = RoundCornerImageProcessor(cornerRadius: 40)\n        let roundCornered = testImage.kf.image(withRoundRadius: 40, fit: testImage.kf.size)\n\n        let p2 = BlurImageProcessor(blurRadius: 3.0)\n        let blurred = testImage.kf.blurred(withRadius: 3.0)\n        \n        let group = DispatchGroup()\n\n        group.enter()\n        let task1 = downloader.downloadImage(with: url, options: [.processor(p1)]) { result in\n            XCTAssertTrue(result.value!.image.renderEqual(to: roundCornered))\n            group.leave()\n        }\n\n        group.enter()\n        let task2 = downloader.downloadImage(with: url, options: [.processor(p2)]) { result in\n            XCTAssertTrue(result.value!.image.renderEqual(to: blurred))\n            group.leave()\n        }\n\n        XCTAssertNotNil(task1)\n        XCTAssertEqual(task1.sessionTask?.task, task2.sessionTask?.task)\n\n        _ = stub.go()\n        \n        group.notify(queue: .main, execute: exp.fulfill)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadedDataCouldBeModified() {\n        let exp = expectation(description: #function)\n        \n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        let modifier = URLNilDataModifier()\n\n        downloader.delegate = modifier\n        downloader.downloadImage(with: url) { result in\n            XCTAssertNil(result.value)\n            XCTAssertNotNil(result.error)\n            if case .responseError(reason: .dataModifyingFailed) = result.error! {\n            } else {\n                XCTFail()\n            }\n            self.downloader.delegate = nil\n            // hold delegate\n            _ = modifier\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testDownloadedDataCouldBeModifiedWithTask() {\n        let exp = expectation(description: #function)\n        \n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        let modifier = TaskNilDataModifier()\n\n        downloader.delegate = modifier\n        downloader.downloadImage(with: url) { result in\n            XCTAssertNil(result.value)\n            XCTAssertNotNil(result.error)\n            if case .responseError(reason: .dataModifyingFailed) = result.error! {\n            } else {\n                XCTFail()\n            }\n            self.downloader.delegate = nil\n            // hold delegate\n            _ = modifier\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n\t#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)\n\t    func testModifierShouldOnlyApplyForFinalResultWhenDownload() {\n\t        let exp = expectation(description: #function)\n\n\t        let url = testURLs[0]\n\t        stub(url, data: testImageData)\n\n\t        let modifierCalled = LockIsolated(false)\n\t        let modifier = AnyImageModifier { image in\n\t            modifierCalled.setValue(true)\n\t            return image.withRenderingMode(.alwaysTemplate)\n\t        }\n\n\t        downloader.downloadImage(with: url, options: [.imageModifier(modifier)]) { result in\n\t            XCTAssertEqual(result.value?.image.renderingMode, .automatic)\n\t            XCTAssertFalse(modifierCalled.value)\n\t            exp.fulfill()\n\t        }\n\n\t        waitForExpectations(timeout: 3, handler: nil)\n\t    }\n\t#endif\n    \n    func testDownloadTaskTakePriorityOption() {\n        let exp = expectation(description: #function)\n        \n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        let task = downloader.downloadImage(with: url, options: [.downloadPriority(URLSessionTask.highPriority)])\n        {\n            _ in\n            exp.fulfill()\n        }\n        XCTAssertEqual(task.sessionTask?.task.priority, URLSessionTask.highPriority)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    \n    func testSessionDelegate() {\n        class ExtensionDelegate: SessionDelegate, @unchecked Sendable {\n            //'exp' only for test\n            public let exp: XCTestExpectation\n            init(_ expectation:XCTestExpectation) {\n                exp = expectation\n            }\n            override func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {\n                exp.fulfill()\n            }\n        }\n        downloader.sessionDelegate = ExtensionDelegate(expectation(description: #function))\n        \n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        downloader.downloadImage(with: url) { result in\n            XCTAssertNotNil(result.value)\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testDownloaderReceiveResponsePass() {\n\n        let exp = expectation(description: #function)\n\n        let url = testURLs[0]\n        stub(url, data: testImageData, headers: [\"someKey\": \"someValue\"])\n\n        let handler = TaskResponseCompletion()\n        let obj = NSObject()\n        handler.onReceiveResponse.delegate(on: obj) { (obj, response) in\n            guard let httpResponse = response as? HTTPURLResponse else {\n                XCTFail(\"Should be an HTTP response.\")\n                return .cancel\n            }\n            XCTAssertEqual(httpResponse.statusCode, 200)\n            XCTAssertEqual(httpResponse.url, url)\n            XCTAssertEqual(httpResponse.allHeaderFields[\"someKey\"] as? String, \"someValue\")\n\n            return .allow\n        }\n\n        downloader.delegate = handler\n        downloader.downloadImage(with: url) { result in\n            XCTAssertNotNil(result.value)\n            XCTAssertNil(result.error)\n\n            self.downloader.delegate = nil\n            // hold delegate\n            _ = handler\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testDownloaderReceiveResponseFailure() {\n        let exp = expectation(description: #function)\n\n        let url = testURLs[0]\n        stub(url, data: testImageData, headers: [\"someKey\": \"someValue\"])\n\n        let handler = TaskResponseCompletion()\n        let obj = NSObject()\n        handler.onReceiveResponse.delegate(on: obj) { (obj, response) in\n            guard let httpResponse = response as? HTTPURLResponse else {\n                XCTFail(\"Should be an HTTP response.\")\n                return .cancel\n            }\n            XCTAssertEqual(httpResponse.statusCode, 200)\n            XCTAssertEqual(httpResponse.url, url)\n            XCTAssertEqual(httpResponse.allHeaderFields[\"someKey\"] as? String, \"someValue\")\n\n            return .cancel\n        }\n\n        downloader.delegate = handler\n        downloader.downloadImage(with: url) { result in\n            XCTAssertNil(result.value)\n            XCTAssertNotNil(result.error)\n\n            if case .responseError(reason: .cancelledByDelegate) = result.error! {\n            } else {\n                XCTFail()\n            }\n\n            self.downloader.delegate = nil\n            // hold delegate\n            _ = handler\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownloadingLivePhotoResources() async throws {\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        let result = try await downloader.downloadLivePhotoResource(with: url, options: .init(.empty))\n        XCTAssertEqual(result.originalData, testImageData)\n        XCTAssertEqual(result.url, url)\n    }\n\n    func testConcurrentDownloadSameURL() {\n        let exp = expectation(description: #function)\n\n        // Given\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        final class CallbackCounter: @unchecked Sendable {\n            private let lock = NSLock()\n            private var value = 0\n\n            func increment() {\n                lock.lock()\n                value += 1\n                lock.unlock()\n            }\n\n            func read() -> Int {\n                lock.lock()\n                let current = value\n                lock.unlock()\n                return current\n            }\n        }\n\n        let callbackCounter = CallbackCounter()\n        let expectedCount = 10\n        exp.expectedFulfillmentCount = expectedCount\n\n        // When\n        DispatchQueue.concurrentPerform(iterations: expectedCount) { index in\n            downloader.downloadImage(with: url) { result in\n                switch result {\n                case .success(let imageResult):\n                    XCTAssertNotNil(imageResult.image)\n                    XCTAssertEqual(imageResult.url, url)\n                    XCTAssertEqual(imageResult.originalData, testImageData)\n\n                    callbackCounter.increment()\n\n                    exp.fulfill()\n\n                case .failure(let error):\n                    XCTFail(\"Download should succeed: \\(error)\")\n                    exp.fulfill()\n                }\n            }\n        }\n\n        // Then\n        waitForExpectations(timeout: 3) { _ in\n            let finalCount = callbackCounter.read()\n            XCTAssertEqual(finalCount, expectedCount, \"All \\(expectedCount) concurrent requests should receive callbacks\")\n        }\n    }\n}\n\nclass URLNilDataModifier: ImageDownloaderDelegate {\n    func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {\n        return nil\n    }\n}\n\nclass TaskNilDataModifier: ImageDownloaderDelegate {\n    func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with dataTask: SessionDataTask) -> Data? {\n        return nil\n    }\n}\n\nclass TaskResponseCompletion: ImageDownloaderDelegate {\n\n    let onReceiveResponse = Delegate<URLResponse, URLSession.ResponseDisposition>()\n    func imageDownloader(_ downloader: ImageDownloader, didReceive response: URLResponse) async -> URLSession.ResponseDisposition {\n        return onReceiveResponse.call(response)!\n    }\n}\n\nfinal class URLModifier: ImageDownloadRequestModifier {\n    let url: URL?\n    init(url: URL?) {\n        self.url = url\n    }\n    func modified(for request: URLRequest) -> URLRequest? {\n        var r = request\n        r.url = url\n        return r\n    }\n}\n\nfinal class AsyncURLModifier: AsyncImageDownloadRequestModifier {\n    let url: URL?\n    let onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)?\n    \n    init(url: URL?, onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)?) {\n        self.url = url\n        self.onDownloadTaskStarted = onDownloadTaskStarted\n    }\n\n    func modified(for request: URLRequest) async -> URLRequest? {\n        var r = request\n        r.url = url\n        // Simulate an async action\n        try? await Task.sleep(nanoseconds: 1_000_000)\n        return r\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/ImageDrawingTests.swift",
    "content": "//\n//  ImageDrawingTests.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/10/26.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass ImageDrawingTests: XCTestCase {\n\n    func testImageResizing() {\n        let result = testImage.kf.resize(to: CGSize(width: 20, height: 20))\n        XCTAssertEqual(result.size, CGSize(width: 20, height: 20))\n    }\n    \n    func testImageCropping() {\n        let result = testImage.kf.crop(to: CGSize(width: 20, height: 20), anchorOn: .zero)\n        XCTAssertEqual(result.size, CGSize(width: 20, height: 20))\n    }\n    \n    func testImageScaling() {\n        XCTAssertEqual(testImage.kf.scale, 1)\n        let result = testImage.kf.scaled(to: 2.0)\n        #if os(macOS)\n        // No scale supported on macOS.\n        XCTAssertEqual(result.kf.scale, 1)\n        XCTAssertEqual(result.size.height, testImage.size.height)\n        #else\n        XCTAssertEqual(result.kf.scale, 2)\n        XCTAssertEqual(result.size.height, testImage.size.height / 2)\n        #endif\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/ImageExtensionTests.swift",
    "content": "//\n//  ImageExtensionTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/10/24.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\nimport ImageIO\n@testable import Kingfisher\n\nclass ImageExtensionTests: XCTestCase {\n\n    func testImageFormat() {\n        var format: ImageFormat\n        format = testImageJEPGData.kf.imageFormat\n        XCTAssertEqual(format, .JPEG)\n        \n        format = testImagePNGData.kf.imageFormat\n        XCTAssertEqual(format, .PNG)\n        \n        format = testImageGIFData.kf.imageFormat\n        XCTAssertEqual(format, .GIF)\n        \n        let raw: [UInt8] = [1, 2, 3, 4, 5, 6, 7, 8]\n        format = Data(raw).kf.imageFormat\n        XCTAssertEqual(format, .unknown)\n    }\n    \n    func testGenerateJPEGImage() {\n        let options = ImageCreatingOptions()\n        let image = KingfisherWrapper<KFCrossPlatformImage>.image(data: testImageJEPGData, options: options)\n        XCTAssertNotNil(image)\n        XCTAssertNil(image?.kf.imageFrameCount)\n        XCTAssertTrue(image!.renderEqual(to: KFCrossPlatformImage(data: testImageJEPGData)!))\n    }\n    \n    func testGenerateGIFImage() {\n        let options = ImageCreatingOptions()\n        let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: options)\n        XCTAssertNotNil(image)\n        #if os(iOS) || os(tvOS) || os(visionOS)\n        XCTAssertEqual(image!.kf.imageFrameCount!, 8)\n        #else\n        XCTAssertEqual(image!.kf.images!.count, 8)\n        XCTAssertEqual(image!.kf.duration, 0.8, accuracy: 0.001)\n        #endif\n    }\n\n    #if os(iOS) || os(tvOS) || os(visionOS)\n    func testScaleForGIFImage() {\n        let options = ImageCreatingOptions(scale: 2.0, duration: 0.0, preloadAll: false, onlyFirstFrame: false)\n        let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: options)\n        XCTAssertNotNil(image)\n        XCTAssertEqual(image!.scale, 2.0)\n    }\n    #endif\n\n    func testGIFRepresentation() {\n        let options = ImageCreatingOptions()\n        let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: options)!\n        let data = image.kf.gifRepresentation()\n        \n        XCTAssertNotNil(data)\n        XCTAssertEqual(data?.kf.imageFormat, ImageFormat.GIF)\n        \n        let preloadOptions = ImageCreatingOptions(preloadAll: true)\n        let allLoadImage = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: data!, options: preloadOptions)!\n        let allLoadData = allLoadImage.kf.gifRepresentation()\n        XCTAssertNotNil(allLoadData)\n        XCTAssertEqual(allLoadData?.kf.imageFormat, ImageFormat.GIF)\n    }\n    \n    func testGenerateSingleFrameGIFImage() {\n        let options = ImageCreatingOptions()\n        let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageSingleFrameGIFData, options: options)\n        XCTAssertNotNil(image)\n        #if os(iOS) || os(tvOS) || os(visionOS)\n        XCTAssertEqual(image!.kf.imageFrameCount!, 1)\n        #else\n        XCTAssertEqual(image!.kf.images!.count, 1)\n        XCTAssertEqual(image!.kf.duration, Double.infinity)\n        #endif\n    }\n    \n    func testGenerateFromNonImage() {\n        let data = \"hello\".data(using: .utf8)!\n        let options = ImageCreatingOptions()\n        let image = KingfisherWrapper<KFCrossPlatformImage>.image(data: data, options: options)\n        XCTAssertNil(image)\n    }\n    \n    func testPreloadAllAnimationData() {\n        let preloadOptions = ImageCreatingOptions(preloadAll: true)\n        let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageSingleFrameGIFData, options: preloadOptions)!\n        XCTAssertNotNil(image, \"The image should be initiated.\")\n#if os(iOS) || os(tvOS) || os(visionOS)\n        XCTAssertNil(image.kf.imageSource, \"Image source should be nil\")\n#endif\n        XCTAssertEqual(image.kf.duration, image.kf.duration)\n        XCTAssertEqual(image.kf.images!.count, image.kf.images!.count)\n    }\n    \n    func testLoadOnlyFirstFrame() {\n        let preloadOptions = ImageCreatingOptions(preloadAll: true, onlyFirstFrame: true)\n        let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: preloadOptions)!\n        XCTAssertNotNil(image, \"The image should be initiated.\")\n        XCTAssertNil(image.kf.images, \"The image should be nil\")\n    }\n    \n    func testSizeContent() {\n        func getRatio(image: KFCrossPlatformImage) -> CGFloat {\n            return image.size.height / image.size.width\n        }\n        \n        let image = testImage\n        let ratio = getRatio(image: image)\n        \n        let targetSize = CGSize(width: 100, height: 50)\n        \n        let fillImage = image.kf.resize(to: targetSize, for: .aspectFill)\n        XCTAssertEqual(getRatio(image: fillImage), ratio)\n        XCTAssertEqual(max(fillImage.size.width, fillImage.size.height), 100)\n        \n        let fitImage = image.kf.resize(to: targetSize, for: .aspectFit)\n        XCTAssertEqual(getRatio(image: fitImage), ratio)\n        XCTAssertEqual(max(fitImage.size.width, fitImage.size.height), 50)\n        \n        let resizeImage = image.kf.resize(to: targetSize)\n        XCTAssertEqual(resizeImage.size.width, 100)\n        XCTAssertEqual(resizeImage.size.height, 50)\n    }\n    \n    func testSizeConstraintByAnchor() {\n        let size = CGSize(width: 100, height: 100)\n        \n        let topLeft = CGPoint(x: 0, y: 0)\n        let top = CGPoint(x: 0.5, y: 0)\n        let topRight = CGPoint(x: 1, y: 0)\n        let center = CGPoint(x: 0.5, y: 0.5)\n        let bottomRight = CGPoint(x: 1, y: 1)\n        let invalidAnchor = CGPoint(x: -1, y: 2)\n        \n        let inSize = CGSize(width: 20, height: 20)\n        let outX = CGSize(width: 120, height: 20)\n        let outY = CGSize(width: 20, height: 120)\n        let outSize = CGSize(width: 120, height: 120)\n\n        let kf = size.kf\n\n        XCTAssertEqual(\n            kf.constrainedRect(for: inSize, anchor: topLeft),\n            CGRect(x: 0, y: 0, width: 20, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outX, anchor: topLeft),\n            CGRect(x: 0, y: 0, width: 100, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outY, anchor: topLeft),\n            CGRect(x: 0, y: 0, width: 20, height: 100))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outSize, anchor: topLeft),\n            CGRect(x: 0, y: 0, width: 100, height: 100))\n        \n        XCTAssertEqual(\n            kf.constrainedRect(for: inSize, anchor: top),\n            CGRect(x: 40, y: 0, width: 20, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outX, anchor: top),\n            CGRect(x: 0, y: 0, width: 100, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outY, anchor: top),\n            CGRect(x: 40, y: 0, width: 20, height: 100))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outSize, anchor: top),\n            CGRect(x: 0, y: 0, width: 100, height: 100))\n        \n        XCTAssertEqual(\n            kf.constrainedRect(for: inSize, anchor: topRight),\n            CGRect(x: 80, y: 0, width: 20, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outX, anchor: topRight),\n            CGRect(x: 0, y: 0, width: 100, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outY, anchor: topRight),\n            CGRect(x: 80, y: 0, width: 20, height: 100))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outSize, anchor: topRight),\n            CGRect(x: 0, y: 0, width: 100, height: 100))\n        \n        XCTAssertEqual(\n            kf.constrainedRect(for: inSize, anchor: center),\n            CGRect(x: 40, y: 40, width: 20, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outX, anchor: center),\n            CGRect(x: 0, y: 40, width: 100, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outY, anchor: center),\n            CGRect(x: 40, y: 0, width: 20, height: 100))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outSize, anchor: center),\n            CGRect(x: 0, y: 0, width: 100, height: 100))\n        \n        XCTAssertEqual(\n            kf.constrainedRect(for: inSize, anchor: bottomRight),\n            CGRect(x: 80, y: 80, width: 20, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outX, anchor: bottomRight),\n            CGRect(x: 0, y: 80, width: 100, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outY, anchor: bottomRight),\n            CGRect(x:80, y: 0, width: 20, height: 100))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outSize, anchor: bottomRight),\n            CGRect(x: 0, y: 0, width: 100, height: 100))\n        \n        XCTAssertEqual(\n            kf.constrainedRect(for: inSize, anchor: invalidAnchor),\n            CGRect(x: 0, y: 80, width: 20, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outX, anchor: invalidAnchor),\n            CGRect(x: 0, y: 80, width: 100, height: 20))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outY, anchor: invalidAnchor),\n            CGRect(x:0, y: 0, width: 20, height: 100))\n        XCTAssertEqual(\n            kf.constrainedRect(for: outSize, anchor: invalidAnchor),\n            CGRect(x: 0, y: 0, width: 100, height: 100))\n    }\n    \n    func testDecodeScale() {\n        #if os(iOS) || os(tvOS) || os(visionOS)\n        let image = testImage\n        XCTAssertEqual(image.size, CGSize(width: 64, height: 64))\n        XCTAssertEqual(image.scale, 1.0)\n\n        let image_2x = KingfisherWrapper<KFCrossPlatformImage>.image(cgImage: image.cgImage!, scale: 2.0, refImage: image)\n        XCTAssertEqual(image_2x.size, CGSize(width: 32, height: 32))\n        XCTAssertEqual(image_2x.scale, 2.0)\n        \n        let decoded = image.kf.decoded\n        XCTAssertEqual(decoded.size, CGSize(width: 64, height: 64))\n        XCTAssertEqual(decoded.scale, 1.0)\n        \n        let decodedDifferentScale = image.kf.decoded(scale: 2.0)\n        XCTAssertEqual(decodedDifferentScale.size, CGSize(width: 32, height: 32))\n        XCTAssertEqual(decodedDifferentScale.scale, 2.0)\n        \n        let decoded_2x = image_2x.kf.decoded\n        XCTAssertEqual(decoded_2x.size, CGSize(width: 32, height: 32))\n        XCTAssertEqual(decoded_2x.scale, 2.0)\n        #endif\n    }\n    \n    func testNormalized() {\n        // Full loaded GIF image should not be normalized since it is a set of images.\n        let options = ImageCreatingOptions()\n        let gifImage = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: options)\n        \n        XCTAssertNotNil(gifImage)\n        XCTAssertEqual(gifImage!.kf.normalized, gifImage!)\n        \n        #if os(iOS) || os(tvOS) || os(visionOS)\n        // No need to normalize up orientation image.\n        let normalImage = testImage\n        XCTAssertEqual(normalImage.imageOrientation, .up)\n        XCTAssertEqual(normalImage.kf.normalized, testImage)\n\n        let colorImage = UIImage.from(color: .red, size: CGSize(width: 100, height: 200))\n        let rotatedImage = UIImage(cgImage: colorImage.cgImage!, scale: colorImage.scale, orientation: .right)\n\n        XCTAssertEqual(rotatedImage.imageOrientation, .right)\n\n        let rotatedNormalizedImage = rotatedImage.kf.normalized\n        XCTAssertEqual(rotatedNormalizedImage.imageOrientation, .up)\n        XCTAssertEqual(rotatedNormalizedImage.size, CGSize(width: 200, height: 100))\n        #endif\n    }\n    \n    func testDownsampling() {\n        let size = CGSize(width: 15, height: 15)\n        XCTAssertEqual(testImage.size, CGSize(width: 64, height: 64))\n        XCTAssertEqual(testImage.kf.scale, 1.0)\n        \n        let image = KingfisherWrapper<KFCrossPlatformImage>.downsampledImage(data: testImageData, to: size, scale: 1)\n        XCTAssertEqual(image?.size, size)\n        XCTAssertEqual(image?.kf.scale, 1.0)\n    }\n    \n    func testDownsamplingWithScale() {\n        let size = CGSize(width: 15, height: 15)\n        XCTAssertEqual(testImage.size, CGSize(width: 64, height: 64))\n        XCTAssertEqual(testImage.kf.scale, 1.0)\n        \n        let image2x = KingfisherWrapper<KFCrossPlatformImage>.downsampledImage(data: testImageData, to: size, scale: 2)\n        #if os(macOS)\n        XCTAssertEqual(image2x?.size, CGSize(width: 30, height: 30))\n        XCTAssertEqual(image2x?.kf.scale, 1.0)\n        #else\n        XCTAssertEqual(image2x?.size, size)\n        XCTAssertEqual(image2x?.kf.scale, 2.0)\n        #endif\n        \n        let image3x = KingfisherWrapper<KFCrossPlatformImage>.downsampledImage(data: testImageData, to: size, scale: 3)\n        #if os(macOS)\n        XCTAssertEqual(image3x?.size, CGSize(width: 45, height: 45))\n        XCTAssertEqual(image3x?.kf.scale, 1.0)\n        #else\n        XCTAssertEqual(image3x?.size, size)\n        XCTAssertEqual(image3x?.kf.scale, 3.0)\n        #endif\n    }\n\n    func testDownsamplingWithEdgeCaseSize() {\n\n        // Zero size would fail downsampling before iOS 17.4.\n        let result = KingfisherWrapper<KFCrossPlatformImage>.downsampledImage(data: testImageData, to: .zero, scale: 1)\n        if #available(iOS 17.4, macOS 14.4, tvOS 17.4, *) {\n            XCTAssertEqual(result?.size, CGSize(width: 64, height: 64))\n        } else {\n            XCTAssertNil(result)\n        }\n\n        let largerSize = CGSize(width: 100, height: 100)\n        let largerImage = KingfisherWrapper<KFCrossPlatformImage>.downsampledImage(data: testImageData, to: largerSize, scale: 1)\n        // You can not \"downsample\" an image to a larger size.\n        XCTAssertEqual(largerImage?.size, CGSize(width: 64, height: 64))\n    }\n\n    #if os(macOS)\n    func testSVGImageSize() {\n        let svgString = \"\"\"\n        <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n        <svg width=\"100px\" height=\"200px\" viewBox=\"0 0 100 200\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n            <rect width=\"100\" height=\"200\" fill=\"red\"/>\n        </svg>\n        \"\"\"\n        \n        guard let data = svgString.data(using: .utf8),\n              let image = NSImage(data: data)\n        else {\n            XCTFail(\"Failed to create image from SVG data\")\n            return\n        }\n        \n        let size = image.kf.size\n        XCTAssertEqual(size.width, 100)\n        XCTAssertEqual(size.height, 200)\n    }\n    #endif\n}\n\n#if !os(watchOS)\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\nfinal class AnimatedImageViewAnimatorTests: XCTestCase {\n\n    func testAnimatorPurgeFramesKeepsCurrentFrameByDefault() {\n        let source = CGImageSourceCreateWithData(testImageGIFData as CFData, nil)!\n        let queue = DispatchQueue(label: \"com.onevcat.KingfisherTests.AnimatorPreload\")\n\n        #if os(macOS)\n        let contentMode: KFCrossPlatformContentMode = .scaleAxesIndependently\n        #else\n        let contentMode: KFCrossPlatformContentMode = .scaleToFill\n        #endif\n\n        let animator = AnimatedImageView.Animator(\n            imageSource: source,\n            contentMode: contentMode,\n            size: CGSize(width: 40, height: 40),\n            imageSize: CGSize(width: 40, height: 40),\n            imageScale: 1,\n            framePreloadCount: 2,\n            repeatCount: .infinite,\n            preloadQueue: queue\n        )\n\n        animator.prepareFramesAsynchronously()\n        queue.sync { }\n\n        XCTAssertNotNil(animator.frame(at: 0))\n        XCTAssertNotNil(animator.frame(at: 1))\n\n        animator.purgeFrames()\n        queue.sync { }\n\n        XCTAssertNotNil(animator.frame(at: 0))\n        XCTAssertNil(animator.frame(at: 1))\n    }\n\n    func testAnimatorPurgeFramesCanPurgeCurrentFrame() {\n        let source = CGImageSourceCreateWithData(testImageGIFData as CFData, nil)!\n        let queue = DispatchQueue(label: \"com.onevcat.KingfisherTests.AnimatorPreload\")\n\n        #if os(macOS)\n        let contentMode: KFCrossPlatformContentMode = .scaleAxesIndependently\n        #else\n        let contentMode: KFCrossPlatformContentMode = .scaleToFill\n        #endif\n\n        let animator = AnimatedImageView.Animator(\n            imageSource: source,\n            contentMode: contentMode,\n            size: CGSize(width: 40, height: 40),\n            imageSize: CGSize(width: 40, height: 40),\n            imageScale: 1,\n            framePreloadCount: 2,\n            repeatCount: .infinite,\n            preloadQueue: queue\n        )\n\n        animator.prepareFramesAsynchronously()\n        queue.sync { }\n\n        XCTAssertNotNil(animator.frame(at: 0))\n\n        animator.purgeFrames(keepCurrentFrame: false)\n        queue.sync { }\n\n        XCTAssertNil(animator.frame(at: 0))\n    }\n}\n\n#if os(iOS)\n\nfinal class AnimatedImageViewBackgroundPurgeTests: XCTestCase {\n\n    @MainActor\n    func testPurgeFramesOnBackgroundStopsAnimation() {\n        let imageView = AnimatedImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))\n        imageView.purgeFramesOnBackground = true\n        imageView.image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(\n            data: testImageGIFData,\n            options: .init(scale: 1, duration: 0, preloadAll: false, onlyFirstFrame: false)\n        )\n\n        imageView.startAnimating()\n        XCTAssertTrue(imageView.isAnimating)\n\n        NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)\n        RunLoop.main.run(until: Date().addingTimeInterval(0.05))\n\n        XCTAssertFalse(imageView.isAnimating)\n    }\n\n    @MainActor\n    func testPurgeFramesOnBackgroundCanResumeOnForegroundWhenViewAttached() {\n        let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 100, height: 100))\n        let host = UIViewController()\n        window.rootViewController = host\n        window.makeKeyAndVisible()\n\n        let imageView = AnimatedImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))\n        imageView.purgeFramesOnBackground = true\n        imageView.image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(\n            data: testImageGIFData,\n            options: .init(scale: 1, duration: 0, preloadAll: false, onlyFirstFrame: false)\n        )\n        host.view.addSubview(imageView)\n\n        imageView.startAnimating()\n        XCTAssertTrue(imageView.isAnimating)\n\n        NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)\n        RunLoop.main.run(until: Date().addingTimeInterval(0.05))\n        XCTAssertFalse(imageView.isAnimating)\n\n        NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil)\n        RunLoop.main.run(until: Date().addingTimeInterval(0.05))\n        XCTAssertTrue(imageView.isAnimating)\n    }\n}\n\n#endif\n\n#endif\n"
  },
  {
    "path": "Tests/KingfisherTests/ImageModifierTests.swift",
    "content": "//\n//  ImageModifierTests.swift\n//  Kingfisher\n//\n//  Created by Ethan Gill on 2017/11/29.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\nimport Kingfisher\n\nclass ImageModifierTests: XCTestCase {\n\n    override func setUp() {\n        super.setUp()\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n    }\n\n    override func tearDown() {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n        super.tearDown()\n    }\n\n    func testAnyImageModifier() {\n        let m = AnyImageModifier { image in\n            return image\n        }\n        let image = KFCrossPlatformImage(data: testImagePNGData)!\n        let modifiedImage = m.modify(image)\n        XCTAssert(modifiedImage == image)\n    }\n\n#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)\n\n    func testRenderingModeImageModifier() {\n        let m1 = RenderingModeImageModifier(renderingMode: .alwaysOriginal)\n        let image = KFCrossPlatformImage(data: testImagePNGData)!\n        let alwaysOriginalImage = m1.modify(image)\n        XCTAssert(alwaysOriginalImage.renderingMode == .alwaysOriginal)\n\n        let m2 = RenderingModeImageModifier(renderingMode: .alwaysTemplate)\n        let alwaysTemplateImage = m2.modify(image)\n        XCTAssert(alwaysTemplateImage.renderingMode == .alwaysTemplate)\n    }\n\n    func testFlipsForRightToLeftLayoutDirectionImageModifier() {\n        let m = FlipsForRightToLeftLayoutDirectionImageModifier()\n        let image = KFCrossPlatformImage(data: testImagePNGData)!\n        let modifiedImage = m.modify(image)\n        XCTAssert(modifiedImage.flipsForRightToLeftLayoutDirection == true)\n    }\n\n    func testAlignmentRectInsetsImageModifier() {\n        let insets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)\n        let m = AlignmentRectInsetsImageModifier(alignmentInsets: insets)\n        let image = KFCrossPlatformImage(data: testImagePNGData)!\n        let modifiedImage = m.modify(image)\n        XCTAssert(modifiedImage.alignmentRectInsets == insets)\n    }\n\n#endif\n\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/ImagePrefetcherTests.swift",
    "content": "//\n//  ImagePrefetcherTests.swift\n//  Kingfisher\n//\n//  Created by Claire Knight <claire.knight@moggytech.co.uk> on 24/02/2016\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\n#if os(macOS)\n    import AppKit\n#else\n    import UIKit\n#endif\n\nclass ImagePrefetcherTests: XCTestCase {\n    \n    override class func setUp() {\n        super.setUp()\n        LSNocilla.sharedInstance().start()\n    }\n    \n    override class func tearDown() {\n        LSNocilla.sharedInstance().stop()\n        super.tearDown()\n    }\n    \n    override func setUp() {\n        super.setUp()\n        cleanDefaultCache()\n    }\n    \n    override func tearDown() {\n        cleanDefaultCache()\n        super.tearDown()\n    }\n\n    func testPrefetchingImages() {\n        let exp = expectation(description: #function)\n\n        testURLs.forEach { stub($0, data: testImageData) }\n        let progressCalledCount = LockIsolated(0)\n        let prefetcher = ImagePrefetcher(\n            urls: testURLs,\n            options: [.waitForCache],\n            progressBlock: { _, _, _ in progressCalledCount.withValue { $0 += 1 } }) {\n                skippedResources, failedResources, completedResources in\n\n                XCTAssertEqual(skippedResources.count, 0)\n                XCTAssertEqual(failedResources.count, 0)\n                XCTAssertEqual(completedResources.count, testURLs.count)\n                XCTAssertEqual(progressCalledCount.value, testURLs.count)\n                for url in testURLs {\n                    XCTAssertTrue(KingfisherManager.shared.cache.imageCachedType(forKey: url.absoluteString).cached)\n                }\n                exp.fulfill()\n            }\n        prefetcher.start()\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCancelPrefetching() {\n        let exp = expectation(description: #function)\n        let stubs = testURLs.map { delayedStub($0, data: testImageData) }\n        \n        let maxConcurrentCount = 2\n        let prefetcher = ImagePrefetcher(\n            urls: testURLs,\n            options: [.waitForCache],\n            completionHandler: { skippedResources, failedResources, completedResources in\n                XCTAssertEqual(skippedResources.count, 0)\n                XCTAssertEqual(failedResources.count, testURLs.count)\n                XCTAssertEqual(completedResources.count, 0)\n                delay(0.1) { exp.fulfill() }\n            }\n        )\n        \n        prefetcher.maxConcurrentDownloads = maxConcurrentCount\n        \n        prefetcher.start()\n        \n        DispatchQueue.main.async {\n            prefetcher.stop()\n            stubs.forEach { _ = $0.go() }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n\n    func testPrefetcherCouldSkipCachedImages() {\n        let exp = expectation(description: #function)\n        KingfisherManager.shared.cache.store(KFCrossPlatformImage(), forKey: testKeys[0])\n        \n        testURLs.forEach { stub($0, data: testImageData) }\n        let prefetcher = ImagePrefetcher(\n            urls: testURLs,\n            options: [.waitForCache],\n            completionHandler: { skippedResources, failedResources, completedResources in\n                XCTAssertEqual(skippedResources.count, 1)\n                XCTAssertEqual(skippedResources[0].downloadURL, testURLs[0])\n                XCTAssertEqual(failedResources.count, 0)\n                XCTAssertEqual(completedResources.count, testURLs.count - 1)\n                exp.fulfill()\n            }\n        )\n        \n        prefetcher.start()\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testPrefetcherForceRefreshDownloadImages() {\n        let exp = expectation(description: #function)\n        KingfisherManager.shared.cache.store(KFCrossPlatformImage(), forKey: testKeys[0])\n        \n        testURLs.forEach { stub($0, data: testImageData) }\n        let prefetcher = ImagePrefetcher(urls: testURLs, options: [.forceRefresh, .waitForCache], completionHandler:  {\n            skippedResources, failedResources, completedResources in\n            XCTAssertEqual(skippedResources.count, 0)\n            XCTAssertEqual(failedResources.count, 0)\n            XCTAssertEqual(completedResources.count, testURLs.count)\n            exp.fulfill()\n        })\n        \n        prefetcher.start()\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testPrefetchWithWrongInitParameters() {\n        let exp = expectation(description: #function)\n        let prefetcher = ImagePrefetcher(urls: [], options: [.waitForCache], completionHandler:  {\n            skippedResources, failedResources, completedResources in\n            XCTAssertEqual(skippedResources.count, 0)\n            XCTAssertEqual(failedResources.count, 0)\n            XCTAssertEqual(completedResources.count, 0)\n            exp.fulfill()\n        })\n        \n        prefetcher.start()\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testFetchWithProcessor() {\n        let exp = expectation(description: #function)\n        testURLs.forEach { stub($0, data: testImageData, length: 123) }\n\n        let p = RoundCornerImageProcessor(cornerRadius: 20)\n\n        @Sendable func prefetchAgain() {\n            let progressCalledCount = LockIsolated(0)\n            let prefetcher = ImagePrefetcher(\n                urls: testURLs,\n                options: [.processor(p), .waitForCache],\n                progressBlock: { _, _, _ in progressCalledCount.withValue { $0 += 1 } })\n            {\n                skippedResources, failedResources, completedResources in\n\n                XCTAssertEqual(skippedResources.count, testURLs.count)\n                XCTAssertEqual(failedResources.count, 0)\n                XCTAssertEqual(completedResources.count, 0)\n                XCTAssertEqual(progressCalledCount.value, testURLs.count)\n                for url in testURLs {\n                    let cached = KingfisherManager.shared.cache.imageCachedType(\n                        forKey: url.absoluteString, processorIdentifier: p.identifier).cached\n                    XCTAssertTrue(cached)\n                }\n                exp.fulfill()\n\n            }\n            prefetcher.start()\n        }\n\n        let progressCalledCount = LockIsolated(0)\n        let prefetcher = ImagePrefetcher(\n            urls: testURLs,\n            options: [.processor(p), .waitForCache],\n            progressBlock: { _, _, _ in progressCalledCount.withValue { $0 += 1 } })\n        {\n            skippedResources, failedResources, completedResources in\n\n            XCTAssertEqual(skippedResources.count, 0)\n            XCTAssertEqual(failedResources.count, 0)\n            XCTAssertEqual(completedResources.count, testURLs.count)\n            XCTAssertEqual(progressCalledCount.value, testURLs.count)\n            for url in testURLs {\n                let cached = KingfisherManager.shared.cache.imageCachedType(\n                    forKey: url.absoluteString, processorIdentifier: p.identifier).cached\n                XCTAssertTrue(cached)\n            }\n\n            prefetchAgain()\n        }\n        prefetcher.start()\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testAlsoPrefetchToMemory() {\n        let exp = expectation(description: #function)\n        let cache = KingfisherManager.shared.cache\n        let key = testKeys[0]\n        cache.store(KFCrossPlatformImage(), forKey: key)\n        cache.store(testImage, forKey: key) { result in\n            cache.memoryStorage.remove(forKey: key)\n            \n            XCTAssertEqual(cache.imageCachedType(forKey: key), .disk)\n            \n            testURLs.forEach { stub($0, data: testImageData) }\n            let prefetcher = ImagePrefetcher(\n                urls: testURLs,\n                options: [.waitForCache, .alsoPrefetchToMemory], \n                completionHandler: { skippedResources, failedResources, completedResources in\n                        \n                    XCTAssertEqual(cache.imageCachedType(forKey: key), .memory)\n                    XCTAssertEqual(skippedResources.count, 1)\n                    XCTAssertEqual(skippedResources[0].downloadURL, testURLs[0])\n                    XCTAssertEqual(failedResources.count, 0)\n                    XCTAssertEqual(completedResources.count, testURLs.count - 1)\n                    exp.fulfill()\n                }\n            )\n            \n            prefetcher.start()\n            \n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testNotPrefetchToMemory() {\n        let exp = expectation(description: #function)\n        let cache = KingfisherManager.shared.cache\n        let key = testKeys[0]\n\n        cache.store(testImage, forKey: key) { result in\n            cache.memoryStorage.remove(forKey: key)\n            \n            XCTAssertEqual(cache.imageCachedType(forKey: key), .disk)\n            \n            testURLs.forEach { stub($0, data: testImageData) }\n            let prefetcher = ImagePrefetcher(\n                urls: testURLs,\n                options: [.waitForCache],\n                completionHandler: { skippedResources, failedResources, completedResources in\n                        \n                    XCTAssertEqual(cache.imageCachedType(forKey: key), .disk)\n                    \n                    XCTAssertEqual(skippedResources.count, 1)\n                    XCTAssertEqual(skippedResources[0].downloadURL, testURLs[0])\n                    XCTAssertEqual(failedResources.count, 0)\n                    XCTAssertEqual(completedResources.count, testURLs.count - 1)\n                    exp.fulfill()\n                }\n            )\n            \n            prefetcher.start()\n            \n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testPrefetchMoreTaskThanMaxConcurrency() {\n        let exp = expectation(description: #function)\n        \n        testURLs.forEach { stub($0, data: testImageData) }\n        let prefetcher = ImagePrefetcher(\n            urls: testURLs,\n            options: [.waitForCache], \n            completionHandler: { skippedResources, failedResources, completedResources in\n                XCTAssertEqual(skippedResources.count, 0)\n                XCTAssertEqual(failedResources.count, 0)\n                XCTAssertEqual(completedResources.count, testURLs.count)\n                exp.fulfill()\n            }\n        )\n        prefetcher.maxConcurrentDownloads = 1\n        prefetcher.start()\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testPrefetchMultiTimes() {\n        let exp = expectation(description: #function)\n        let group = DispatchGroup()\n        testURLs.forEach { stub($0, data: testImageData) }\n        for _ in 0..<10000 {\n            group.enter()\n            let prefetcher = ImagePrefetcher(\n                resources: testURLs,\n                options: [.cacheMemoryOnly], \n                completionHandler: {\n                    _, _, _ in group.leave()\n                }\n            )\n            prefetcher.start()\n        }\n        group.notify(queue: .main) { exp.fulfill() }\n        waitForExpectations(timeout: 15, handler: nil)\n    }\n\n    func testPrefetchSources() {\n        let exp = expectation(description: #function)\n\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let sources: [Source] = [\n            .provider(SimpleImageDataProvider(cacheKey: \"1\") { .success(testImageData) }),\n            .provider(SimpleImageDataProvider(cacheKey: \"2\") { .success(testImageData) }),\n            .network(url)\n        ]\n        let counter = LockIsolated(0)\n        let prefetcher = ImagePrefetcher(\n            sources: sources,\n            options: [.waitForCache],\n            progressBlock: {\n                skipped, failed, completed in\n                counter.withValue { $0 += 1 }\n                XCTAssertEqual(skipped.count, 0)\n                XCTAssertEqual(failed.count, 0)\n                XCTAssertEqual(completed.count, counter.value)\n            },\n            completionHandler: {\n                skipped, failed, completed in\n                XCTAssertEqual(skipped.count, 0)\n                XCTAssertEqual(failed.count, 0)\n                XCTAssertEqual(completed.count, sources.count)\n                XCTAssertEqual(counter.value, sources.count)\n\n                let allCached = [ImageCache.default.isCached(forKey: \"1\"),\n                                 ImageCache.default.isCached(forKey: \"2\"),\n                                 ImageCache.default.isCached(forKey: url.absoluteString)\n                ].allSatisfy { $0 == true }\n                XCTAssertTrue(allCached)\n\n                exp.fulfill()\n            })\n        prefetcher.start()\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/ImageProcessorTests.swift",
    "content": "//\n//  ImageProcessorTests.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2019/03/02.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass ImageProcessorTests: XCTestCase {\n\n    // Issue 1125. https://github.com/onevcat/Kingfisher/issues/1125\n    func testDownsamplingSizes() {\n        XCTAssertEqual(testImage.size, CGSize(width: 64, height: 64))\n\n        let emptyOption = KingfisherParsedOptionsInfo(nil)\n\n        let targetSize = CGSize(width: 20, height: 40)\n        let downsamplingProcessor = DownsamplingImageProcessor(size: targetSize)\n\n        let resultFromData = downsamplingProcessor.process(item: .data(testImageData), options: emptyOption)\n        XCTAssertEqual(resultFromData!.size, CGSize(width: 40, height: 40))\n\n        let resultFromImage = downsamplingProcessor.process(item: .image(testImage), options: emptyOption)\n        XCTAssertEqual(resultFromImage!.size, CGSize(width: 40, height: 40))\n    }\n\n    func testProcessorConcating() {\n        let p1 = BlurImageProcessor(blurRadius: 10)\n        let p2 = RoundCornerImageProcessor(cornerRadius: 10)\n        let p3 = TintImageProcessor(tint: .blue)\n\n        let two = p1 |> p2\n        let three = p1 |> p2 |> p3\n\n        XCTAssertNotNil(two)\n        XCTAssertNotNil(three)\n    }\n    \n    func testParsingColorRGBA() {\n        let sRGB = KFCrossPlatformColor(red: 0.5, green: 0.6, blue: 0.7, alpha: 0.8)\n        let rgba = sRGB.rgba\n        XCTAssertEqual(rgba.r, 0.5, accuracy: 0.01)\n        XCTAssertEqual(rgba.g, 0.6, accuracy: 0.01)\n        XCTAssertEqual(rgba.b, 0.7, accuracy: 0.01)\n        XCTAssertEqual(rgba.a, 0.8, accuracy: 0.01)\n        \n        let extended = KFCrossPlatformColor(displayP3Red: 0, green: 1, blue: 0, alpha: 0.8)\n        let rgbaExt = extended.rgba\n        // extended sRGB\n        XCTAssertTrue(rgbaExt.r < 0)\n        XCTAssertTrue(rgbaExt.g > 1)\n        XCTAssertTrue(rgbaExt.b < 0)\n        XCTAssertEqual(rgbaExt.a, 0.8)\n        \n        let blackWhite = KFCrossPlatformColor(white: 0.3, alpha: 1.0)\n        let rgbaBlackWhite = blackWhite.rgba\n        XCTAssertEqual(rgbaBlackWhite.r, 0.3, accuracy: 0.01)\n        XCTAssertEqual(rgbaBlackWhite.g, 0.3, accuracy: 0.01)\n        XCTAssertEqual(rgbaBlackWhite.b, 0.3, accuracy: 0.01)\n        XCTAssertEqual(rgbaBlackWhite.a, 1.0, accuracy: 0.01)\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/ImageViewExtensionTests.swift",
    "content": "//\n//  UIImageViewExtensionTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/17.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass ImageViewExtensionTests: XCTestCase {\n\n    var imageView: KFCrossPlatformImageView!\n    \n    override class func setUp() {\n        super.setUp()\n        LSNocilla.sharedInstance().start()\n    }\n    \n    override class func tearDown() {\n        LSNocilla.sharedInstance().stop()\n        super.tearDown()\n    }\n    \n    override func setUp() {\n        super.setUp()\n\n        imageView = KFCrossPlatformImageView()\n        KingfisherManager.shared.downloader = ImageDownloader(name: \"testDownloader\")\n        KingfisherManager.shared.defaultOptions = [.waitForCache]\n        \n        cleanDefaultCache()\n    }\n    \n    override func tearDown() {\n        LSNocilla.sharedInstance().clearStubs()\n        imageView = nil\n        cleanDefaultCache()\n        KingfisherManager.shared.defaultOptions = .empty\n        super.tearDown()\n    }\n\n    @MainActor func testImageDownloadForImageView() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n\n        var progressBlockIsCalled = false\n        \n        imageView.kf.setImage(\n            with: url,\n            progressBlock: { _, _ in\n                progressBlockIsCalled = true\n                XCTAssertTrue(Thread.isMainThread)\n            })\n        {\n            result in\n            XCTAssertTrue(progressBlockIsCalled)\n            XCTAssertNotNil(result.value)\n\n            let value = result.value!\n            XCTAssertTrue(value.image.renderEqual(to: testImage))\n            XCTAssertTrue(self.imageView.image!.renderEqual(to: testImage))\n            \n            XCTAssertEqual(value.cacheType, .none)\n            XCTAssertTrue(Thread.isMainThread)\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testImageDownloadCompletionHandlerRunningOnMainQueue() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let customQueue = DispatchQueue(label: \"com.kingfisher.testQueue\")\n        imageView.kf.setImage(\n            with: url,\n            options: [.callbackQueue(.dispatch(customQueue))],\n            progressBlock: { _, _ in XCTAssertTrue(Thread.isMainThread) })\n        {\n            result in\n            XCTAssertTrue(Thread.isMainThread)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testImageDownloadWithResourceForImageView() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n\n        var progressBlockIsCalled = false\n\n        let resource = KF.ImageResource(downloadURL: url)\n        imageView.kf.setImage(\n            with: resource,\n            progressBlock: { _, _ in progressBlockIsCalled = true })\n        {\n            result in\n            XCTAssertTrue(progressBlockIsCalled)\n            XCTAssertNotNil(result.value)\n\n            let value = result.value!\n            XCTAssertTrue(value.image.renderEqual(to: testImage))\n            XCTAssertTrue(self.imageView.image!.renderEqual(to: testImage))\n\n            XCTAssertEqual(value.cacheType, .none)\n            XCTAssertTrue(Thread.isMainThread)\n\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testImageDownloadCancelForImageView() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData, length: 123)\n\n        let task = imageView.kf.setImage(\n            with: url,\n            progressBlock: { _, _ in XCTFail() })\n        {\n            result in\n            XCTAssertNotNil(result.error)\n            delay(0.1) { exp.fulfill() }\n        }\n\n        XCTAssertNotNil(task)\n        task?.cancel()\n        _ = stub.go()\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @MainActor func testImageDownloadCancelPartialTaskBeforeRequest() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n\n        let group = DispatchGroup()\n        \n        group.enter()\n        let task1 = KF.url(url)\n            .onFailure { _ in group.leave() }\n            .set(to: imageView)\n        \n        group.enter()\n        KF.url(url)\n            .onSuccess { _ in group.leave() }\n            .set(to: imageView)\n        \n        group.enter()\n        let anotherImageView = KFCrossPlatformImageView()\n        KF.url(url)\n            .onSuccess { _ in group.leave() }\n            .set(to: anotherImageView)\n        \n        task1?.cancel()\n        _ = stub.go()\n        \n        group.notify(queue: .main) {\n            delay(0.1) { exp.fulfill() }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testImageDownloadCancelAllTasksAfterRequestStarted() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n\n        let group = DispatchGroup()\n        \n        group.enter()\n        let task1 = imageView.kf.setImage(with: url) { result in\n            XCTAssertNotNil(result.error)\n            group.leave()\n        }\n        \n        group.enter()\n        let task2 = imageView.kf.setImage(with: url) { result in\n            XCTAssertNotNil(result.error)\n            group.leave()\n        }\n        \n        group.enter()\n        let task3 = imageView.kf.setImage(with: url) { result in\n            XCTAssertNotNil(result.error)\n            group.leave()\n        }\n\n        task1?.cancel()\n        task2?.cancel()\n        task3?.cancel()\n        _ = stub.go()\n        \n        group.notify(queue: .main) {\n            delay(0.1) { exp.fulfill() }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testImageDownloadMultipleCaches() {\n        \n        let cache1 = ImageCache(name: \"cache1\")\n        let cache2 = ImageCache(name: \"cache2\")\n        \n        cache1.clearDiskCache()\n        cache2.clearDiskCache()\n        cleanDefaultCache()\n        \n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let key = url.cacheKey\n\n        imageView.kf.setImage(with: url, options: [.targetCache(cache1)]) { result in\n\n            XCTAssertTrue(cache1.imageCachedType(forKey: key).cached)\n            XCTAssertFalse(cache2.imageCachedType(forKey: key).cached)\n            XCTAssertFalse(KingfisherManager.shared.cache.imageCachedType(forKey: key).cached)\n            \n            self.imageView.kf.setImage(with: url, options: [.targetCache(cache2), .waitForCache]) { result in\n                XCTAssertTrue(cache1.imageCachedType(forKey: key).cached)\n                XCTAssertTrue(cache2.imageCachedType(forKey: key).cached)\n                XCTAssertFalse(KingfisherManager.shared.cache.imageCachedType(forKey: key).cached)\n                exp.fulfill()\n            }\n        }\n        \n        waitForExpectations(timeout: 5) { error in\n            clearCaches([cache1, cache2])\n        }\n    }\n    \n    @MainActor func testIndicatorViewExisting() {\n        imageView.kf.indicatorType = .activity\n        XCTAssertNotNil(imageView.kf.indicator)\n        XCTAssertTrue(imageView.kf.indicator is ActivityIndicator)\n\n        imageView.kf.indicatorType = .none\n        XCTAssertNil(imageView.kf.indicator)\n    }\n    \n    @MainActor func testCustomizeStructIndicatorExisting() {\n        struct StructIndicator: Indicator {\n            let view = KFCrossPlatformView()\n            func startAnimatingView() {}\n            func stopAnimatingView() {}\n        }\n        \n        imageView.kf.indicatorType = .custom(indicator: StructIndicator())\n        XCTAssertNotNil(imageView.kf.indicator)\n        XCTAssertTrue(imageView.kf.indicator is StructIndicator)\n        \n        imageView.kf.indicatorType = .none\n        XCTAssertNil(imageView.kf.indicator)\n    }\n    \n    @MainActor func testActivityIndicatorViewAnimating() {\n        imageView.kf.indicatorType = .activity\n        \n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        imageView.kf.setImage(with: url, progressBlock: { receivedSize, totalSize in\n            let indicator = self.imageView.kf.indicator\n            XCTAssertNotNil(indicator)\n            XCTAssertFalse(indicator!.view.isHidden)\n        })\n        {\n            result in\n            let indicator = self.imageView.kf.indicator\n            XCTAssertTrue(indicator!.view.isHidden)\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testCanUseImageIndicatorViewAnimating() {\n        \n        imageView.kf.indicatorType = .image(imageData: testImageData)\n        XCTAssertTrue(imageView.kf.indicator is ImageIndicator)\n        let image = (imageView.kf.indicator?.view as? KFCrossPlatformImageView)?.image\n        XCTAssertNotNil(image)\n        XCTAssertTrue(image!.renderEqual(to: testImage))\n        \n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        imageView.kf.setImage(with: url, progressBlock: { receivedSize, totalSize in\n            let indicator = self.imageView.kf.indicator\n            XCTAssertNotNil(indicator)\n            XCTAssertFalse(indicator!.view.isHidden)\n        })\n        {\n            result in\n            let indicator = self.imageView.kf.indicator\n            XCTAssertTrue(indicator!.view.isHidden)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testCancelImageTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n\n        imageView.kf.setImage(with: url, progressBlock: { _, _ in XCTFail() }) { result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            delay(0.1) { exp.fulfill() }\n        }\n\n        self.imageView.kf.cancelDownloadTask()\n        _ = stub.go()\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testDownloadForMultipleURLs() {\n        let exp = expectation(description: #function)\n\n        stub(testURLs[0], data: testImageData)\n        stub(testURLs[1], data: testImageData)\n\n        let group = DispatchGroup()\n        \n        group.enter()\n        imageView.kf.setImage(with: testURLs[0]) { result in\n            // The download succeeded, but not with the resource we want.\n            XCTAssertNotNil(result.error)\n            if case .imageSettingError(\n                reason: .notCurrentSourceTask(let result, _, let source)) = result.error!\n            {\n                XCTAssertEqual(source.url, testURLs[0])\n                XCTAssertEqual(result?.originalSource.url, testURLs[0])\n                XCTAssertNotEqual(result!.image, self.imageView.image)\n            } else {\n                XCTFail()\n            }\n            group.leave()\n        }\n        \n        group.enter()\n        self.imageView.kf.setImage(with: testURLs[1]) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value?.source.url, testURLs[1])\n            XCTAssertEqual(result.value!.image, self.imageView.image)\n            group.leave()\n        }\n        \n        group.notify(queue: .main, execute: exp.fulfill)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testSettingNilURL() {\n        let exp = expectation(description: #function)\n        let url: URL? = nil\n        imageView.kf.setImage(with: url, progressBlock: { _, _ in XCTFail() }) {\n            result in\n            XCTAssertNotNil(result.error)\n            guard case .imageSettingError(reason: .emptySource) = result.error! else {\n                XCTFail()\n                fatalError()\n            }\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testSettingImageWhileKeepingCurrentOne() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        imageView.image = testImage\n        imageView.kf.setImage(with: url) { result in }\n        XCTAssertNil(imageView.image)\n        \n        imageView.image = testImage\n        imageView.kf.setImage(with: url, options: [.keepCurrentImageWhileLoading]) { result in\n            XCTAssertEqual(self.imageView.image, result.value!.image)\n            XCTAssertNotEqual(self.imageView.image, testImage)\n            exp.fulfill()\n        }\n        XCTAssertEqual(testImage, imageView.image)\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testSettingImageKeepingRespectingPlaceholder() {\n        let exp = expectation(description: #function)\n        \n        // While current image is nil, set placeholder\n        let url = testURLs[0]\n        imageView.kf.setImage(with: url, placeholder: testImage, options: [.keepCurrentImageWhileLoading]) { result in\n            exp.fulfill()\n        }\n        XCTAssertEqual(testImage, imageView.image)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testMe() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        // While current image is not nil, keep it\n        let anotherImage = KFCrossPlatformImage(data: testImageJEPGData)\n        imageView.image = anotherImage\n        imageView.kf.setImage(with: url, placeholder: testImage, options: [.keepCurrentImageWhileLoading]) { result in\n            XCTAssertNotEqual(self.imageView.image, anotherImage)\n            exp.fulfill()\n        }\n        XCTAssertEqual(anotherImage, imageView.image)\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testSetGIFImageOnlyFirstFrameThenFullFrames() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageGIFData, length: 123)\n\n        func loadFullGIFImage() {\n            ImageCache.default.clearMemoryCache()\n            \n            imageView.kf.setImage(with: url, progressBlock: { _, _ in XCTFail() })\n            {\n                result in\n                let image = result.value?.image\n                XCTAssertNotNil(image)\n                XCTAssertNotNil(image!.kf.images)\n                XCTAssertEqual(image!.kf.images?.count, 8)\n                \n                XCTAssertEqual(result.value!.cacheType, .disk)\n                XCTAssertTrue(Thread.isMainThread)\n                \n                exp.fulfill()\n            }\n        }\n\n        var progressBlockIsCalled = false\n        imageView.kf.setImage(with: url, options: [.onlyLoadFirstFrame, .waitForCache], progressBlock: { _, _ in\n            progressBlockIsCalled = true\n            XCTAssertTrue(Thread.isMainThread)\n        })\n        {\n            result in\n\n            XCTAssertTrue(progressBlockIsCalled)\n            let image = result.value?.image\n            XCTAssertNotNil(image)\n            XCTAssertNil(image!.kf.images)\n\n            XCTAssert(result.value!.cacheType == .none)\n\n            let memory = KingfisherManager.shared.cache.memoryStorage.value(forKey: url.cacheKey)\n            XCTAssertNotNil(memory)\n\n            let disk = try! KingfisherManager.shared.cache.diskStorage.value(forKey: url.cacheKey)\n            XCTAssertNotNil(disk)\n\n            XCTAssertTrue(Thread.isMainThread)\n            loadFullGIFImage()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    // https://github.com/onevcat/Kingfisher/issues/1923\n    @MainActor func testLoadGIFImageWithDifferentOptions() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageGIFData)\n        \n        imageView.kf.setImage(with: url) { result in\n            let fullImage = result.value?.image\n            XCTAssertNotNil(fullImage)\n            XCTAssertEqual(fullImage!.kf.images?.count, 8)\n            \n            self.imageView.kf.setImage(with: url, options: [.onlyLoadFirstFrame]) { result in\n                let firstFrameImage = result.value?.image\n                XCTAssertNotNil(firstFrameImage)\n                XCTAssertNil(firstFrameImage!.kf.images)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3)\n    }\n    \n    // https://github.com/onevcat/Kingfisher/issues/665\n    // The completion handler should be called even when the image view loading url gets changed.\n    @MainActor func testIssue665() {\n        let exp = expectation(description: #function)\n\n        stub(testURLs[0], data: testImageData)\n        stub(testURLs[1], data: testImageData)\n\n        let group = DispatchGroup()\n        \n        group.enter()\n        imageView.kf.setImage(with: testURLs[0]) { _ in\n            group.leave()\n        }\n        \n        group.enter()\n        imageView.kf.setImage(with: testURLs[1]) { _ in\n            group.leave()\n        }\n        \n        group.notify(queue: .main, execute: exp.fulfill)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @MainActor func testImageSettingWithPlaceholder() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n\n        stub(url, data: testImageData, length: 123)\n\n        let emptyImage = KFCrossPlatformImage()\n        var processBlockCalled = false\n\n        imageView.kf.setImage(\n            with: url,\n            placeholder: emptyImage,\n            progressBlock: { _, _ in\n                processBlockCalled = true\n                XCTAssertEqual(self.imageView.image, emptyImage)\n            })\n        {\n            result in\n            XCTAssertTrue(processBlockCalled)\n            XCTAssertTrue(self.imageView.image!.renderEqual(to: testImage))\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @MainActor func testImageSettingWithCustomizePlaceholder() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n\n        stub(url, data: testImageData, length: 123)\n\n        let view = KFCrossPlatformView()\n        var processBlockCalled = false\n\n        imageView.kf.setImage(\n            with: url,\n            placeholder: view,\n            progressBlock: { _, _ in\n                processBlockCalled = true\n                XCTAssertNil(self.imageView.image)\n                XCTAssertTrue(self.imageView.subviews.contains(view))\n            })\n        {\n            result in\n            XCTAssertTrue(processBlockCalled)\n            XCTAssertTrue(self.imageView.image!.renderEqual(to: testImage))\n            XCTAssertFalse(self.imageView.subviews.contains(view))\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @MainActor func testSettingNonWorkingImageWithCustomizePlaceholderAndFailureImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n\n        stub(url, errorCode: 404)\n\n        let view = KFCrossPlatformView()\n\n        imageView.kf.setImage(\n            with: url,\n            placeholder: view,\n            options: [.onFailureImage(testImage)])\n        {\n            result in\n            XCTAssertEqual(self.imageView.image, testImage)\n            XCTAssertFalse(self.imageView.subviews.contains(view))\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testSettingNonWorkingImageWithFailureImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, errorCode: 404)\n\n        imageView.kf.setImage(with: url, options: [.onFailureImage(testImage)]) {\n            result in\n            XCTAssertNil(result.value)\n\n            if case KingfisherError.responseError(let reason) = result.error!,\n               case .URLSessionError(error: let nsError) = reason\n            {\n                XCTAssertEqual((nsError as NSError).code, 404)\n            } else {\n                XCTFail()\n            }\n            XCTAssertEqual(self.imageView.image, testImage)\n            exp.fulfill()\n        }\n        XCTAssertNil(imageView.image)\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    @MainActor func testSettingNonWorkingImageWithEmptyFailureImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, errorCode: 404)\n        \n        imageView.kf.setImage(with: url, placeholder: testImage, options: [.onFailureImage(nil)]) {\n            result in\n            XCTAssertNil(result.value)\n            XCTAssertNil(self.imageView.image)\n            exp.fulfill()\n        }\n        XCTAssertEqual(testImage, imageView.image)\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    @MainActor func testSettingNonWorkingImageWithoutFailureImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, errorCode: 404)\n        \n        imageView.kf.setImage(with: url, placeholder: testImage) {\n            result in\n            XCTAssertNil(result.value)\n            XCTAssertEqual(testImage, self.imageView.image)\n            exp.fulfill()\n        }\n        XCTAssertEqual(testImage, imageView.image)\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    // https://github.com/onevcat/Kingfisher/issues/1053\n    @MainActor func testSetSameURLWithDifferentProcessors() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        \n        stub(url, data: testImageData)\n        \n        let size1 = CGSize(width: 10, height: 10)\n        let p1 = ResizingImageProcessor(referenceSize: size1)\n        \n        let size2 = CGSize(width: 20, height: 20)\n        let p2 = ResizingImageProcessor(referenceSize: size2)\n        \n        let group = DispatchGroup()\n        \n        group.enter()\n        imageView.kf.setImage(with: url, options: [.processor(p1), .cacheMemoryOnly]) { result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isNotCurrentTask)\n            group.leave()\n        }\n        \n        group.enter()\n        imageView.kf.setImage(with: url, options: [.processor(p2), .cacheMemoryOnly]) { result in\n            XCTAssertNotNil(result.value)\n            XCTAssertEqual(result.value!.image.size, size2)\n            group.leave()\n        }\n        \n        group.notify(queue: .main) { exp.fulfill() }\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    @MainActor func testMemoryImageCacheExtendingExpirationTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        let options: KingfisherOptionsInfo = [.cacheMemoryOnly, .memoryCacheExpiration(.seconds(1)), .memoryCacheAccessExtendingExpiration(.expirationTime(.seconds(100)))]\n       \n        imageView.kf.setImage(with: url, options: options) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertTrue(result.value!.cacheType == .none)\n            \n            let cacheKey = result.value!.source.cacheKey as NSString\n            let expirationTime1 = ImageCache.default.memoryStorage.storage.object(forKey: cacheKey)?.estimatedExpiration\n            XCTAssertNotNil(expirationTime1)\n            \n            delay(0.1, block: {\n                self.imageView.kf.setImage(with: url, options: options) { result in\n                    XCTAssertNotNil(result.value?.image)\n                    XCTAssertTrue(result.value!.cacheType == .memory)\n                    \n                    let expirationTime2 = ImageCache.default.memoryStorage.storage.object(forKey: cacheKey)?.estimatedExpiration\n                    \n                    XCTAssertNotNil(expirationTime2)\n                    XCTAssertNotEqual(expirationTime1, expirationTime2)\n                    XCTAssert(expirationTime1!.isPast(referenceDate: expirationTime2!))\n                    XCTAssertGreaterThan(expirationTime2!.timeIntervalSince(expirationTime1!), 10)\n                    \n                    exp.fulfill()\n                }\n            })\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testMemoryImageCacheNotExtendingExpirationTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        let options: KingfisherOptionsInfo = [.cacheMemoryOnly, .memoryCacheExpiration(.seconds(1)), .memoryCacheAccessExtendingExpiration(.none)]\n  \n        imageView.kf.setImage(with: url, options: options) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertTrue(result.value!.cacheType == .none)\n            \n            let cacheKey = result.value!.source.cacheKey as NSString\n            let expirationTime1 = ImageCache.default.memoryStorage.storage.object(forKey: cacheKey)?.estimatedExpiration\n            XCTAssertNotNil(expirationTime1)\n            \n            delay(0.1, block: {\n                self.imageView.kf.setImage(with: url, options: options) { result in\n                    XCTAssertNotNil(result.value?.image)\n                    XCTAssertTrue(result.value!.cacheType == .memory)\n                    \n                    let expirationTime2 = ImageCache.default.memoryStorage.storage.object(forKey: cacheKey)?.estimatedExpiration\n                    \n                    XCTAssertNotNil(expirationTime2)\n                    XCTAssertEqual(expirationTime1, expirationTime2)\n                    \n                    exp.fulfill()\n                }\n            })\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @MainActor func testDiskImageCacheExtendingExpirationTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let options: KingfisherOptionsInfo = [.memoryCacheExpiration(.expired),\n                                              .diskCacheExpiration(.seconds(2)),\n                                              .diskCacheAccessExtendingExpiration(.expirationTime(.seconds(100)))]\n\n        imageView.kf.setImage(with: url, options: options) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertTrue(result.value!.cacheType == .none)\n\n            delay(1, block: {\n                self.imageView.kf.setImage(with: url, options: options) { result in\n                    XCTAssertNotNil(result.value?.image)\n                    XCTAssertTrue(result.value!.cacheType == .disk)\n                    delay(2, block: {\n                        self.imageView.kf.setImage(with: url, options: options) { result in\n                            XCTAssertNotNil(result.value?.image)\n                            XCTAssertTrue(result.value!.cacheType == .disk)\n\n                            exp.fulfill()\n                        }\n                    })\n                }\n            })\n        }\n\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n\n    @MainActor func testDiskImageCacheNotExtendingExpirationTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let options: KingfisherOptionsInfo = [.memoryCacheExpiration(.expired),\n                                              .diskCacheExpiration(.seconds(2)),\n                                              .diskCacheAccessExtendingExpiration(.none)]\n\n        imageView.kf.setImage(with: url, options: options) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertTrue(result.value!.cacheType == .none)\n\n            delay(1, block: {\n                self.imageView.kf.setImage(with: url, options: options) { result in\n                    XCTAssertNotNil(result.value?.image)\n                    XCTAssertTrue(result.value!.cacheType == .disk)\n                    delay(2, block: {\n                        self.imageView.kf.setImage(with: url, options: options) { result in\n                            XCTAssertNotNil(result.value?.image)\n                            XCTAssertTrue(result.value!.cacheType == .none)\n\n                            exp.fulfill()\n                        }\n                    })\n                }\n            })\n        }\n\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n\n    @MainActor func testImageSettingWithAlternativeSource() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        imageView.kf.setImage(\n            with: .network(brokenURL),\n            options: [.alternativeSources([.network(url)])]\n        ) { result in\n            XCTAssertNotNil(result.value)\n            XCTAssertEqual(result.value!.source.url, url)\n            XCTAssertEqual(result.value!.originalSource.url, brokenURL)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @MainActor func testImageSettingCanCancelAlternativeSource() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let dataStub = delayedStub(url, data: testImageData)\n\n        let brokenURL = testURLs[1]\n        let brokenStub = delayedStub(brokenURL, data: Data())\n\n        var finishCalled = false\n\n        delay(0.1) {\n            _ = brokenStub.go()\n        }\n        delay(0.3) {\n            self.imageView.kf.cancelDownloadTask()\n        }\n        delay(0.5) {\n            _ = dataStub.go()\n            XCTAssertTrue(finishCalled)\n            exp.fulfill()\n        }\n\n        imageView.kf.setImage(\n            with: .network(brokenURL),\n            options: [.alternativeSources([.network(url)])]\n        ) { result in\n            finishCalled = true\n            XCTAssertNotNil(result.error)\n            guard case .requestError(reason: .taskCancelled(let task, _)) = result.error! else {\n                XCTFail(\"The error should be a task cancelled.\")\n                return\n            }\n            XCTAssertEqual(task.task.originalRequest?.url, url, \"Should be the alternative url cancelled.\")\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)\n    @MainActor func testLowDataModeSource() {\n        let exp = expectation(description: #function)\n\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        // Stub a failure of `.constrained`. It is what happens when an image downloading fails when low data mode on.\n        let brokenURL = testURLs[1]\n        let error = URLError(\n            .notConnectedToInternet,\n            userInfo: [NSURLErrorNetworkUnavailableReasonKey: URLError.NetworkUnavailableReason.constrained.rawValue]\n        )\n        stub(brokenURL, error: error)\n\n        imageView.kf.setImage(with: .network(brokenURL), options: [.lowDataMode(.network(url))]) { result in\n            XCTAssertNotNil(result.value)\n            XCTAssertEqual(result.value?.source.url, url)\n            XCTAssertEqual(result.value?.originalSource.url, brokenURL)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n}\n\n#if compiler(>=6)\nextension KFCrossPlatformView: @retroactive Placeholder {}\n#else\nextension KFCrossPlatformView: Placeholder {}\n#endif\n"
  },
  {
    "path": "Tests/KingfisherTests/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>8.8.0</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>3260</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "Tests/KingfisherTests/KingfisherManagerTests.swift",
    "content": "//\n//  KingfisherManagerTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/10/22.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nactor CallingChecker {\n    var called = false\n    func mark() {\n        called = true\n    }\n    \n    func checkCancelBehavior(\n        stub: LSStubResponseDSL,\n        block: @escaping () async throws -> Void\n    ) async throws {\n        let task = Task {\n            do {\n                _ = try await block()\n                XCTFail()\n            } catch {\n                mark()\n                XCTAssertTrue((error as! KingfisherError).isTaskCancelled)\n            }\n        }\n        try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10)\n        task.cancel()\n        _ = stub.go()\n        try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10)\n        XCTAssertTrue(called)\n    }\n}\n\nclass KingfisherManagerTests: XCTestCase {\n    \n    var manager: KingfisherManager!\n    \n    override class func setUp() {\n        super.setUp()\n        LSNocilla.sharedInstance().start()\n    }\n    \n    override class func tearDown() {\n        LSNocilla.sharedInstance().stop()\n        super.tearDown()\n    }\n    \n    override func setUp() {\n        super.setUp()\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n        let uuid = UUID()\n        let downloader = ImageDownloader(name: \"test.manager.\\(uuid.uuidString)\")\n        let cache = ImageCache(name: \"test.cache.\\(uuid.uuidString)\")\n        \n        manager = KingfisherManager(downloader: downloader, cache: cache)\n        manager.defaultOptions = [.waitForCache]\n    }\n    \n    override func tearDown() {\n        LSNocilla.sharedInstance().clearStubs()\n        clearCaches([manager.cache])\n        cleanDefaultCache()\n        manager = nil\n        super.tearDown()\n    }\n    \n    func testRetrieveImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let manager = self.manager!\n        manager.retrieveImage(with: url) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .none)\n\n        manager.retrieveImage(with: url) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .memory)\n\n        manager.cache.clearMemoryCache()\n        manager.retrieveImage(with: url) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .disk)\n\n        manager.cache.clearMemoryCache()\n        manager.cache.clearDiskCache {\n            manager.retrieveImage(with: url) { result in\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value!.cacheType, .none)\n                exp.fulfill()\n        }}}}}\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testRetrieveImageAsync() async throws {\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let manager = self.manager!\n        \n        var result = try await manager.retrieveImage(with: url)\n        XCTAssertNotNil(result.image)\n        XCTAssertEqual(result.cacheType, .none)\n        \n        result = try await manager.retrieveImage(with: url)\n        XCTAssertNotNil(result.image)\n        XCTAssertEqual(result.cacheType, .memory)\n        \n        manager.cache.clearMemoryCache()\n        result = try await manager.retrieveImage(with: url)\n        XCTAssertNotNil(result.image)\n        XCTAssertEqual(result.cacheType, .disk)\n        \n        manager.cache.clearMemoryCache()\n        await manager.cache.clearDiskCache()\n        result = try await manager.retrieveImage(with: url)\n        XCTAssertNotNil(result.image)\n        XCTAssertEqual(result.cacheType, .none)\n    }\n    \n    func testRetrieveImageWithProcessor() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        let p = RoundCornerImageProcessor(cornerRadius: 20)\n        let manager = self.manager!\n\n        manager.retrieveImage(with: url, options: [.processor(p)]) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .none)\n            \n        manager.retrieveImage(with: url) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .none,\n                           \"Need a processor to get correct image. Cannot get from cache, need download again.\")\n\n        manager.retrieveImage(with: url, options: [.processor(p)]) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .memory)\n                    \n        self.manager.cache.clearMemoryCache()\n        manager.retrieveImage(with: url, options: [.processor(p)]) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .disk)\n                        \n        self.manager.cache.clearMemoryCache()\n        self.manager.cache.clearDiskCache {\n            self.manager.retrieveImage(with: url, options: [.processor(p)]) { result in\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value!.cacheType, .none)\n\n                exp.fulfill()\n        }}}}}}\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testRetrieveImageOriginalCacheClaimsCachedButReturnsNoneShouldStillCallback() {\n        let exp = expectation(description: #function)\n\n        let uuid = UUID()\n        let originalCache = ImageCache(name: \"test.original.\\(uuid.uuidString)\")\n        let targetCache = ImageCache(name: \"test.target.\\(uuid.uuidString)\")\n\n        addTeardownBlock {\n            clearCaches([originalCache, targetCache])\n        }\n\n        let url = testURLs[0]\n        let key = url.cacheKey\n\n        stub(url, data: testImageData)\n\n        // Store invalid data as the \"original\" image. This makes `imageCachedType` report `.disk`,\n        // while `retrieveImage` returns `.none` due to deserialization failure.\n        do {\n            try originalCache.diskStorage.store(value: Data([0x01, 0x02, 0x03]), forKey: key)\n        } catch {\n            XCTFail(\"Failed to prepare original cache: \\(error)\")\n            return\n        }\n\n        XCTAssertTrue(\n            originalCache.imageCachedType(forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier).cached\n        )\n\n        let p = RoundCornerImageProcessor(cornerRadius: 20)\n        manager.retrieveImage(\n            with: url,\n            options: [\n                .processor(p),\n                .originalCache(originalCache),\n                .targetCache(targetCache)\n            ]\n        ) { result in\n            // It should never hang without calling the completion handler.\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value?.cacheType, CacheType.none)\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testRetrieveImageForceRefresh() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        manager.cache.store(\n            testImage,\n            original: testImageData,\n            forKey: url.cacheKey,\n            processorIdentifier: DefaultImageProcessor.default.identifier,\n            cacheSerializer: DefaultCacheSerializer.default,\n            toDisk: true)\n        {\n            _ in\n            XCTAssertTrue(self.manager.cache.imageCachedType(forKey: url.cacheKey).cached)\n            self.manager.retrieveImage(with: url, options: [.forceRefresh]) { result in\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value!.cacheType, .none)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testRetrieveImageCancel() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData, length: 123)\n\n        let task = manager.retrieveImage(with: url) {\n            result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            exp.fulfill()\n        }\n\n        XCTAssertNotNil(task)\n        task?.cancel()\n        _ = stub.go()\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testRetrieveImageCancelAsync() async throws {\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData, length: 123)\n\n        let checker = CallingChecker()\n        try await checker.checkCancelBehavior(stub: stub) {\n            _ = try await self.manager.retrieveImage(with: url)\n        }\n    }\n    \n    /// Test to reproduce the Swift Task Continuation Misuse issue\n    /// This test verifies that continuations are properly resumed even under rapid cancellation scenarios\n    /// \n    /// NOTE: Single test run may not reproduce the issue, but running this test repeatedly \n    /// (e.g., 100 times in Xcode) will almost certainly trigger the SWIFT TASK CONTINUATION MISUSE warning.\n    /// This confirms the existence of a race condition in the async retrieveImage implementation.\n    func testRetrieveImageContinuationMisuseReproduction() async throws {\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData, length: 123)\n        \n        // Create multiple concurrent tasks that are cancelled quickly\n        // This should reproduce the continuation leak scenario\n        let taskCount = 50 // Increased to make race condition more likely\n        var tasks: [Task<Void, Never>] = []\n        \n        for i in 0..<taskCount {\n            let task = Task {\n                do {\n                    _ = try await self.manager.retrieveImage(with: url)\n                    // If we reach here without cancellation, something is wrong\n                    print(\"Task \\(i) completed without cancellation - unexpected\")\n                } catch {\n                    // This should be a cancellation error\n                    if let kfError = error as? KingfisherError, kfError.isTaskCancelled {\n                        // Expected cancellation\n                    } else if error is CancellationError {\n                        // Expected cancellation\n                    } else {\n                        print(\"Task \\(i) failed with unexpected error: \\(error)\")\n                    }\n                }\n            }\n            tasks.append(task)\n            \n            // Cancel immediately after creation to create race conditions\n            task.cancel()\n            \n            // Add a tiny delay to create more variation in timing\n            if i % 5 == 0 {\n                try await Task.sleep(nanoseconds: NSEC_PER_SEC / 1000) // 1ms\n            }\n        }\n        \n        // Wait a bit to ensure all tasks have had a chance to start and be cancelled\n        try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10) // 100ms\n        \n        // Complete the stub to allow any pending operations to finish\n        _ = stub.go()\n        \n        // Wait for all tasks to complete\n        for task in tasks {\n            await task.value\n        }\n        \n        // If we get here without hanging, the continuation handling is working correctly\n        // The test passes if no SWIFT TASK CONTINUATION MISUSE warning is printed to console\n    }\n    \n    /// Another test that creates a more specific race condition scenario\n    /// This test checks the exact timing described in the issue\n    /// \n    /// NOTE: Like the previous test, run this repeatedly to increase chances of reproducing the issue.\n    func testRetrieveImageRaceConditionSpecific() async throws {\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData, length: 5000) // Longer delay\n        \n        // This creates the specific race condition:\n        // 1. Task starts\n        // 2. Gets to the withCheckedThrowingContinuation\n        // 3. Cancel happens before the inner retrieveImage call completes setup\n        let task = Task {\n            do {\n                _ = try await self.manager.retrieveImage(with: url)\n                XCTFail(\"Task should have been cancelled\")\n            } catch {\n                // Should be cancelled\n                XCTAssertTrue((error as? KingfisherError)?.isTaskCancelled == true)\n            }\n        }\n        \n        // Very short delay to let the task start but not complete\n        try await Task.sleep(nanoseconds: NSEC_PER_SEC / 1000) // 1ms\n        \n        // Cancel before the network stub is triggered\n        task.cancel()\n        \n        // Now trigger the network response\n        _ = stub.go()\n        \n        // Wait for the task to complete\n        await task.value\n    }\n    \n    func testSuccessCompletionHandlerRunningOnMainQueueByDefault() {\n        let progressExpectation = expectation(description: \"progressBlock running on main queue\")\n        let completionExpectation = expectation(description: \"completionHandler running on main queue\")\n\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n        \n        manager.retrieveImage(with: url, options: nil, progressBlock: { _, _ in\n            XCTAssertTrue(Thread.isMainThread)\n            progressExpectation.fulfill()})\n        {\n            result in\n            XCTAssertNil(result.error)\n            XCTAssertTrue(Thread.isMainThread)\n            completionExpectation.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testShouldNotDownloadImageIfCacheOnlyAndNotInCache() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        manager.retrieveImage(with: url, options: [.onlyFromCache]) { result in\n            XCTAssertNil(result.value)\n            XCTAssertNotNil(result.error)\n            if case .cacheError(reason: .imageNotExisting(let key)) = result.error! {\n                XCTAssertEqual(key, url.cacheKey)\n            } else {\n                XCTFail()\n            }\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testErrorCompletionHandlerRunningOnMainQueueByDefault() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, statusCode: 404)\n\n        manager.retrieveImage(with: url) { result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(Thread.isMainThread)\n            XCTAssertTrue(result.error!.isInvalidResponseStatusCode(404))\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testSuccessCompletionHandlerRunningOnCustomQueue() {\n        let progressExpectation = expectation(description: \"progressBlock running on custom queue\")\n        let completionExpectation = expectation(description: \"completionHandler running on custom queue\")\n\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n\n        let customQueue = DispatchQueue(label: \"com.kingfisher.testQueue\")\n        let options: KingfisherOptionsInfo = [.callbackQueue(.dispatch(customQueue))]\n        manager.retrieveImage(with: url, options: options, progressBlock: { _, _ in\n            XCTAssertTrue(Thread.isMainThread)\n            progressExpectation.fulfill()\n        })\n        {\n            result in\n            XCTAssertNil(result.error)\n            dispatchPrecondition(condition: .onQueue(customQueue))\n            completionExpectation.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testLoadCacheCompletionHandlerRunningOnCustomQueue() {\n        let completionExpectation = expectation(description: \"completionHandler running on custom queue\")\n\n        let url = testURLs[0]\n        manager.cache.store(testImage, forKey: url.cacheKey)\n\n        let customQueue = DispatchQueue(label: \"com.kingfisher.testQueue\")\n        manager.retrieveImage(with: url, options: [.callbackQueue(.dispatch(customQueue))]) {\n            result in\n            XCTAssertNil(result.error)\n            dispatchPrecondition(condition: .onQueue(customQueue))\n            completionExpectation.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDefaultOptionCouldApply() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        manager.defaultOptions = [.scaleFactor(2)]\n        manager.retrieveImage(with: url, completionHandler: { result in\n            #if !os(macOS)\n            XCTAssertEqual(result.value!.image.scale, 2.0)\n            #endif\n            exp.fulfill()\n        })\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testOriginalImageCouldBeStored() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let manager = self.manager!\n        let p = SimpleProcessor()\n        let options = KingfisherParsedOptionsInfo([.processor(p), .cacheOriginalImage])\n        let source = Source.network(url)\n        let context = RetrievingContext(options: options, originalSource: source)\n        manager.loadAndCacheImage(source: .network(url), context: context) { result in\n            \n            var imageCached = manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n            var originalCached = manager.cache.imageCachedType(forKey: url.cacheKey)\n\n            XCTAssertEqual(imageCached, .memory)\n\n            delay(0.3) {\n                manager.cache.clearMemoryCache()\n                \n                imageCached = manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n                originalCached = manager.cache.imageCachedType(forKey: url.cacheKey)\n                XCTAssertEqual(imageCached, .disk)\n                XCTAssertEqual(originalCached, .disk)\n                \n                exp.fulfill()\n            }\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testOriginalImageNotBeStoredWithoutOptionSet() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let p = SimpleProcessor()\n        let options = KingfisherParsedOptionsInfo([.processor(p), .waitForCache])\n        let source = Source.network(url)\n        let context = RetrievingContext(options: options, originalSource: source)\n        manager.loadAndCacheImage(source: .network(url), context: context) {\n            result in\n            var imageCached = self.manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n            var originalCached = self.manager.cache.imageCachedType(forKey: url.cacheKey)\n            \n            XCTAssertEqual(imageCached, .memory)\n            XCTAssertEqual(originalCached, .none)\n            \n            self.manager.cache.clearMemoryCache()\n            \n            imageCached = self.manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n            originalCached = self.manager.cache.imageCachedType(forKey: url.cacheKey)\n            XCTAssertEqual(imageCached, .disk)\n            XCTAssertEqual(originalCached, .none)\n            \n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCouldProcessOnOriginalImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        \n        manager.cache.store(\n            testImage,\n            original: testImageData,\n            forKey: url.cacheKey,\n            processorIdentifier: DefaultImageProcessor.default.identifier,\n            cacheSerializer: DefaultCacheSerializer.default,\n            toDisk: true)\n        {\n            _ in\n            let p = SimpleProcessor()\n            \n            let cached = self.manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n            XCTAssertFalse(cached.cached)\n            \n            // No downloading will happen\n            self.manager.retrieveImage(with: url, options: [.processor(p)]) { result in\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value!.cacheType, .none)\n                XCTAssertTrue(p.processed)\n                \n                // The processed image should be cached\n                let cached = self.manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n                XCTAssertTrue(cached.cached)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testFailingProcessOnOriginalImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        \n        manager.cache.store(\n            testImage,\n            original: testImageData,\n            forKey: url.cacheKey,\n            processorIdentifier: DefaultImageProcessor.default.identifier,\n            cacheSerializer: DefaultCacheSerializer.default,\n            toDisk: true)\n        {\n            _ in\n            let p = FailingProcessor()\n            \n            let cached = self.manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n            XCTAssertFalse(cached.cached)\n            \n            // No downloading will happen\n            self.manager.retrieveImage(with: url, options: [.processor(p)]) { result in\n                XCTAssertNotNil(result.error)\n                XCTAssertTrue(p.processed)\n                if case .processorError(reason: .processingFailed(let processor, _)) = result.error! {\n                    XCTAssertEqual(processor.identifier, p.identifier)\n                } else {\n                    XCTFail()\n                }\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testFailingProcessOnDataProviderImage() {\n        let exp = expectation(description: #function)\n        let provider = SimpleImageDataProvider(cacheKey: \"key\") { .success(testImageData) }\n        let p = FailingProcessor()\n        let options = [KingfisherOptionsInfoItem.processor(p), .processingQueue(.mainCurrentOrAsync)]\n        _ = manager.retrieveImage(with: .provider(provider), options: options) { result in\n            XCTAssertNotNil(result.error)\n            if case .processorError(reason: .processingFailed(let processor, _)) = result.error! {\n                XCTAssertEqual(processor.identifier, p.identifier)\n            } else {\n                XCTFail()\n            }\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCacheOriginalImageWithOriginalCache() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        \n        let originalCache = ImageCache(name: \"test-originalCache\")\n        \n        // Clear original cache first.\n        originalCache.clearMemoryCache()\n        originalCache.clearDiskCache {\n            \n            XCTAssertEqual(originalCache.imageCachedType(forKey: url.cacheKey), .none)\n            \n            stub(url, data: testImageData)\n            \n            let p = RoundCornerImageProcessor(cornerRadius: 20)\n            self.manager.retrieveImage(\n                with: url,\n                options: [.processor(p), .cacheOriginalImage, .originalCache(originalCache)])\n            {\n                result in\n                let originalCached = originalCache.imageCachedType(forKey: url.cacheKey)\n                XCTAssertEqual(originalCached, .disk)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    func testCouldProcessOnOriginalImageWithOriginalCache() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        \n        let originalCache = ImageCache(name: \"test-originalCache\")\n        \n        // Clear original cache first.\n        originalCache.clearMemoryCache()\n        originalCache.clearDiskCache {\n            originalCache.store(\n                testImage,\n                original: testImageData,\n                forKey: url.cacheKey,\n                processorIdentifier: DefaultImageProcessor.default.identifier,\n                cacheSerializer: DefaultCacheSerializer.default,\n                toDisk: true)\n            {\n                _ in\n                let p = SimpleProcessor()\n                \n                let cached = self.manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n                XCTAssertFalse(cached.cached)\n                \n                // No downloading will happen\n                self.manager.retrieveImage(with: url, options: [.processor(p), .originalCache(originalCache)]) {\n                    result in\n                    XCTAssertNotNil(result.value?.image)\n                    XCTAssertEqual(result.value!.cacheType, .none)\n                    XCTAssertTrue(p.processed)\n                    \n                    // The processed image should be cached\n                    let cached = self.manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n                    XCTAssertTrue(cached.cached)\n                    exp.fulfill()\n                }\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCouldProcessDoNotHappenWhenSerializerCachesTheProcessedData() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        \n        stub(url, data: testImageData)\n        \n        let s = DefaultCacheSerializer()\n\n        let p1 = SimpleProcessor()\n        let options1: KingfisherOptionsInfo = [.processor(p1), .cacheSerializer(s), .waitForCache]\n        let source = Source.network(url)\n        \n        manager.retrieveImage(with: source, options: options1) { result in\n            XCTAssertTrue(p1.processed)\n            \n            let p2 = SimpleProcessor()\n            let options2: KingfisherOptionsInfo = [.processor(p2), .cacheSerializer(s), .waitForCache]\n            self.manager.cache.clearMemoryCache()\n            \n            self.manager.retrieveImage(with: source, options: options2) { result in\n                XCTAssertEqual(result.value?.cacheType, .disk)\n                XCTAssertFalse(p2.processed)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testCouldProcessAgainWhenSerializerCachesOriginalData() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        \n        stub(url, data: testImageData)\n        \n        var s = DefaultCacheSerializer()\n        s.preferCacheOriginalData = true\n\n        let p1 = SimpleProcessor()\n        let options1: KingfisherOptionsInfo = [.processor(p1), .cacheSerializer(s), .waitForCache]\n        let source = Source.network(url)\n        \n        manager.retrieveImage(with: source, options: options1) { [s] result in\n            XCTAssertTrue(p1.processed)\n            \n            let p2 = SimpleProcessor()\n            let options2: KingfisherOptionsInfo = [.processor(p2), .cacheSerializer(s), .waitForCache]\n            self.manager.cache.clearMemoryCache()\n            \n            self.manager.retrieveImage(with: source, options: options2) { result in\n                XCTAssertEqual(result.value?.cacheType, .disk)\n                XCTAssertTrue(p2.processed)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testWaitForCacheOnRetrieveImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        self.manager.retrieveImage(with: url) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .none)\n            \n            self.manager.cache.clearMemoryCache()\n            let cached = self.manager.cache.imageCachedType(forKey: url.cacheKey)\n            XCTAssertEqual(cached, .disk)\n            \n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testNotWaitForCacheOnRetrieveImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        self.manager.defaultOptions = .empty\n        self.manager.retrieveImage(with: url, options: [.callbackQueue(.untouch)]) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .none)\n            \n            // We are not waiting for cache finishing here. So only sync memory cache is done.\n            XCTAssertEqual(self.manager.cache.imageCachedType(forKey: url.cacheKey), .memory)\n            \n            // Clear the memory cache.\n            self.manager.cache.clearMemoryCache()\n            // After some time, the disk cache should be done.\n            delay(0.5) {\n                XCTAssertEqual(self.manager.cache.imageCachedType(forKey: url.cacheKey), .disk)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testWaitForCacheOnRetrieveImageWithProcessor() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        let p = RoundCornerImageProcessor(cornerRadius: 20)\n        self.manager.retrieveImage(with: url, options: [.processor(p)]) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .none)\n            \n            // `waitForCache` is enabled by default in test setup, so the processed image should already be cached to disk\n            // when the completion handler is called.\n            self.manager.cache.clearMemoryCache()\n            let cached = self.manager.cache.imageCachedType(forKey: url.cacheKey, processorIdentifier: p.identifier)\n            XCTAssertEqual(cached, .disk)\n\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 10, handler: nil)\n    }\n\n    func testImageShouldOnlyFromMemoryCacheOrRefreshCanBeGotFromMemory() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        manager.retrieveImage(with: url, options: [.fromMemoryCacheOrRefresh]) { result in\n            // Can be downloaded and cached normally.\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .none)\n            \n            // Can still be got from memory even when disk cache cleared.\n            self.manager.cache.clearDiskCache {\n                self.manager.retrieveImage(with: url, options: [.fromMemoryCacheOrRefresh]) { result in\n                    XCTAssertNotNil(result.value?.image)\n                    XCTAssertEqual(result.value!.cacheType, .memory)\n                    \n                    exp.fulfill()\n                }\n            }\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testImageShouldOnlyFromMemoryCacheOrRefreshCanRefreshIfNotInMemory() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        manager.retrieveImage(with: url, options: [.fromMemoryCacheOrRefresh]) { result in\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.cacheType, .none)\n            XCTAssertEqual(self.manager.cache.imageCachedType(forKey: url.cacheKey), .memory)\n\n            self.manager.cache.clearMemoryCache()\n            XCTAssertEqual(self.manager.cache.imageCachedType(forKey: url.cacheKey), .disk)\n            \n            // Should skip disk cache and download again.\n            self.manager.retrieveImage(with: url, options: [.fromMemoryCacheOrRefresh]) { result in\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value!.cacheType, .none)\n                XCTAssertEqual(self.manager.cache.imageCachedType(forKey: url.cacheKey), .memory)\n\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n\n    func testShouldDownloadAndCacheProcessedImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let size = CGSize(width: 1, height: 1)\n        let processor = ResizingImageProcessor(referenceSize: size)\n\n        manager.retrieveImage(with: url, options: [.processor(processor)]) { result in\n            // Can download and cache normally\n            XCTAssertNotNil(result.value?.image)\n            XCTAssertEqual(result.value!.image.size, size)\n            XCTAssertEqual(result.value!.cacheType, .none)\n\n            self.manager.cache.clearMemoryCache()\n            let cached = self.manager.cache.imageCachedType(\n                forKey: url.cacheKey, processorIdentifier: processor.identifier)\n            XCTAssertEqual(cached, .disk)\n\n            self.manager.retrieveImage(with: url, options: [.processor(processor)]) { result in\n                XCTAssertNotNil(result.value?.image)\n                XCTAssertEqual(result.value!.image.size, size)\n                XCTAssertEqual(result.value!.cacheType, .disk)\n\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)\n    func testShouldApplyImageModifierWhenDownload() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        let modifierCalled = LockIsolated(false)\n        let modifier = AnyImageModifier { image in\n            modifierCalled.setValue(true)\n            return image.withRenderingMode(.alwaysTemplate)\n        }\n        manager.retrieveImage(with: url, options: [.imageModifier(modifier)]) { result in\n            XCTAssertEqual(result.value?.image.renderingMode, .alwaysTemplate)\n            XCTAssertTrue(modifierCalled.value)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testShouldApplyImageModifierWhenLoadFromMemoryCache() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        let modifierCalled = LockIsolated(false)\n        let modifier = AnyImageModifier { image in\n            modifierCalled.setValue(true)\n            return image.withRenderingMode(.alwaysTemplate)\n        }\n\n        manager.cache.store(testImage, forKey: url.cacheKey)\n        manager.retrieveImage(with: url, options: [.imageModifier(modifier)]) { result in\n            XCTAssertEqual(result.value?.cacheType, .memory)\n            XCTAssertEqual(result.value?.image.renderingMode, .alwaysTemplate)\n            XCTAssertTrue(modifierCalled.value)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testShouldApplyImageModifierWhenLoadFromDiskCache() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let modifierCalled = LockIsolated(false)\n        let modifier = AnyImageModifier { image in\n            modifierCalled.setValue(true)\n            return image.withRenderingMode(.alwaysTemplate)\n        }\n\n        manager.cache.store(testImage, forKey: url.cacheKey) { _ in\n            self.manager.cache.clearMemoryCache()\n            self.manager.retrieveImage(with: url, options: [.imageModifier(modifier)]) { result in\n                XCTAssertEqual(result.value!.cacheType, .disk)\n                XCTAssertEqual(result.value!.image.renderingMode, .alwaysTemplate)\n                XCTAssertTrue(modifierCalled.value)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testImageModifierResultShouldNotBeCached() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let modifierCalled = LockIsolated(false)\n        let modifier = AnyImageModifier { image in\n            modifierCalled.setValue(true)\n            return image.withRenderingMode(.alwaysTemplate)\n        }\n        manager.retrieveImage(with: url, options: [.imageModifier(modifier)]) { result in\n            XCTAssertEqual(result.value?.image.renderingMode, .alwaysTemplate)\n\n            let memoryCached = self.manager.cache.retrieveImageInMemoryCache(forKey: url.absoluteString)\n            XCTAssertNotNil(memoryCached)\n            XCTAssertEqual(memoryCached?.renderingMode, .automatic)\n\n            self.manager.cache.retrieveImageInDiskCache(forKey: url.absoluteString) { result in\n                XCTAssertNotNil(result.value!)\n                XCTAssertEqual(result.value??.renderingMode, .automatic)\n                XCTAssertTrue(modifierCalled.value)\n                exp.fulfill()\n            }\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n#endif\n    \n    func testRetrieveWithImageProvider() {\n        let exp = expectation(description: #function)\n        let provider = SimpleImageDataProvider(cacheKey: \"key\") { .success(testImageData) }\n        manager.defaultOptions = .empty\n        _ = manager.retrieveImage(with: .provider(provider), options: [.processingQueue(.mainCurrentOrAsync)]) {\n            result in\n            XCTAssertNotNil(result.value)\n            XCTAssertTrue(result.value!.image.renderEqual(to: testImage))\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testRetrieveWithImageProviderFail() {\n        let exp = expectation(description: #function)\n        let provider = SimpleImageDataProvider(cacheKey: \"key\") { .failure(SimpleImageDataProvider.E()) }\n        _ = manager.retrieveImage(with: .provider(provider)) { result in\n            XCTAssertNotNil(result.error)\n            if case .imageSettingError(reason: .dataProviderError(_, let error)) = result.error! {\n                XCTAssertTrue(error is SimpleImageDataProvider.E)\n            } else {\n                XCTFail()\n            }\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testContextRemovingAlternativeSource() {\n        let allSources: [Source] = [\n            .network(URL(string: \"1\")!),\n            .network(URL(string: \"2\")!)\n        ]\n        let info = KingfisherParsedOptionsInfo([.alternativeSources(allSources)])\n        let context = RetrievingContext<Source>(\n            options: info, originalSource: .network(URL(string: \"0\")!))\n\n        let source1 = context.popAlternativeSource()\n        XCTAssertNotNil(source1)\n        guard case .network(let r1) = source1! else {\n            XCTFail(\"Should be a network source, but \\(source1!)\")\n            return\n        }\n        XCTAssertEqual(r1.downloadURL.absoluteString, \"1\")\n\n        let source2 = context.popAlternativeSource()\n        XCTAssertNotNil(source2)\n        guard case .network(let r2) = source2! else {\n            XCTFail(\"Should be a network source, but \\(source2!)\")\n            return\n        }\n        XCTAssertEqual(r2.downloadURL.absoluteString, \"2\")\n\n        XCTAssertNil(context.popAlternativeSource())\n    }\n\n    func testRetrievingWithAlternativeSource() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        _ = manager.retrieveImage(\n            with: .network(brokenURL),\n            options: [.alternativeSources([.network(url)])])\n        {\n            result in\n\n            XCTAssertNotNil(result.value)\n            XCTAssertEqual(result.value!.source.url, url)\n            XCTAssertEqual(result.value!.originalSource.url, brokenURL)\n\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testRetrievingErrorsWithAlternativeSource() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: Data())\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        let anotherBrokenURL = URL(string: \"anotherBrokenURL\")!\n        stub(anotherBrokenURL, data: Data())\n\n        _ = manager.retrieveImage(\n            with: .network(brokenURL),\n            options: [.alternativeSources([.network(anotherBrokenURL), .network(url)])])\n        {\n            result in\n\n            defer { exp.fulfill() }\n\n            XCTAssertNil(result.value)\n            XCTAssertNotNil(result.error)\n\n            guard case .imageSettingError(reason: let reason) = result.error! else {\n                XCTFail(\"The error should be image setting error\")\n                return\n            }\n\n            guard case .alternativeSourcesExhausted(let errorInfo) = reason else {\n                XCTFail(\"The error reason should be alternativeSourcesFailed\")\n                return\n            }\n\n            XCTAssertEqual(errorInfo.count, 3)\n            XCTAssertEqual(errorInfo[0].source.url, brokenURL)\n            XCTAssertEqual(errorInfo[1].source.url, anotherBrokenURL)\n            XCTAssertEqual(errorInfo[2].source.url, url)\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testRetrievingAlternativeSourceTaskUpdateBlockCalled() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        let downloadTaskUpdatedCount = LockIsolated(0)\n        let task = manager.retrieveImage(\n          with: .network(brokenURL),\n          options: [.alternativeSources([.network(url)])],\n          downloadTaskUpdated: { newTask in\n              downloadTaskUpdatedCount.withValue { $0 += 1 }\n              XCTAssertEqual(newTask?.sessionTask?.task.currentRequest?.url, url)\n          })\n        {\n            result in\n            XCTAssertEqual(downloadTaskUpdatedCount.value, 1)\n            exp.fulfill()\n        }\n\n        XCTAssertEqual(task?.sessionTask?.task.currentRequest?.url, brokenURL)\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testRetrievingAlternativeSourceCancelled() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        let task = manager.retrieveImage(\n            with: .network(brokenURL),\n            options: [.alternativeSources([.network(url)])]\n        )\n        {\n            result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            exp.fulfill()\n        }\n        task?.cancel()\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testRetrievingAlternativeSourceCanCancelUpdatedTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let dataStub = delayedStub(url, data: testImageData)\n        \n        let called = LockIsolated(false)\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        let task = manager.retrieveImage(\n            with: .network(brokenURL),\n            options: [.alternativeSources([.network(url)])],\n            downloadTaskUpdated: { newTask in\n                XCTAssertNotNil(newTask)\n                newTask?.cancel()\n                called.setValue(true)\n            }\n        )\n        {\n            result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error?.isTaskCancelled ?? false)\n\n            delay(0.3) {\n                _ = dataStub.go()\n                XCTAssertTrue(called.value)\n                exp.fulfill()\n            }\n        }\n        \n        XCTAssertNotNil(task)\n        XCTAssertTrue(task!.isInitialized)\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownsamplingHandleScale2x() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        _ = manager.retrieveImage(\n            with: .network(url),\n            options: [.processor(DownsamplingImageProcessor(size: .init(width: 4, height: 4))), .scaleFactor(2)])\n        {\n            result in\n\n            let image = result.value?.image\n            XCTAssertNotNil(image)\n            \n            #if os(macOS)\n            XCTAssertEqual(image?.size, .init(width: 8, height: 8))\n            XCTAssertEqual(image?.kf.scale, 1)\n            #else\n            XCTAssertEqual(image?.size, .init(width: 4, height: 4))\n            XCTAssertEqual(image?.kf.scale, 2)\n            #endif\n            \n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testDownsamplingHandleScale3x() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        _ = manager.retrieveImage(\n            with: .network(url),\n            options: [.processor(DownsamplingImageProcessor(size: .init(width: 4, height: 4))), .scaleFactor(3)])\n        {\n            result in\n\n            let image = result.value?.image\n            XCTAssertNotNil(image)\n            #if os(macOS)\n            XCTAssertEqual(image?.size, .init(width: 12, height: 12))\n            XCTAssertEqual(image?.kf.scale, 1)\n            #else\n            XCTAssertEqual(image?.size, .init(width: 4, height: 4))\n            XCTAssertEqual(image?.kf.scale, 3)\n            #endif\n            \n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testCacheCallbackCoordinatorStateChanging() {\n        var coordinator = CacheCallbackCoordinator(\n            shouldWaitForCache: false, shouldCacheOriginal: false)\n        var called = false\n        coordinator.apply(.cacheInitiated) {\n            called = true\n        }\n        XCTAssertTrue(called)\n        XCTAssertEqual(coordinator.state, .done)\n        coordinator.apply(.cachingImage) { XCTFail() }\n        XCTAssertEqual(coordinator.state, .done)\n\n        coordinator = CacheCallbackCoordinator(\n            shouldWaitForCache: true, shouldCacheOriginal: false)\n        called = false\n        coordinator.apply(.cacheInitiated) { XCTFail() }\n        XCTAssertEqual(coordinator.state, .idle)\n        coordinator.apply(.cachingImage) {\n            called = true\n        }\n        XCTAssertTrue(called)\n        XCTAssertEqual(coordinator.state, .done)\n\n        coordinator = CacheCallbackCoordinator(\n            shouldWaitForCache: false, shouldCacheOriginal: true)\n        coordinator.apply(.cacheInitiated) {\n            called = true\n        }\n        XCTAssertEqual(coordinator.state, .done)\n        coordinator.apply(.cachingOriginalImage) { XCTFail() }\n        XCTAssertEqual(coordinator.state, .done)\n\n        coordinator = CacheCallbackCoordinator(\n            shouldWaitForCache: true, shouldCacheOriginal: true)\n        coordinator.apply(.cacheInitiated) { XCTFail() }\n        XCTAssertEqual(coordinator.state, .idle)\n        coordinator.apply(.cachingOriginalImage) { XCTFail() }\n        XCTAssertEqual(coordinator.state, .originalImageCached)\n        coordinator.apply(.cachingImage) { called = true }\n        XCTAssertEqual(coordinator.state, .done)\n\n        coordinator = CacheCallbackCoordinator(\n            shouldWaitForCache: true, shouldCacheOriginal: true)\n        coordinator.apply(.cacheInitiated) { XCTFail() }\n        XCTAssertEqual(coordinator.state, .idle)\n        coordinator.apply(.cachingImage) { XCTFail() }\n        XCTAssertEqual(coordinator.state, .imageCached)\n        coordinator.apply(.cachingOriginalImage) { called = true }\n        XCTAssertEqual(coordinator.state, .done)\n    }\n    \n    func testCallbackClearAfterSuccess() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        \n        stub(url, data: testImageData)\n        \n        let task = LockIsolated<DownloadTask?>(nil)\n        let callbackCount = LockIsolated(0)\n        \n        let t: DownloadTask? = manager.retrieveImage(with: url) { result in\n            let count = callbackCount.withValue { value in\n                value += 1\n                return value\n            }\n\n            XCTAssertEqual(count, 1, \"Callback should not be invoked again.\")\n            XCTAssertNotNil(result.value?.image)\n\n            task.value?.cancel()\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {\n                exp.fulfill()\n            }\n        }\n\n        task.setValue(t)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testCanUseCustomizeDefaultCacheSerializer() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n\n        var cacheSerializer = DefaultCacheSerializer()\n        cacheSerializer.preferCacheOriginalData = true\n\n        manager.cache.store(\n            testImage,\n            original: testImageData,\n            forKey: url.cacheKey,\n            processorIdentifier: DefaultImageProcessor.default.identifier,\n            cacheSerializer: cacheSerializer, toDisk: true) {\n                result in\n\n                let computedKey = url.cacheKey.computedKey(with: DefaultImageProcessor.default.identifier)\n                let fileURL = self.manager.cache.diskStorage.cacheFileURL(forKey: computedKey)\n                let data = try! Data(contentsOf: fileURL)\n                XCTAssertEqual(data, testImageData)\n\n                exp.fulfill()\n            }\n        waitForExpectations(timeout: 3.0)\n    }\n\n    func testCanUseCustomizeDefaultCacheSerializerStoreEncoded() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n\n        var cacheSerializer = DefaultCacheSerializer()\n        cacheSerializer.compressionQuality = 0.8\n\n        manager.cache.store(\n            testImage,\n            original: testImageJEPGData,\n            forKey: url.cacheKey,\n            processorIdentifier: DefaultImageProcessor.default.identifier,\n            cacheSerializer: cacheSerializer, toDisk: true) {\n                result in\n\n                let computedKey = url.cacheKey.computedKey(with: DefaultImageProcessor.default.identifier)\n                let fileURL = self.manager.cache.diskStorage.cacheFileURL(forKey: computedKey)\n                let data = try! Data(contentsOf: fileURL)\n                XCTAssertNotEqual(data, testImageJEPGData)\n                XCTAssertEqual(data, testImage.kf.jpegRepresentation(compressionQuality: 0.8))\n\n                exp.fulfill()\n            }\n        waitForExpectations(timeout: 3.0)\n    }\n    \n    func testImageResultContainsDataWhenDownloaded() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n        \n        manager.retrieveImage(with: url) { result in\n            XCTAssertNotNil(result.value?.data())\n            XCTAssertEqual(result.value!.data(), testImageData)\n            XCTAssertEqual(result.value!.cacheType, .none)\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testImageResultContainsDataWhenLoadFromMemoryCache() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        manager.retrieveImage(with: url) { _ in\n            self.manager.retrieveImage(with: url) { result in\n                XCTAssertEqual(result.value!.cacheType, .memory)\n                XCTAssertNotNil(result.value?.data())\n                XCTAssertEqual(\n                    result.value!.data(),\n                    DefaultCacheSerializer.default.data(with: result.value!.image, original: nil)\n                )\n                exp.fulfill()\n            }\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testImageResultContainsDataWhenLoadFromDiskCache() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData)\n\n        manager.retrieveImage(with: url) { _ in\n            self.manager.cache.clearMemoryCache()\n            self.manager.retrieveImage(with: url) { result in\n                XCTAssertEqual(result.value!.cacheType, .disk)\n                XCTAssertNotNil(result.value?.data())\n                XCTAssertEqual(\n                    result.value!.data(),\n                    DefaultCacheSerializer.default.data(with: result.value!.image, original: nil)\n                )\n                exp.fulfill()\n            }\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    // https://github.com/onevcat/Kingfisher/issues/1923\n    func testAnimatedImageShouldRecreateFromCache() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let data = testImageGIFData\n        stub(url, data: data)\n        let p = SimpleProcessor()\n        manager.retrieveImage(with: url, options: [.processor(p), .onlyLoadFirstFrame]) { result in\n            XCTAssertTrue(p.processed)\n            XCTAssertTrue(result.value!.image.creatingOptions!.onlyFirstFrame)\n            p.processed = false\n            self.manager.retrieveImage(with: url, options: [.processor(p)]) { result in\n                XCTAssertTrue(p.processed)\n                XCTAssertFalse(result.value!.image.creatingOptions!.onlyFirstFrame)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testAnimatedImageShouldNotRecreateWithSameOptions() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let data = testImageGIFData\n        stub(url, data: data)\n        let p = SimpleProcessor()\n        manager.retrieveImage(with: url, options: [.processor(p), .onlyLoadFirstFrame]) { result in\n            XCTAssertTrue(p.processed)\n            XCTAssertTrue(result.value!.image.creatingOptions!.onlyFirstFrame)\n            p.processed = false\n            self.manager.retrieveImage(with: url, options: [.processor(p), .onlyLoadFirstFrame]) { result in\n                XCTAssertFalse(p.processed)\n                XCTAssertTrue(result.value!.image.creatingOptions!.onlyFirstFrame)\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testMissingResourceOfLivePhotoFound() {\n        let resource = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        let source = LivePhotoSource(resources: [resource])\n        \n        let missing = manager.missingResources(source, options: .init(.empty))\n        XCTAssertEqual(missing.count, 1)\n    }\n    \n    func testMissingResourceOfLivePhotoNotFound() async throws {\n        let resource = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        \n        try await manager.cache.storeToDisk(\n            testImageData,\n            forKey: resource.cacheKey,\n            forcedExtension: resource.downloadURL.pathExtension\n        )\n        \n        let source = LivePhotoSource(resources: [resource])\n        let missing = manager.missingResources(source, options: .init(.empty))\n        XCTAssertEqual(missing.count, 0)\n    }\n    \n    func testMissingResourceOfLivePhotoFoundOne() async throws {\n        let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.heic)\n        let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        \n        try await manager.cache.storeToDisk(\n            testImageData,\n            forKey: resource1.cacheKey,\n            forcedExtension: resource1.downloadURL.pathExtension\n        )\n        \n        let source = LivePhotoSource(resources: [resource1, resource2])\n        let missing = manager.missingResources(source, options: .init(.empty))\n        XCTAssertEqual(missing.count, 1)\n        XCTAssertEqual(missing[0].downloadURL, resource2.downloadURL)\n    }\n    \n    func testMissingResourceOfLivePhotoForceRefresh() async throws {\n        let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.heic)\n        let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        \n        try await manager.cache.storeToDisk(\n            testImageData,\n            forKey: resource1.cacheKey,\n            forcedExtension: resource1.downloadURL.pathExtension\n        )\n        \n        let source = LivePhotoSource(resources: [resource1, resource2])\n        let missing = manager.missingResources(source, options: .init([.forceRefresh]))\n        XCTAssertEqual(missing.count, 2)\n        XCTAssertEqual(missing[0].downloadURL, resource1.downloadURL)\n        XCTAssertEqual(missing[1].downloadURL, resource2.downloadURL)\n    }\n    \n    func testDownloadAndCacheLivePhotoResourcesAll() async throws {\n        let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic)\n        \n        stub(resource1.downloadURL, data: testImageData)\n        stub(resource2.downloadURL, data: testImageData)\n        \n        let result = try await manager.downloadAndCache(\n            resources: [resource1, resource2].map { LivePhotoResource.init(resource: $0)\n            },\n            options: .init(.empty))\n        XCTAssertEqual(result.count, 2)\n        \n        let urls = result.compactMap(\\.url)\n        XCTAssertTrue(urls.contains(LivePhotoURL.mov))\n        XCTAssertTrue(urls.contains(LivePhotoURL.heic))\n        \n        let resourceCached1 = manager.cache.imageCachedType(\n            forKey: resource1.cacheKey,\n            forcedExtension: resource1.downloadURL.pathExtension\n        )\n        let resourceCached2 = manager.cache.imageCachedType(\n            forKey: resource2.cacheKey,\n            forcedExtension: resource2.downloadURL.pathExtension\n        )\n        XCTAssertEqual(resourceCached1, .disk)\n        XCTAssertEqual(resourceCached2, .disk)\n    }\n    \n    func testRetrieveLivePhotoFromNetwork() async throws {\n        let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic)\n        \n        stub(resource1.downloadURL, data: testImageData)\n        stub(resource2.downloadURL, data: testImageData)\n        \n        let resource1Cached = manager.cache.isCached(\n            forKey: resource1.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier\n        )\n        let resource2Cached = manager.cache.isCached(\n            forKey: resource2.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier\n        )\n        XCTAssertFalse(resource1Cached)\n        XCTAssertFalse(resource2Cached)\n        \n        let source = LivePhotoSource(resources: [resource1, resource2])\n        let result = try await manager.retrieveLivePhoto(with: source)\n        XCTAssertEqual(result.fileURLs.count, 2)\n        result.fileURLs.forEach { url in\n            XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))\n        }\n        XCTAssertEqual(result.cacheType, .none)\n        XCTAssertEqual(result.data(), [testImageData, testImageData])\n        let urlsInSource = result.source.resources.map(\\.downloadURL)\n        XCTAssertTrue(urlsInSource.contains(LivePhotoURL.mov))\n        XCTAssertTrue(urlsInSource.contains(LivePhotoURL.heic))\n    }\n    \n    func testRetrieveLivePhotoFromLocal() async throws {\n        let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic)\n        \n        try await manager.cache.storeToDisk(\n            testImageData,\n            forKey: resource1.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource1.downloadURL.pathExtension\n        )\n        try await manager.cache.storeToDisk(\n            testImageData,\n            forKey: resource2.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource2.downloadURL.pathExtension\n        )\n        \n        let resource1Cached = manager.cache.isCached(\n            forKey: resource1.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource1.downloadURL.pathExtension\n        )\n        let resource2Cached = manager.cache.isCached(\n            forKey: resource2.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource2.downloadURL.pathExtension\n        )\n        XCTAssertTrue(resource1Cached)\n        XCTAssertTrue(resource2Cached)\n        \n        let source = LivePhotoSource(resources: [resource1, resource2])\n        let result = try await manager.retrieveLivePhoto(with: source)\n        XCTAssertEqual(result.fileURLs.count, 2)\n        result.fileURLs.forEach { url in\n            XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))\n        }\n        XCTAssertEqual(result.cacheType, .disk)\n        XCTAssertEqual(result.data(), [])\n        let urlsInSource = result.source.resources.map(\\.downloadURL)\n        XCTAssertTrue(urlsInSource.contains(LivePhotoURL.mov))\n        XCTAssertTrue(urlsInSource.contains(LivePhotoURL.heic))\n    }\n    \n    func testRetrieveLivePhotoMixed() async throws {\n        let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic)\n        \n        try await manager.cache.storeToDisk(\n            testImageData,\n            forKey: resource1.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource1.downloadURL.pathExtension\n        )\n        stub(resource2.downloadURL, data: testImageData)\n        \n        let resource1Cached = manager.cache.isCached(\n            forKey: resource1.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource1.downloadURL.pathExtension\n        )\n        let resource2Cached = manager.cache.isCached(\n            forKey: resource2.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource2.downloadURL.pathExtension\n        )\n        XCTAssertTrue(resource1Cached)\n        XCTAssertFalse(resource2Cached)\n        \n        let source = LivePhotoSource(resources: [resource1, resource2])\n        let result = try await manager.retrieveLivePhoto(with: source)\n        XCTAssertEqual(result.fileURLs.count, 2)\n        result.fileURLs.forEach { url in\n            XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))\n        }\n        XCTAssertEqual(result.cacheType, .none)\n        XCTAssertEqual(result.data(), [testImageData])\n        let urlsInSource = result.source.resources.map(\\.downloadURL)\n        XCTAssertTrue(urlsInSource.contains(LivePhotoURL.mov))\n        XCTAssertTrue(urlsInSource.contains(LivePhotoURL.heic))\n    }\n    \n    func testRetrieveLivePhotoNetworkThenCache() async throws {\n        let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov)\n        let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic)\n        \n        stub(resource1.downloadURL, data: testImageData)\n        stub(resource2.downloadURL, data: testImageData)\n        \n        let resource1Cached = manager.cache.isCached(\n            forKey: resource1.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource1.downloadURL.pathExtension\n        )\n        let resource2Cached = manager.cache.isCached(\n            forKey: resource2.cacheKey,\n            processorIdentifier: LivePhotoImageProcessor.default.identifier,\n            forcedExtension: resource2.downloadURL.pathExtension\n        )\n        XCTAssertFalse(resource1Cached)\n        XCTAssertFalse(resource2Cached)\n        \n        let source = LivePhotoSource(resources: [resource1, resource2])\n        let result = try await manager.retrieveLivePhoto(with: source)\n        XCTAssertEqual(result.fileURLs.count, 2)\n        result.fileURLs.forEach { url in\n            XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))\n        }\n        XCTAssertEqual(result.cacheType, .none)\n        XCTAssertEqual(result.data(), [testImageData, testImageData])\n        let urlsInSource = result.source.resources.map(\\.downloadURL)\n        XCTAssertTrue(urlsInSource.contains(LivePhotoURL.mov))\n        XCTAssertTrue(urlsInSource.contains(LivePhotoURL.heic))\n        \n        let localResult = try await manager.retrieveLivePhoto(with: source)\n        XCTAssertEqual(localResult.fileURLs.count, 2)\n        XCTAssertEqual(localResult.cacheType, .disk)\n    }\n    \n    func testDownloadAndCacheLivePhotoWithEmptyResources() async throws {\n        let result = try await manager.downloadAndCache(resources: [], options: .init([]))\n        XCTAssertTrue(result.isEmpty)\n    }\n    \n    func testDownloadAndCacheLivePhotoWithSingleResource() async throws {\n        let resource = LivePhotoResource(downloadURL: LivePhotoURL.heic)\n        stub(resource.downloadURL!, data: testImageData)\n        \n        let result = try await manager.downloadAndCache(resources: [resource], options: .init([]))\n        XCTAssertEqual(result.count, 1)\n        \n        let t = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: \"heic\")\n        XCTAssertEqual(t, .disk)\n    }\n    \n    func testDownloadAndCacheLivePhotoWithSingleResourceGuessingUnsupportedExtension() async throws {\n        let resource = LivePhotoResource(downloadURL: URL(string: \"https://example.com\")!)\n        stub(resource.downloadURL!, data: testImageData)\n        \n        XCTAssertEqual(resource.referenceFileType, .other(\"\"))\n        \n        let result = try await manager.downloadAndCache(resources: [resource], options: .init([]))\n        XCTAssertEqual(result.count, 1)\n        \n        var cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: \"heic\")\n        XCTAssertEqual(cacheType, .none)\n        \n        cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey)\n        XCTAssertEqual(cacheType, .disk)\n    }\n    \n    func testDownloadAndCacheLivePhotoWithSingleResourceExplicitSetExtension() async throws {\n        let resource = LivePhotoResource(downloadURL: URL(string: \"https://example.com\")!, fileType: .heic)\n        stub(resource.downloadURL!, data: testImageData)\n        \n        XCTAssertEqual(resource.referenceFileType, .heic)\n        \n        let result = try await manager.downloadAndCache(resources: [resource], options: .init([]))\n        XCTAssertEqual(result.count, 1)\n        \n        var cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: \"heic\")\n        XCTAssertEqual(cacheType, .disk)\n        \n        cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey)\n        XCTAssertEqual(cacheType, .none)\n    }\n    \n    func testDownloadAndCacheLivePhotoWithSingleResourceGuessingHEICExtension() async throws {\n        let resource = LivePhotoResource(downloadURL: URL(string: \"https://example.com\")!)\n        stub(resource.downloadURL!, data: partitalHEICData)\n        \n        XCTAssertEqual(resource.referenceFileType, .other(\"\"))\n        \n        let result = try await manager.downloadAndCache(resources: [resource], options: .init([]))\n        XCTAssertEqual(result.count, 1)\n        \n        var cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: \"heic\")\n        XCTAssertEqual(cacheType, .disk)\n        \n        cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey)\n        XCTAssertEqual(cacheType, .none)\n    }\n    \n    func testDownloadAndCacheLivePhotoWithSingleResourceGuessingMOVExtension() async throws {\n        let resource = LivePhotoResource(downloadURL: URL(string: \"https://example.com\")!)\n        stub(resource.downloadURL!, data: partitalMOVData)\n        \n        XCTAssertEqual(resource.referenceFileType, .other(\"\"))\n        \n        let result = try await manager.downloadAndCache(resources: [resource], options: .init([]))\n        XCTAssertEqual(result.count, 1)\n        \n        var cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: \"mov\")\n        XCTAssertEqual(cacheType, .disk)\n        \n        cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey)\n        XCTAssertEqual(cacheType, .none)\n    }\n}\n\nprivate var imageCreatingOptionsKey: Void?\n\nextension KFCrossPlatformImage {\n    var creatingOptions: ImageCreatingOptions? {\n        get { return getAssociatedObject(self, &imageCreatingOptionsKey) }\n        set { setRetainedAssociatedObject(self, &imageCreatingOptionsKey, newValue) }\n    }\n}\n\nfinal class SimpleProcessor: ImageProcessor, @unchecked Sendable {\n    public let identifier = \"id\"\n    var processed = false\n    /// Initialize a `DefaultImageProcessor`\n    public init() {}\n    \n    /// Process an input `ImageProcessItem` item to an image for this processor.\n    ///\n    /// - parameter item:    Input item which will be processed by `self`\n    /// - parameter options: Options when processing the item.\n    ///\n    /// - returns: The processed image.\n    ///\n    /// - Note: See documentation of `ImageProcessor` protocol for more.\n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        processed = true\n        switch item {\n        case .image(let image):\n            return image\n        case .data(let data):\n            let creatingOptions = options.imageCreatingOptions\n            let image = KingfisherWrapper<KFCrossPlatformImage>.image(data: data, options: creatingOptions)\n            image?.creatingOptions = creatingOptions\n            return image\n        }\n    }\n}\n\nfinal class FailingProcessor: ImageProcessor, @unchecked Sendable {\n    public let identifier = \"FailingProcessor\"\n    var processed = false\n    public init() {}\n    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        processed = true\n        return nil\n    }\n}\n\nstruct SimpleImageDataProvider: ImageDataProvider, @unchecked Sendable {\n    let cacheKey: String\n    let provider: () -> (Result<Data, any Error>)\n    \n    func data(handler: @escaping (Result<Data, any Error>) -> Void) {\n        handler(provider())\n    }\n    \n    struct E: Error {}\n}\n\nactor ActorArray<Element> {\n    var value: [Element]\n    init(_ value: [Element]) {\n        self.value = value\n    }\n    \n    func append(_ newElement: Element) {\n        value.append(newElement)\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/KingfisherOptionsInfoTests.swift",
    "content": "//\n//  KingfisherOptionsInfoTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 16/1/4.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n\nimport XCTest\n@testable import Kingfisher\n\nclass KingfisherOptionsInfoTests: XCTestCase {\n\n    func testEmptyOptionsShouldParseCorrectly() {\n        let options = KingfisherParsedOptionsInfo(KingfisherOptionsInfo.empty)\n        XCTAssertTrue(options.targetCache === nil)\n        XCTAssertTrue(options.downloader === nil)\n\n#if os(iOS) || os(tvOS) || os(visionOS)\n        switch options.transition {\n        case .none: break\n        default: XCTFail(\"The transition for empty option should be .None. But \\(options.transition)\")\n        }\n#endif\n        \n        XCTAssertEqual(options.downloadPriority, URLSessionTask.defaultPriority)\n        XCTAssertFalse(options.forceRefresh)\n        XCTAssertFalse(options.fromMemoryCacheOrRefresh)\n        XCTAssertFalse(options.cacheMemoryOnly)\n        XCTAssertFalse(options.backgroundDecode)\n        XCTAssertEqual(options.callbackQueue.queue.label, DispatchQueue.main.label)\n        XCTAssertEqual(options.scaleFactor, 1.0)\n        XCTAssertFalse(options.keepCurrentImageWhileLoading)\n        XCTAssertFalse(options.onlyLoadFirstFrame)\n        XCTAssertFalse(options.cacheOriginalImage)\n        XCTAssertEqual(options.diskStoreWriteOptions, [])\n    }\n    \n    func testSetOptionsShouldParseCorrectly() {\n        let cache = ImageCache(name: \"com.onevcat.Kingfisher.KingfisherOptionsInfoTests\")\n        let downloader = ImageDownloader(name: \"com.onevcat.Kingfisher.KingfisherOptionsInfoTests\")\n        \n        let queue = DispatchQueue.global(qos: .default)\n        let testModifier = TestModifier()\n        let testRedirectHandler = TestRedirectHandler()\n        let processor = RoundCornerImageProcessor(cornerRadius: 20)\n        let serializer = FormatIndicatedCacheSerializer.png\n        let modifier = AnyImageModifier { i in return i }\n        let alternativeSource = Source.network(URL(string: \"https://onevcat.com\")!)\n\n        var options = KingfisherParsedOptionsInfo([\n            .targetCache(cache),\n            .downloader(downloader),\n            .originalCache(cache),\n            .downloadPriority(0.8),\n            .forceRefresh,\n            .forceTransition,\n            .fromMemoryCacheOrRefresh,\n            .cacheMemoryOnly,\n            .waitForCache,\n            .onlyFromCache,\n            .backgroundDecode,\n            .callbackQueue(.dispatch(queue)),\n            .scaleFactor(2.0),\n            .preloadAllAnimationData,\n            .requestModifier(testModifier),\n            .redirectHandler(testRedirectHandler),\n            .processor(processor),\n            .cacheSerializer(serializer),\n            .imageModifier(modifier),\n            .keepCurrentImageWhileLoading,\n            .onlyLoadFirstFrame,\n            .cacheOriginalImage,\n            .diskStoreWriteOptions([.atomic]),\n            .alternativeSources([alternativeSource]),\n            .retryStrategy(DelayRetryStrategy(maxRetryCount: 10))\n        ])\n        \n        XCTAssertTrue(options.targetCache === cache)\n        XCTAssertTrue(options.originalCache === cache)\n        XCTAssertTrue(options.downloader === downloader)\n\n        #if os(iOS) || os(tvOS) || os(visionOS)\n        let transition = ImageTransition.fade(0.5)\n        options.transition = transition\n        switch options.transition {\n        case .fade(let duration): XCTAssertEqual(duration, 0.5)\n        default: XCTFail()\n        }\n        #endif\n        \n        XCTAssertEqual(options.downloadPriority, 0.8)\n        XCTAssertTrue(options.forceRefresh)\n        XCTAssertTrue(options.fromMemoryCacheOrRefresh)\n        XCTAssertTrue(options.forceTransition)\n        XCTAssertTrue(options.cacheMemoryOnly)\n        XCTAssertTrue(options.waitForCache)\n        XCTAssertTrue(options.onlyFromCache)\n        XCTAssertTrue(options.backgroundDecode)\n        \n        XCTAssertEqual(options.callbackQueue.queue.label, queue.label)\n        XCTAssertEqual(options.scaleFactor, 2.0)\n        XCTAssertTrue(options.preloadAllAnimationData)\n        XCTAssertTrue(options.requestModifier is TestModifier)\n        XCTAssertTrue(options.redirectHandler is TestRedirectHandler)\n        XCTAssertEqual(options.processor.identifier, processor.identifier)\n        XCTAssertTrue(options.cacheSerializer is FormatIndicatedCacheSerializer)\n        XCTAssertTrue(options.imageModifier is AnyImageModifier)\n        XCTAssertTrue(options.keepCurrentImageWhileLoading)\n        XCTAssertTrue(options.onlyLoadFirstFrame)\n        XCTAssertTrue(options.cacheOriginalImage)\n        XCTAssertEqual(options.diskStoreWriteOptions, [Data.WritingOptions.atomic])\n        XCTAssertEqual(options.alternativeSources?.count, 1)\n        XCTAssertEqual(options.alternativeSources?.first?.url, alternativeSource.url)\n\n        let retry = options.retryStrategy as? DelayRetryStrategy\n        XCTAssertNotNil(retry)\n        XCTAssertEqual(retry?.maxRetryCount, 10)\n    }\n    \n    func testOptionCouldBeOverwritten() {\n        var options = KingfisherParsedOptionsInfo([.downloadPriority(0.5), .onlyFromCache])\n        XCTAssertEqual(options.downloadPriority, 0.5)\n\n        options = KingfisherParsedOptionsInfo([.downloadPriority(0.5), .onlyFromCache, .downloadPriority(0.8)])\n        XCTAssertEqual(options.downloadPriority, 0.8)\n    }\n}\n\nfinal class TestModifier: ImageDownloadRequestModifier {\n    func modified(for request: URLRequest) -> URLRequest? {\n        return nil\n    }\n}\n\nfinal class TestRedirectHandler: ImageDownloadRedirectHandler {\n    func handleHTTPRedirection(\n        for task: Kingfisher.SessionDataTask, response: HTTPURLResponse, newRequest: URLRequest\n    ) async -> URLRequest? {\n        newRequest\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/KingfisherTestHelper.swift",
    "content": "//\n//  KingfisherTestHelper.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/10.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n@testable import Kingfisher\nimport CoreGraphics\n\nlet testImageString =\n    \"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAD8GlDQ1BJQ0MgUHJvZmlsZQAAOI2NVd1v21QUP4lvXKQWP6Cxjg4Vi69VU1u5GxqtxgZJk6XpQhq5zdgqpMl1bhpT1za2021V\" +\n    \"n/YCbwz4A4CyBx6QeEIaDMT2su0BtElTQRXVJKQ9dNpAaJP2gqpwrq9Tu13GuJGvfznndz7v0TVAx1ea45hJGWDe8l01n5GPn5iWO1YhCc9BJ/RAp6Z7TrpcLgIuxoVH1sNfIcHeNwfa6/9z\" +\n    \"dVappwMknkJsVz19HvFpgJSpO64PIN5G+fAp30Hc8TziHS4miFhheJbjLMMzHB8POFPqKGKWi6TXtSriJcT9MzH5bAzzHIK1I08t6hq6zHpRdu2aYdJYuk9Q/881bzZa8Xrx6fLmJo/iu4/V\" +\n    \"XnfH1BB/rmu5ScQvI77m+BkmfxXxvcZcJY14L0DymZp7pML5yTcW61PvIN6JuGr4halQvmjNlCa4bXJ5zj6qhpxrujeKPYMXEd+q00KR5yNAlWZzrF+Ie+uNsdC/MO4tTOZafhbroyXuR3Df\" +\n    \"08bLiHsQf+ja6gTPWVimZl7l/oUrjl8OcxDWLbNU5D6JRL2gxkDu16fGuC054OMhclsyXTOOFEL+kmMGs4i5kfNuQ62EnBuam8tzP+Q+tSqhz9SuqpZlvR1EfBiOJTSgYMMM7jpYsAEyqJCH\" +\n    \"DL4dcFFTAwNMlFDUUpQYiadhDmXteeWAw3HEmA2s15k1RmnP4RHuhBybdBOF7MfnICmSQ2SYjIBM3iRvkcMki9IRcnDTthyLz2Ld2fTzPjTQK+Mdg8y5nkZfFO+se9LQr3/09xZr+5GcaSuf\" +\n    \"eAfAww60mAPx+q8u/bAr8rFCLrx7s+vqEkw8qb+p26n11Aruq6m1iJH6PbWGv1VIY25mkNE8PkaQhxfLIF7DZXx80HD/A3l2jLclYs061xNpWCfoB6WHJTjbH0mV35Q/lRXlC+W8cndbl9t2\" +\n    \"SfhU+Fb4UfhO+F74GWThknBZ+Em4InwjXIyd1ePnY/Psg3pb1TJNu15TMKWMtFt6ScpKL0ivSMXIn9QtDUlj0h7U7N48t3i8eC0GnMC91dX2sTivgloDTgUVeEGHLTizbf5Da9JLhkhh29QO\" +\n    \"s1luMcScmBXTIIt7xRFxSBxnuJWfuAd1I7jntkyd/pgKaIwVr3MgmDo2q8x6IdB5QH162mcX7ajtnHGN2bov71OU1+U0fqqoXLD0wX5ZM005UHmySz3qLtDqILDvIL+iH6jB9y2x83ok898G\" +\n    \"OPQX3lk3Itl0A+BrD6D7tUjWh3fis58BXDigN9yF8M5PJH4B8Gr79/F/XRm8m241mw/wvur4BGDj42bzn+Vmc+NL9L8GcMn8F1kAcXgSteGGAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlU\" +\n    \"WHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9\" +\n    \"Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJo\" +\n    \"dHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8\" +\n    \"L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAAKZklEQVR4Ae2ax28VyxLGywYMJuecgwgSIILIgg1pQRRJQrBkxZr9/RNYAhJiA0gEIbIE6JEzIggQIKLJOefod351+fzmzps5njke3wV2\" +\n    \"SeM+Mx2qvq+qq3t6XNS1a9fyHz9+WE2V4poMHqcX11TPC3ctAWKippa1EVBTPS/cNT4C6oqJf7MsKiqKVVdeXh5bVx0V/woBcYCDYNVGpcAG2+hZlmW1EgAYrl+/ftnPnz+NTdenT5/s8+fP\" +\n    \"sRgaN25sDRo0sLp161pxcbFfkFBdRFQLAQIO6G/fvtmHDx8cwMCBA61Pnz7WqVMna9GihQG2fv36Tsj79+/t5cuXdu/ePbt165ZdunTJGjVqZKWlpVZSUuJEQGTWkjkBeA1D8fKXL1+sd+/e\" +\n    \"Nnr0aBs8eLADLqlfYqUNSq1evXru5Tp16nh0fP/+3cmiD6S9fv3azp07Z8eOHbNHjx45GZCFZBkNRR07dsws6wAe4wHfrVs3mzp1quH1Jk2aOHgig6iAIIU1pSJGIU9Ju48fPzoRZ86csT17\" +\n    \"9tiLFy98LEjLKhoyIwCjAY7hs2fPtgkTJljLli09xAHJ/BdYvAjooFAnUTvyAO2IiocPHzoJu3fv9unDtMiChEwIwCPM39yrtc2ZM8dGjBhRARxCkDBggc1XihTGpz+55MCBA7ZlyxYnhRyi\" +\n    \"8fONk6+uTi48/8rXoLI6jMM7Q4cOtUWLFnmJ5zBMniwEPHrpx4WnuVgdevToYW3atLGysjJ79eqVJ0kRVZmtUfVVIoCwx/NDhgxx8P369XMdGCvjo5SmfaaxGBdyO3fubK1bt7YbN24YqwfT\" +\n    \"oVASCt4KA565iTHz5s3zbC9PAVC/CzVMJNFfY/GMyCLqhg0bZnPnznXg1ENSIVIQASjDMLIyCa9///5+L7AYyPpNyPKb56qTkXoWLFVHqed4nHEYD9IRSGAZZXmdNWuWL5PoKUQKmgIoe/Dg\" +\n    \"gS1YsMCmTJnixikZUUeyun//vpcAYEODKC/wGzBctFeICzQlAKl7+/atPXnyxMnWpoh6xuKefMCe4erVq75EUpdGUhOAsWxa2rdvbwsXLrQOHTrY169fK7wDqPPnz9uyZcvs9u3bvi8ACBm7\" +\n    \"YcOGDgwD6cPFNNK5JBsdLtozt69fv24HDx609evX27p162zcuHEOGPDooWzatKkTcfToUS/TEpB6J4hxZH3mfW4T5WRoSuBtDGcri5AgV61a5XmC+dqzZ08n5N27d/bmzRvf6AAeb0MQ22MA\" +\n    \"IRcvXrQ1a9b4b8hG2BF2797dCWLeIwBmtzljxgzbsGGDL8Ui1BtU8icVAQDFY2T7AQMGeGiz+cEbGEI9Xn327Jk1a9bMM3aXLl382fLly313ePfu3bwmQQAXUwyCIZyxIQnSAAfRiKKRDRc7\" +\n    \"zu3bt3vClEPyKvpdmYoAFON9Eg/Gse1FWVgwGA+pxHhWC8jhRUigwv0YC4CENsQFsz/TjvHCQh/ad89FxsSJE23Hjh2poiDVKqCwwzj29ygOEkA9iYn3AKaC6kQEwBGBpH/w4jl9NL/DgNu2\" +\n    \"beuRgB6NTUk/6pgKaSUxARjPzmvs2LGeiGRsUCHGcImoYB2/ARQGlaZN+W/g6IgahxWBPKNpGR476j4VAbyd9erVy1cAPBclzF0yd7t27WKJiOoX9wygACZB/mf/fl8OiZCwYA/TC/ueP39e\" +\n    \"sSqF24Xv/3+kcIvf92K8VatWnvyCYUidVgBeVlgFmPfqEzNk4seMw9Q6fvy4nTp1ynNJMI8o6pgGEI9EkRSlMBEBKJDH2ZVFCW1Y3o4cOVJhRFS7Qp5BAICaN29up0+f9uQbBEg9F05gOUXi\" +\n    \"pmFYfyIC6AQBzDFePMICeHICyx8rA4JBWQo6EPYW7A6D46uOZ0QekikBKIAA2GWnFlTOb9Uz95Cgd/xBBn/Qg4dZDknGwSnI8NhAG+xjhQrXx5mQOAIYkHkntqMGDBITVV/VZ9Id5V3ppo2W\" +\n    \"2yT6EhOAV4mCKOUoQnHU9EhiRNI2gESPwjyqH/YpX0XVh58lIgDFsMrmJmr3p3q2pPyOIymsPM09wMkzgEcPDkEXQkk9gn3YGaz3ipg/iQigLwTw/q8kp/FQjAHMTyVJCJBBapdFiWeDJ8wa\" +\n    \"UzYAmhyB8DuJJGoFQM0rXobiwLFE8uGjOiKAMQHF+Ngi7wNSEUCE4H0kUwIYUKBZhjjwQEHQCJSzSvDOTpukBjB2ZSIPs/wNHz7cT4iIBtlEf/Q9ffrUL+6TOiFRBDCgwo9DCt7LFRHUYciv\" +\n    \"n3+f2g4aNMhGjhzpn7iySopML3TOnDnTX3jC5GMD9tCGXSg7wmohgJ3YyZMnPReEw7Co+O+TIrbKHH5wZlCWO7qGBAyGpKDHMDpO1BYdJD3OEIisadOm+TacaIsai43YnTt3fNucOQEYq7Dm\" +\n    \"hYdpECZBcxHwS5Ys8a9DkMBBBgYJWBxwnqsN7TlXBDznD4sXL/aXnTAwdGIHmzCO4JDg1PQHef6kPhNkp8Uc50SIUCPryhuUMkjv55wY40VI43CTJKr9epRdhDHvFETb5MmT/eB1/PjxfvYI\" +\n    \"eJGsvtxj05UrV2zt2rX+vSANAalOhDCAtzKUcQrL6yfzk+dBEuQl6iGCiJg+fbq/p1+4cMH27dvnXtNcpi9Ecmi6dOlSf6dnRYEEPoAgIlp6eAbQejn9bI05QxRxTJGkkjgJakCSIaD4SPn4\" +\n    \"8WP3bphxjOTSnoGTYyKGwwoyOUdjAq9xiRLqOHOgLaSRTwAu8GobLItz4U/i4ygMwrEvjaQmQCF38+ZNO3TokBuN8WESMAISeI5HAHbixAnbuXOnL2NhT0IIxu/du9cuX75ccVwuMsOgiDIS\" +\n    \"LEsfEQVJYVLDfaLuUxPAICiH7c2bN9vZs2cdYD7lqiNJ8ZEEwjRNZBT35AZyBO0Arn5qoxJSGYNEvD93SsTFQUha7zNeQQRgAAZi8NatW/0jZT6DUaRIIPzjhDEgAG/GCeOQ9WnDP05s27at\" +\n    \"IkHG9cn3vCACGBCP4U2WObLvtWvXXE94aYxSDtA4oS6qHuBcJF2mFP8+s2nTpsQ64/SlWgXCgxByZGmmATJ//nxPYMxNjMRgyT9A/e+xqv9RBvtRAdlMB8Bz4su54MaNG31DxkeUQkJfCguO\" +\n    \"AA2Acr4TkBRXrlzp/8HBcoTBzFMEQICghIicL9U9tlQf2rPOI+w/du3aZatXr/YPovo2ETtIgooqRYDGhwSM4U1sxYoV/lFz0qRJ/vWItV0AICXfksZ48jZRBHg8ztdfljrAE218dmNc2lZV\" +\n    \"MiFAhgOUHHD48GFPUKNGjbIxY8Z4ksKjgCdv8DtKeM4YJEKWN/YRZbkcw0kzGyjq+T6AjizAY0Mm/yQVBKO5DliAMB369u3r3wY5UAEYkRAlAKQ/SyxE4XXeB9gRQoz2G3EERo1Z2bPMCZBC\" +\n    \"QGIonoIMkqIiRG3iStrifaYB3hZhWQKX7symgAZUqRDFeIBzJQVAtlcCZbyk/aQ7TVltBMiIQo0vtJ/0Ji2jJ2PS3n9Au1oC/gAnVglCbQRUib4/oHNtBPwBTqwShGI2HTVZ/gvZ53KpZJXY\" +\n    \"DwAAAABJRU5ErkJggg==\"\n\nvar testImage = KFCrossPlatformImage(data: testImageData)!\nlet testImageData = Data(base64Encoded: testImageString)!\n\nlet partitalHEICData = Data(base64Encoded: \"AAAALGZ0eXBoZWljAAAAAG1pZjFNaUhCTWlIRU1pUHI=\")!\nlet partitalMOVData = Data(base64Encoded: \"AAAAFGZ0eXBxdCAgAAAAAHF0ICAAAAAId2lkZQAgJto=\")!\n\nlet testImagePNGData = testImage.kf.pngRepresentation()!\nlet testImageJEPGData = testImage.kf.jpegRepresentation(compressionQuality: 1.0)!\nlet testImageGIFData = Data(fileName: \"dancing-banana.gif\")\nlet testImageSingleFrameGIFData = Data(fileName: \"single-frame.gif\")\n\nlet testKeys = [\n    \"http://stackoverflow.com/questions/11251340/convert-image-to-base64-string-in-ios-swift\",\n    \"https://onevcat.com\",\n    \"http://onevcat.com/content/images/2014/May/200.jpg\",\n    \"http://onevcat.com/content/images/2014/May/200.jpg?fads#kj1asf\"\n]\n\nenum LivePhotoURL {\n    static let mov = URL(string: \"https://example.com/sample.mov\")!\n    static let heic = URL(string: \"https://example.com/sample.heic\")!\n}\n\nlet testURLs = testKeys.map { URL(string: $0)! }\n\nfunc cleanDefaultCache() {\n    let cache = KingfisherManager.shared.cache\n    cache.clearMemoryCache()\n    try? cache.diskStorage.removeAll()\n}\n\nfunc clearCaches(_ caches: [ImageCache]) {\n    for c in caches {\n        c.clearMemoryCache()\n        try? c.diskStorage.removeAll(skipCreatingDirectory: true)\n    }\n}\n\nfunc delay(_ time: Double, block: @escaping () -> Void) {\n    DispatchQueue.main.asyncAfter(deadline: .now() + time) { block() }\n}\n\nextension KFCrossPlatformImage {\n    func renderEqual(to image: KFCrossPlatformImage, withinTolerance tolerance: UInt8 = 3) -> Bool {\n        \n        guard size == image.size else { return false }\n        guard let imageData1 = kf.pngRepresentation(),\n              let imageData2 = image.kf.pngRepresentation() else\n        {\n            return false\n        }\n        guard let unifiedImage1 = KFCrossPlatformImage(data: imageData1),\n              let unifiedImage2 = KFCrossPlatformImage(data: imageData2) else\n        {\n            return false\n        }\n        \n        guard let rendered1 = unifiedImage1.rendered(),\n              let rendered2 = unifiedImage2.rendered() else\n        {\n            return false\n        }\n        guard let data1 = rendered1.kf.cgImage?.dataProvider?.data,\n              let data2 = rendered2.kf.cgImage?.dataProvider?.data else\n        {\n            return false\n        }\n        \n        let length1 = CFDataGetLength(data1)\n        let length2 = CFDataGetLength(data2)\n        guard length1 == length2 else { return false }\n        \n        let dataPtr1: UnsafePointer<UInt8> = CFDataGetBytePtr(data1)\n        let dataPtr2: UnsafePointer<UInt8> = CFDataGetBytePtr(data2)\n        \n        for index in 0..<length1 {\n            let byte1 = dataPtr1[index]\n            let byte2 = dataPtr2[index]\n            let delta = UInt8(abs(Int(byte1) - Int(byte2)))\n            \n            guard delta <= tolerance else {\n                return false\n            }\n        }\n        \n        return true\n    }\n    \n    func rendered() -> KFCrossPlatformImage? {\n        // Ignore non CG images\n        guard let cgImage = kf.cgImage else {\n            return nil\n        }\n        \n        var bitmapInfo = cgImage.bitmapInfo\n        let colorSpace = CGColorSpaceCreateDeviceRGB()\n        let alpha = (bitmapInfo.rawValue & CGBitmapInfo.alphaInfoMask.rawValue)\n        \n        let w = cgImage.width\n        let h = cgImage.height\n        \n        let size = CGSize(width: w, height: h)\n        \n        if alpha == CGImageAlphaInfo.none.rawValue {\n            bitmapInfo.remove(.alphaInfoMask)\n            bitmapInfo = CGBitmapInfo(rawValue: bitmapInfo.rawValue | CGImageAlphaInfo.noneSkipFirst.rawValue)\n        } else if !(alpha == CGImageAlphaInfo.noneSkipFirst.rawValue) ||\n                  !(alpha == CGImageAlphaInfo.noneSkipLast.rawValue)\n        {\n            bitmapInfo.remove(.alphaInfoMask)\n            bitmapInfo = CGBitmapInfo(rawValue: bitmapInfo.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)\n        }\n        \n        // Render the image\n        guard let context = CGContext(data: nil,\n                                      width: w,\n                                      height: h,\n                                      bitsPerComponent: cgImage.bitsPerComponent,\n                                      bytesPerRow: 0,\n                                      space: colorSpace,\n                                      bitmapInfo: bitmapInfo.rawValue) else\n        {\n            return nil\n        }\n        \n        context.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: size))\n        \n        #if os(macOS)\n        return context.makeImage().flatMap { KFCrossPlatformImage(cgImage: $0, size: kf.size) }\n        #else\n        return context.makeImage().flatMap { KFCrossPlatformImage(cgImage: $0) }\n        #endif\n    }\n}\n\n#if os(iOS) || os(tvOS) || os(visionOS)\nimport UIKit\nextension KFCrossPlatformImage {\n    static func from(color: KFCrossPlatformColor, size: CGSize) -> KFCrossPlatformImage {\n        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)\n        UIGraphicsBeginImageContext(rect.size)\n        let context = UIGraphicsGetCurrentContext()\n        context!.setFillColor(color.cgColor)\n        context!.fill(rect)\n        let img = UIGraphicsGetImageFromCurrentImageContext()\n        UIGraphicsEndImageContext()\n        return img!\n    }\n}\n#endif\n\nextension Data {\n    init(fileName: String) {\n        let comp = fileName.components(separatedBy: \".\")\n        guard comp.count == 2 else { fatalError() }\n        self.init(named: comp[0], type: comp[1])\n    }\n    \n    init(named name: String, type: String) {\n        guard let path = Bundle.test.path(forResource: name, ofType: type) else {\n            fatalError()\n        }\n        try! self.init(contentsOf: URL(fileURLWithPath: path))\n    }\n}\n\nextension Bundle {\n    static let test: Bundle = Bundle(for: ImageExtensionTests.self)\n}\n\n// Make tests happier with old Result type\nextension Result {\n    var value: Success? {\n        switch self {\n        case .success(let success): return success\n        case .failure: return nil\n        }\n    }\n\n    var error: Failure? {\n        switch self {\n        case .success: return nil\n        case .failure(let failure): return failure\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/KingfisherTests-Bridging-Header.h",
    "content": "//\n//  Use this file to import your target's public headers that you would like to expose to Swift.\n//\n\n#import \"Nocilla.h\""
  },
  {
    "path": "Tests/KingfisherTests/LivePhotoSourceTests.swift",
    "content": "//\n//  LivePhotoSourceTests.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2024/10/01.\n//\n//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass LivePhotoSourceTests: XCTestCase {\n    \n    func testLivePhotoResourceInitialization() {\n        let url = URL(string: \"https://example.com/photo.heic\")!\n        let resource = LivePhotoResource(downloadURL: url)\n        \n        XCTAssertEqual(resource.downloadURL, url)\n        XCTAssertEqual(resource.referenceFileType, .heic)\n    }\n    \n    func testLivePhotoResourceInitializationWithResource() {\n        let url = URL(string: \"https://example.com/photo.mov\")!\n        let imageResource = KF.ImageResource(downloadURL: url)\n        let resource = LivePhotoResource(resource: imageResource)\n        \n        XCTAssertEqual(resource.downloadURL, url)\n        XCTAssertEqual(resource.referenceFileType, .mov)\n    }\n    \n    func testLivePhotoResourceFileExtensionByType() {\n        let mov = LivePhotoResource.FileType.mov\n        XCTAssertEqual(mov.determinedFileExtension(Data()), \"mov\")\n        XCTAssertEqual(mov.fileExtension, \"mov\")\n        \n        let heic = LivePhotoResource.FileType.heic\n        XCTAssertEqual(heic.determinedFileExtension(Data()), \"heic\")\n        XCTAssertEqual(heic.fileExtension, \"heic\")\n        \n        let other = LivePhotoResource.FileType.other(\"exe\")\n        XCTAssertEqual(other.fileExtension, \"exe\")\n    }\n    \n    func testLivePhotoResourceFileTypeDeterminationForHEIC() {\n        let data = Data([0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63])\n        let fileType = LivePhotoResource.FileType.other(\"\")\n        let determinedExtension = fileType.determinedFileExtension(data)\n        \n        XCTAssertEqual(determinedExtension, \"heic\")\n    }\n\n    func testLivePhotoResourceFileTypeDeterminationForQT() {\n        let data = Data([0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20])\n        let fileType = LivePhotoResource.FileType.other(\"\")\n        let determinedExtension = fileType.determinedFileExtension(data)\n        \n        XCTAssertEqual(determinedExtension, \"mov\")\n    }\n\n    func testLivePhotoResourceFileTypeDeterminationForExplicitFileType() {\n        let data = Data([0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20])\n        let fileType = LivePhotoResource.FileType.other(\"ext\")\n        let determinedExtension = fileType.determinedFileExtension(data)\n        \n        XCTAssertEqual(determinedExtension, \"ext\")\n    }\n\n    func testLivePhotoResourceFileTypeDeterminationForUnknown() {\n        let data = Data([0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x22])\n        let fileType = LivePhotoResource.FileType.other(\"\")\n        let determinedExtension = fileType.determinedFileExtension(data)\n        \n        XCTAssertEqual(determinedExtension, nil)\n    }\n    \n    func testLivePhotoResourceFileTypeDeterminationForNonFYTP() {\n        let data = Data([0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x71, 0x74, 0x20, 0x20])\n        let fileType = LivePhotoResource.FileType.other(\"\")\n        let determinedExtension = fileType.determinedFileExtension(data)\n        \n        XCTAssertEqual(determinedExtension, nil)\n    }\n    \n    func testLivePhotoResourceFileTypeDeterminationForNotEnoughData() {\n        let data = Data([0x00, 0x00, 0x00, 0x00])\n        let fileType = LivePhotoResource.FileType.other(\"\")\n        let determinedExtension = fileType.determinedFileExtension(data)\n        \n        XCTAssertEqual(determinedExtension, nil)\n    }\n    \n    func testLivePhotoSourceInitializationWithResources() {\n        let url1 = URL(string: \"https://example.com/photo1.heic\")!\n        let url2 = URL(string: \"https://example.com/photo2.mov\")!\n        let resources = [KF.ImageResource(downloadURL: url1), KF.ImageResource(downloadURL: url2)]\n        let livePhotoSource = LivePhotoSource(resources: resources)\n        \n        XCTAssertEqual(livePhotoSource.resources.count, 2)\n        XCTAssertEqual(livePhotoSource.resources[0].downloadURL, url1)\n        XCTAssertEqual(livePhotoSource.resources[1].downloadURL, url2)\n    }\n    \n    func testLivePhotoSourceInitializationWithURLs() {\n        let url1 = URL(string: \"https://example.com/photo1.heic\")!\n        let url2 = URL(string: \"https://example.com/photo2.mov\")!\n        let livePhotoSource = LivePhotoSource(urls: [url1, url2])\n        \n        XCTAssertEqual(livePhotoSource.resources.count, 2)\n        XCTAssertEqual(livePhotoSource.resources[0].downloadURL, url1)\n        XCTAssertEqual(livePhotoSource.resources[1].downloadURL, url2)\n    }\n    \n    func testLivePhotoResourceInitializationWithCacheKey() {\n        let url = URL(string: \"https://example.com/photo.heic\")!\n        let cacheKey = \"customCacheKey\"\n        let resource = LivePhotoResource(downloadURL: url, cacheKey: cacheKey)\n        \n        XCTAssertEqual(resource.downloadURL, url)\n        XCTAssertEqual(resource.cacheKey, cacheKey)\n        XCTAssertEqual(resource.referenceFileType, .heic)\n    }\n\n    func testLivePhotoResourceInitializationWithFileType() {\n        let url = URL(string: \"https://example.com/photo.unknown\")!\n        let resource = LivePhotoResource(downloadURL: url, fileType: .other(\"unknown\"))\n        \n        XCTAssertEqual(resource.downloadURL, url)\n        XCTAssertEqual(resource.referenceFileType, .other(\"unknown\"))\n    }\n\n    func testLivePhotoResourceGuessedFileType() {\n        let url1 = URL(string: \"https://example.com/photo.heic\")!\n        let url2 = URL(string: \"https://example.com/photo.mov\")!\n        let url3 = URL(string: \"https://example.com/photo.unknown\")!\n        \n        let resource1 = KF.ImageResource(downloadURL: url1)\n        let resource2 = KF.ImageResource(downloadURL: url2)\n        let resource3 = KF.ImageResource(downloadURL: url3)\n        \n        XCTAssertEqual(resource1.guessedFileType, .heic)\n        XCTAssertEqual(resource2.guessedFileType, .mov)\n        XCTAssertEqual(resource3.guessedFileType, .other(\"unknown\"))\n    }\n\n    func testLivePhotoSourceInitializationWithMixedResources() {\n        let url1 = URL(string: \"https://example.com/photo1.heic\")!\n        let url2 = URL(string: \"https://example.com/photo2.mov\")!\n        let url3 = URL(string: \"https://example.com/photo3.unknown\")!\n        let resources = [\n            KF.ImageResource(downloadURL: url1),\n            KF.ImageResource(downloadURL: url2),\n            KF.ImageResource(downloadURL: url3)\n        ]\n        let livePhotoSource = LivePhotoSource(resources: resources)\n        \n        XCTAssertEqual(livePhotoSource.resources.count, 3)\n        XCTAssertEqual(livePhotoSource.resources[0].downloadURL, url1)\n        XCTAssertEqual(livePhotoSource.resources[1].downloadURL, url2)\n        XCTAssertEqual(livePhotoSource.resources[2].downloadURL, url3)\n        XCTAssertEqual(livePhotoSource.resources[0].referenceFileType, .heic)\n        XCTAssertEqual(livePhotoSource.resources[1].referenceFileType, .mov)\n        XCTAssertEqual(livePhotoSource.resources[2].referenceFileType, .other(\"unknown\"))\n    }\n\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/MemoryStorageTests.swift",
    "content": "//\n//  MemoryStorageTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/11/12.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nextension Int {\n    public var cacheCost: Int {\n        return 1\n    }\n}\n\n#if compiler(>=6)\nextension Int: @retroactive CacheCostCalculable { }\n#else\nextension Int: CacheCostCalculable { }\n#endif\n\nclass MemoryStorageTests: XCTestCase {\n\n    var storage: MemoryStorage.Backend<Int>!\n\n    override func setUp() {\n        super.setUp()\n        let config = MemoryStorage.Config(totalCostLimit: 3)\n        storage = MemoryStorage.Backend(config: config)\n    }\n\n    override func tearDown() {\n        storage = nil\n        super.tearDown()\n    }\n\n    func testConfigSettingStorage() {\n        XCTAssertEqual(storage.config.totalCostLimit, 3)\n        XCTAssertEqual(storage.storage.totalCostLimit, 3)\n        storage.config = MemoryStorage.Config(totalCostLimit: 10)\n        XCTAssertEqual(storage.config.totalCostLimit, 10)\n        XCTAssertEqual(storage.storage.totalCostLimit, 10)\n\n        storage.config.countLimit = 100\n        XCTAssertEqual(storage.config.countLimit, 100)\n        XCTAssertEqual(storage.storage.countLimit, 100)\n    }\n\n    func testStoreAndGetValue() {\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n\n        storage.store(value: 1, forKey: \"1\")\n\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n        XCTAssertEqual(storage.value(forKey: \"1\"), 1)\n    }\n\n    func testStoreValueOverwriting() {\n        storage.store(value: 1, forKey: \"1\")\n        XCTAssertEqual(storage.value(forKey: \"1\"), 1)\n\n        storage.store(value: 100, forKey: \"1\")\n        XCTAssertEqual(storage.value(forKey: \"1\"), 100)\n    }\n\n    func testRemoveValue() {\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        storage.store(value: 1, forKey: \"1\")\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n\n        storage.remove(forKey: \"1\")\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n    }\n\n    func testRemoveAllValues() {\n        storage.store(value: 1, forKey: \"1\")\n        storage.store(value: 2, forKey: \"2\")\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n        XCTAssertTrue(storage.isCached(forKey: \"2\"))\n\n        storage.removeAll()\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        XCTAssertFalse(storage.isCached(forKey: \"2\"))\n    }\n\n    func testStoreWithExpiration() {\n        let exp = expectation(description: #function)\n\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        storage.store(value: 1, forKey: \"1\", expiration: .seconds(0.1))\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n\n        XCTAssertFalse(storage.isCached(forKey: \"2\"))\n        storage.store(value: 2, forKey: \"2\")\n        XCTAssertTrue(storage.isCached(forKey: \"2\"))\n\n        delay(0.2) {\n            XCTAssertFalse(self.storage.isCached(forKey: \"1\"))\n            XCTAssertTrue(self.storage.isCached(forKey: \"2\"))\n\n            // But the object is still in underlying cache.\n            let obj = self.storage.storage.object(forKey: \"1\")\n            XCTAssertNotNil(obj)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testStoreWithConfigExpiration() {\n        let exp = expectation(description: #function)\n\n        storage.config.expiration = .seconds(0.1)\n\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        storage.store(value: 1, forKey: \"1\")\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n\n        delay(0.2) {\n            XCTAssertFalse(self.storage.isCached(forKey: \"1\"))\n            // But the object is still in underlying cache.\n            let obj = self.storage.storage.object(forKey: \"1\")\n            XCTAssertNotNil(obj)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testStoreWithExpirationExtending() {\n        let exp = expectation(description: #function)\n        \n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        storage.store(value: 1, forKey: \"1\", expiration: .seconds(1))\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n        \n        delay(0.1) {\n            let expirationDate1 = self.storage.storage.object(forKey: \"1\")?.estimatedExpiration\n            XCTAssertNotNil(expirationDate1)\n            \n            // Request for the object to extend it's expiration date\n            let obj = self.storage.value(forKey: \"1\", extendingExpiration: .expirationTime(.seconds(5)))\n            XCTAssertNotNil(obj)\n            \n            let expirationDate2 = self.storage.storage.object(forKey: \"1\")?.estimatedExpiration\n            XCTAssertNotNil(expirationDate2)\n            \n            XCTAssertNotEqual(expirationDate1!, expirationDate2!)\n            XCTAssert(expirationDate1!.isPast(referenceDate: expirationDate2!))\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    func testStoreWithExpirationNotExtending() {\n        let exp = expectation(description: #function)\n        \n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        storage.store(value: 1, forKey: \"1\", expiration: .seconds(1))\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n        \n        delay(0.1) {\n            let expirationDate1 = self.storage.storage.object(forKey: \"1\")?.estimatedExpiration\n            XCTAssertNotNil(expirationDate1)\n            \n            // Request for the object to extend it's expiration date\n            let obj = self.storage.value(forKey: \"1\", extendingExpiration: .none)\n            XCTAssertNotNil(obj)\n            \n            let expirationDate2 = self.storage.storage.object(forKey: \"1\")?.estimatedExpiration\n            XCTAssertNotNil(expirationDate2)\n            \n            XCTAssertEqual(expirationDate1, expirationDate2)\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testRemoveExpired() {\n        let exp = expectation(description: #function)\n\n        XCTAssertFalse(storage.isCached(forKey: \"1\"))\n        storage.store(value: 1, forKey: \"1\", expiration: .seconds(0.1))\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n\n        delay(0.2) {\n            XCTAssertFalse(self.storage.isCached(forKey: \"1\"))\n\n            // But the object is still in underlying cache.\n            XCTAssertNotNil(self.storage.storage.object(forKey: \"1\"))\n            self.storage.removeExpired()\n\n            // It should be removed now.\n            XCTAssertNil(self.storage.storage.object(forKey: \"1\"))\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testExtendExpirationByAccessing() {\n        let exp = expectation(description: #function)\n\n        let expiration = StorageExpiration.seconds(0.5)\n        storage.store(value: 1, forKey: \"1\", expiration: expiration)\n\n        delay(0.3) {\n            // This should extend the expiration to (0.3 + 0.5) from initially created.\n            let v = self.storage.value(forKey: \"1\")\n            XCTAssertEqual(v, 1)\n        }\n\n        delay(0.6) {\n            // Accessing `isCached` does not extend expiration\n            XCTAssertTrue(self.storage.isCached(forKey: \"1\"))\n        }\n        \n        delay(1) {\n            XCTAssertFalse(self.storage.isCached(forKey: \"1\"))\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testAutoCleanExpiredMemory() {\n        let exp = expectation(description: #function)\n        let config = MemoryStorage.Config(totalCostLimit: 3, cleanInterval: 0.1)\n        storage = MemoryStorage.Backend(config: config)\n\n        storage.store(value: 1, forKey: \"1\", expiration: .seconds(0.1))\n        XCTAssertTrue(storage.isCached(forKey: \"1\"))\n        XCTAssertEqual(self.storage.keys.count, 1)\n        \n        delay(0.2) {\n            XCTAssertFalse(self.storage.isCached(forKey: \"1\"))\n            XCTAssertNil(self.storage.storage.object(forKey: \"1\"))\n            XCTAssertEqual(self.storage.keys.count, 0)\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testStorageObject() {\n        let now = Date()\n        let obj = MemoryStorage.StorageObject(1, expiration: .seconds(1))\n        XCTAssertEqual(obj.value, 1)\n\n        XCTAssertEqual(\n            obj.estimatedExpiration.timeIntervalSince1970,\n            now.addingTimeInterval(1).timeIntervalSince1970,\n            accuracy: 0.3)\n\n        let exp = expectation(description: #function)\n        delay(0.5) {\n            obj.extendExpiration()\n            XCTAssertEqual(\n                obj.estimatedExpiration.timeIntervalSince1970,\n                now.addingTimeInterval(1.5).timeIntervalSince1970,\n                accuracy: 0.3)\n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/NSButtonExtensionTests.swift",
    "content": "//\n//  UIButtonExtensionTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/17.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(AppKit) && !targetEnvironment(macCatalyst)\nimport AppKit\nimport XCTest\n@testable import Kingfisher\n\nclass NSButtonExtensionTests: XCTestCase {\n\n    var button: NSButton!\n\n    override class func setUp() {\n        super.setUp()\n        LSNocilla.sharedInstance().start()\n    }\n\n    override class func tearDown() {\n        LSNocilla.sharedInstance().stop()\n        super.tearDown()\n    }\n\n    override func setUp() {\n        super.setUp()\n        \n        button = NSButton()\n        KingfisherManager.shared.downloader = ImageDownloader(name: \"testDownloader\")\n        KingfisherManager.shared.defaultOptions = [.waitForCache]\n        \n        cleanDefaultCache()\n    }\n\n    override func tearDown() {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n        LSNocilla.sharedInstance().clearStubs()\n        button = nil\n        cleanDefaultCache()\n        KingfisherManager.shared.defaultOptions = .empty\n        super.tearDown()\n    }\n\n    @MainActor\n    func testDownloadAndSetImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n        \n        var progressBlockIsCalled = false\n\n        button.kf.setImage(with: url, progressBlock: { _, _ in progressBlockIsCalled = true }) {\n            result in\n            XCTAssertTrue(progressBlockIsCalled)\n            \n            let image = result.value?.image\n            XCTAssertNotNil(image)\n            XCTAssertTrue(image!.renderEqual(to: testImage))\n            XCTAssertTrue(self.button.image!.renderEqual(to: testImage))\n            XCTAssertEqual(result.value!.cacheType, .none)\n            \n            exp.fulfill()\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @MainActor\n    func testDownloadAndSetAlternateImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n\n        var progressBlockIsCalled = false\n        button.kf.setAlternateImage(with: url, progressBlock: { _, _ in progressBlockIsCalled = true }) {\n            result in\n            XCTAssertTrue(progressBlockIsCalled)\n            \n            let image = result.value?.image\n            XCTAssertNotNil(image)\n            XCTAssertTrue(image!.renderEqual(to: testImage))\n            XCTAssertTrue(self.button.alternateImage!.renderEqual(to: testImage))\n            XCTAssertEqual(result.value!.cacheType, .none)\n            \n            exp.fulfill()\n\n        }\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    @MainActor\n    func testCancelImageTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n        \n        button.kf.setImage(with: url, completionHandler: { result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            delay(0.1) { exp.fulfill() }\n        })\n        \n        self.button.kf.cancelImageDownloadTask()\n        _ = stub.go()\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    @MainActor\n    func testCancelAlternateImageTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n        \n        button.kf.setAlternateImage(with: url, completionHandler: { result in\n            XCTAssertNotNil(result.error)\n            XCTAssertTrue(result.error!.isTaskCancelled)\n            delay(0.1) { exp.fulfill() }\n        })\n        \n        self.button.kf.cancelAlternateImageDownloadTask()\n        _ = stub.go()\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor\n    func testSettingNilURL() {\n        let exp = expectation(description: #function)\n        let url: URL? = nil\n        button.kf.setAlternateImage(with: url, progressBlock: { _, _ in XCTFail() }) {\n            result in\n            XCTAssertNil(result.value)\n            XCTAssertNotNil(result.error)\n            \n            guard case .imageSettingError(reason: .emptySource) = result.error! else {\n                XCTFail()\n                fatalError()\n            }\n            exp.fulfill()\n        }\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor\n    func testSettingNonWorkingImageWithFailureImage() {\n        let expectation = self.expectation(description: \"wait for downloading image\")\n        let url = testURLs[0]\n        stub(url, errorCode: 404)\n        \n        button.kf.setImage(with: url, options: [.onFailureImage(testImage)], completionHandler: { result in\n            XCTAssertNil(result.value)\n            expectation.fulfill()\n        })\n        \n        XCTAssertNil(button.image)\n        waitForExpectations(timeout: 5, handler: nil)\n        XCTAssertEqual(testImage, button.image)\n    }\n    \n    @MainActor\n    func testSettingNonWorkingAlternateImageWithFailureImage() {\n        let expectation = self.expectation(description: \"wait for downloading image\")\n        let url = testURLs[0]\n        stub(url, errorCode: 404)\n        \n        button.kf.setAlternateImage(with: url, options: [.onFailureImage(testImage)], completionHandler:  { result in\n            XCTAssertNil(result.value)\n            expectation.fulfill()\n        })\n        \n        XCTAssertNil(button.alternateImage)\n        waitForExpectations(timeout: 5, handler: nil)\n        XCTAssertEqual(testImage, button.alternateImage)\n    }\n\n}\n#endif\n"
  },
  {
    "path": "Tests/KingfisherTests/PixelFormatDecodingTests.swift",
    "content": "import Foundation\nimport XCTest\n@testable import Kingfisher\n\nfinal class PixelFormatDecodingTests: XCTestCase {\n    private struct Sample {\n        let fileName: String\n        let expectedBitsAfterDecoding: Int\n        let expectedColorSpaceName: String?\n    }\n    \n    private let samples: [Sample] = [\n        Sample(fileName: \"gradient-8b-srgb-opaque.png\", expectedBitsAfterDecoding: 8, expectedColorSpaceName: CGColorSpace.sRGB as String),\n        Sample(fileName: \"gradient-8b-srgb-alpha.png\", expectedBitsAfterDecoding: 8, expectedColorSpaceName: CGColorSpace.sRGB as String),\n        Sample(fileName: \"gradient-8b-displayp3-alpha.png\", expectedBitsAfterDecoding: 8, expectedColorSpaceName: CGColorSpace.displayP3 as String),\n        Sample(fileName: \"gradient-8b-gray.png\", expectedBitsAfterDecoding: 8, expectedColorSpaceName: CGColorSpace.genericGrayGamma2_2 as String),\n        Sample(fileName: \"gradient-10b-srgb-opaque.heic\", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.sRGB as String),\n        Sample(fileName: \"gradient-10b-srgb-alpha.heic\", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.sRGB as String),\n        Sample(fileName: \"gradient-10b-displayp3-alpha.heic\", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.displayP3 as String),\n        Sample(fileName: \"gradient-16b-srgb-alpha.png\", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.sRGB as String),\n        Sample(fileName: \"gradient-16b-gray.png\", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.genericGrayGamma2_2 as String)\n    ]\n    \n    func testDecodingSupportsVariousPixelFormats() {\n        for sample in samples {\n            let data = Data(fileName: sample.fileName)\n            let options = ImageCreatingOptions()\n            guard let image = KingfisherWrapper<KFCrossPlatformImage>.image(data: data, options: options) else {\n                XCTFail(\"Failed to construct image for \\(sample.fileName)\")\n                continue\n            }\n            let decoded = image.kf.decoded\n            guard let cgImage = decoded.kf.cgImage else {\n                XCTFail(\"Decoded image lost CGImage for \\(sample.fileName)\")\n                continue\n            }\n            #if os(macOS)\n            if sample.expectedBitsAfterDecoding > 8 {\n                XCTAssertNotIdentical(decoded, image, \"Decoding should redraw \\(sample.fileName)\")\n            }\n            XCTAssertEqual(cgImage.bitsPerComponent, sample.expectedBitsAfterDecoding, \"Unexpected bitsPerComponent for \\(sample.fileName)\")\n            if let expectedColorSpaceName = sample.expectedColorSpaceName {\n                XCTAssertEqual(cgImage.colorSpace?.name as String?, expectedColorSpaceName, \"Unexpected color space for \\(sample.fileName)\")\n            } else {\n                XCTFail(\"expectedColorSpaceName not existing, but needed for \\(sample.fileName)\")\n            }\n            #else\n            // On iOS/tvOS/visionOS, `decoded` may go through `preparingForDisplay`,\n            // which can keep 10-bit HEIC as 10 bpc or promote it to 16 bpc depending\n            // on the runtime display/decode pipeline.\n            if sample.fileName.contains(\"gradient-10b\") {\n                XCTAssertTrue(\n                    cgImage.bitsPerComponent == 10 || cgImage.bitsPerComponent == 16,\n                    \"Unexpected bitsPerComponent for \\(sample.fileName): \\(cgImage.bitsPerComponent)\"\n                )\n            } else {\n                XCTAssertEqual(\n                    cgImage.bitsPerComponent,\n                    sample.expectedBitsAfterDecoding,\n                    \"Unexpected bitsPerComponent for \\(sample.fileName)\"\n                )\n            }\n            #endif\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/RetryStrategyTests.swift",
    "content": "//\n//  RetryStrategyTests.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2020/05/06.\n//\n//  Copyright (c) 2020 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass RetryStrategyTests: XCTestCase {\n\n    var manager: KingfisherManager!\n\n    override class func setUp() {\n        super.setUp()\n        LSNocilla.sharedInstance().start()\n    }\n\n    override class func tearDown() {\n        LSNocilla.sharedInstance().stop()\n        super.tearDown()\n    }\n\n    override func setUpWithError() throws {\n        try super.setUpWithError()\n        let uuid = UUID()\n        let downloader = ImageDownloader(name: \"test.manager.\\(uuid.uuidString)\")\n        let cache = ImageCache(name: \"test.cache.\\(uuid.uuidString)\")\n\n        manager = KingfisherManager(downloader: downloader, cache: cache)\n        manager.defaultOptions = [.waitForCache]\n    }\n\n    override func tearDownWithError() throws {\n        LSNocilla.sharedInstance().clearStubs()\n        clearCaches([manager.cache])\n        cleanDefaultCache()\n        manager = nil\n        try super.tearDownWithError()\n    }\n\n    func testCanCreateRetryStrategy() {\n        let strategy = DelayRetryStrategy(maxRetryCount: 10, retryInterval: .seconds(5))\n        XCTAssertEqual(strategy.maxRetryCount, 10)\n        XCTAssertEqual(strategy.retryInterval.timeInterval(for: 0), 5)\n    }\n\n\n    func testDelayRetryIntervalCalculating() {\n        let secondInternal = DelayRetryStrategy.Interval.seconds(10)\n        XCTAssertEqual(secondInternal.timeInterval(for: 0), 10)\n\n        let accumulatedInternal = DelayRetryStrategy.Interval.accumulated(3)\n        XCTAssertEqual(accumulatedInternal.timeInterval(for: 0), 3)\n        XCTAssertEqual(accumulatedInternal.timeInterval(for: 1), 6)\n        XCTAssertEqual(accumulatedInternal.timeInterval(for: 2), 9)\n        XCTAssertEqual(accumulatedInternal.timeInterval(for: 3), 12)\n\n        let customInternal = DelayRetryStrategy.Interval.custom { TimeInterval($0 * 2) }\n        XCTAssertEqual(customInternal.timeInterval(for: 0), 0)\n        XCTAssertEqual(customInternal.timeInterval(for: 1), 2)\n        XCTAssertEqual(customInternal.timeInterval(for: 2), 4)\n        XCTAssertEqual(customInternal.timeInterval(for: 3), 6)\n    }\n\n    func testKingfisherManagerCanRetry() {\n        let exp = expectation(description: #function)\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        let retry = StubRetryStrategy()\n\n        _ = manager.retrieveImage(\n            with: .network(brokenURL),\n            options: [.retryStrategy(retry)],\n            completionHandler: { result in\n                XCTAssertEqual(retry.count, 3)\n                exp.fulfill()\n            }\n        )\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testImagePrefetcherCanRetry() {\n        let exp = expectation(description: #function)\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        let retry = StubRetryStrategy()\n        let progressCount = LockIsolated(0)\n        let prefetcher = ImagePrefetcher(\n            urls: [brokenURL],\n            options: [.retryStrategy(retry)],\n            progressBlock: { _, _, _ in\n                progressCount.withValue { $0 += 1 }\n            },\n            completionHandler: { skippedResources, failedResources, completedResources in\n                XCTAssertEqual(retry.count, 3)\n                XCTAssertEqual(progressCount.value, 1, \"Progress should be reported once per source, not per retry attempt.\")\n                XCTAssertEqual(skippedResources.count, 0)\n                XCTAssertEqual(failedResources.count, 1)\n                XCTAssertEqual(completedResources.count, 0)\n                exp.fulfill()\n            }\n        )\n        prefetcher.start()\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testImagePrefetcherRetryStrategyStopDoesNotRetry() {\n        let exp = expectation(description: #function)\n\n        let brokenURL = URL(string: \"brokenurl\")!\n        stub(brokenURL, data: Data())\n\n        let retry = ImmediateStopRetryStrategy()\n        let prefetcher = ImagePrefetcher(\n            urls: [brokenURL],\n            options: [.retryStrategy(retry)],\n            completionHandler: { skippedResources, failedResources, completedResources in\n                XCTAssertEqual(retry.count, 1)\n                XCTAssertEqual(skippedResources.count, 0)\n                XCTAssertEqual(failedResources.count, 1)\n                XCTAssertEqual(completedResources.count, 0)\n                exp.fulfill()\n            }\n        )\n\n        prefetcher.start()\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    // MARK: - DelayRetryStrategy Tests\n\n    func testDelayRetryStrategyExceededCount() {\n        let exp = expectation(description: #function)\n        let blockCalled: ActorArray<Bool> = ActorArray([])\n\n        let source = Source.network(URL(string: \"url\")!)\n        let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))\n\n        let group = DispatchGroup()\n\n        group.enter()\n        let context1 = RetryContext(\n            source: source,\n            error: .responseError(reason: .URLSessionError(error: E()))\n        )\n        retry.retry(context: context1) { decision in\n            guard case RetryDecision.retry(let userInfo) = decision else {\n                XCTFail(\"The decision should be `retry`\")\n                return\n            }\n            XCTAssertNil(userInfo)\n            Task {\n                await blockCalled.append(true)\n                group.leave()\n            }\n        }\n\n        group.enter()\n        let context2 = RetryContext(\n            source: source,\n            error: .responseError(reason: .URLSessionError(error: E()))\n        )\n        context2.increaseRetryCount() // 1\n        context2.increaseRetryCount() // 2\n        context2.increaseRetryCount() // 3\n        retry.retry(context: context2) { decision in\n            guard case RetryDecision.stop = decision else {\n                XCTFail(\"The decision should be `stop`\")\n                return\n            }\n            Task {\n                await blockCalled.append(true)\n                group.leave()\n            }\n        }\n\n        group.notify(queue: .main) {\n            Task {\n                let result = await blockCalled.value\n                XCTAssertEqual(result.count, 2)\n                XCTAssertTrue(result.allSatisfy { $0 })\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testDelayRetryStrategyNotRetryForErrorReason() {\n        let exp = expectation(description: #function)\n        // Only non-user cancel error && response error should be retied.\n        let blockCalled: ActorArray<Bool> = ActorArray([])\n        let source = Source.network(URL(string: \"url\")!)\n        let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))\n\n        let task = URLSession.shared.dataTask(with: URL(string: \"url\")!)\n\n        let group = DispatchGroup()\n        group.enter()\n        let context1 = RetryContext(\n            source: source,\n            error: .requestError(reason: .taskCancelled(task: .init(task: task), token: .init()))\n        )\n        retry.retry(context: context1) { decision in\n            guard case RetryDecision.stop = decision else {\n                XCTFail(\"The decision should be `stop` if user cancelled the task.\")\n                return\n            }\n            Task {\n                await blockCalled.append(true)\n                group.leave()\n            }\n        }\n\n        group.enter()\n        let context2 = RetryContext(\n            source: source,\n            error: .cacheError(reason: .imageNotExisting(key: \"any_key\"))\n        )\n        retry.retry(context: context2) { decision in\n            guard case RetryDecision.stop = decision else {\n                XCTFail(\"The decision should be `stop` if the error type is not response error.\")\n                return\n            }\n            Task {\n                await blockCalled.append(true)\n                group.leave()\n            }\n        }\n\n        group.notify(queue: .main) {\n            Task {\n                let result = await blockCalled.value\n                XCTAssertEqual(result.count, 2)\n                XCTAssertTrue(result.allSatisfy { $0 })\n                exp.fulfill()\n            }\n        }\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    func testDelayRetryStrategyDidRetried() {\n        let exp = expectation(description: #function)\n        let source = Source.network(URL(string: \"url\")!)\n        let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))\n        let context = RetryContext(\n            source: source,\n            error: .responseError(reason: .URLSessionError(error: E()))\n        )\n        retry.retry(context: context) { decision in\n            guard case RetryDecision.retry = decision else {\n                XCTFail(\"The decision should be `retry`.\")\n                return\n            }\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n\n    // MARK: - NetworkRetryStrategy Tests\n\n    func testNetworkRetryStrategyRetriesImmediatelyWhenConnected() {\n        let exp = expectation(description: #function)\n        let source = Source.network(URL(string: \"url\")!)\n        let networkMonitor = TestNetworkMonitor(isConnected: true)\n        let retry = NetworkRetryStrategy(\n            timeoutInterval: 30,\n            networkMonitor: networkMonitor\n        )\n\n        let context = RetryContext(\n            source: source,\n            error: .responseError(reason: .URLSessionError(error: E()))\n        )\n\n        retry.retry(context: context) { decision in\n            guard case RetryDecision.retry(let userInfo) = decision else {\n                XCTFail(\"The decision should be `retry` when network is connected\")\n                return\n            }\n            XCTAssertNil(userInfo)\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    func testNetworkRetryStrategyStopsForTaskCancelled() {\n        let exp = expectation(description: #function)\n        let source = Source.network(URL(string: \"url\")!)\n        let networkMonitor = TestNetworkMonitor(isConnected: true)\n        let retry = NetworkRetryStrategy(\n            timeoutInterval: 30,\n            networkMonitor: networkMonitor\n        )\n\n        let task = URLSession.shared.dataTask(with: URL(string: \"url\")!)\n        let context = RetryContext(\n            source: source,\n            error: .requestError(reason: .taskCancelled(task: .init(task: task), token: .init()))\n        )\n\n        retry.retry(context: context) { decision in\n            guard case RetryDecision.stop = decision else {\n                XCTFail(\"The decision should be `stop` if user cancelled the task\")\n                return\n            }\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    func testNetworkRetryStrategyStopsForNonResponseError() {\n        let exp = expectation(description: #function)\n        let source = Source.network(URL(string: \"url\")!)\n        let networkMonitor = TestNetworkMonitor(isConnected: true)\n        let retry = NetworkRetryStrategy(\n            timeoutInterval: 30,\n            networkMonitor: networkMonitor\n        )\n\n        let context = RetryContext(\n            source: source,\n            error: .cacheError(reason: .imageNotExisting(key: \"any_key\"))\n        )\n\n        retry.retry(context: context) { decision in\n            guard case RetryDecision.stop = decision else {\n                XCTFail(\"The decision should be `stop` if the error type is not response error\")\n                return\n            }\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    func testNetworkRetryStrategyWithTimeout() {\n        let exp = expectation(description: #function)\n        let source = Source.network(URL(string: \"url\")!)\n        let networkMonitor = TestNetworkMonitor(isConnected: false)\n        let retry = NetworkRetryStrategy(timeoutInterval: 0.1, networkMonitor: networkMonitor)\n\n        let context = RetryContext(\n            source: source,\n            error: .responseError(reason: .URLSessionError(error: E()))\n        )\n\n        // Test timeout behavior when network is disconnected\n        retry.retry(context: context) { decision in\n            guard case RetryDecision.stop = decision else {\n                XCTFail(\"The decision should be `stop` after timeout\")\n                return\n            }\n            exp.fulfill()\n        }\n\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    func testNetworkRetryStrategyWaitsForReconnection() {\n        let exp = expectation(description: #function)\n        let source = Source.network(URL(string: \"url\")!)\n        let networkMonitor = TestNetworkMonitor(isConnected: false)\n        let retry = NetworkRetryStrategy(\n            timeoutInterval: 30,\n            networkMonitor: networkMonitor\n        )\n\n        let context = RetryContext(\n            source: source,\n            error: .responseError(reason: .URLSessionError(error: E()))\n        )\n\n        // Start retry when network is disconnected - should wait for reconnection\n        retry.retry(context: context) { decision in\n            guard case RetryDecision.retry(let userInfo) = decision else {\n                XCTFail(\"The decision should be `retry` when network reconnects\")\n                return\n            }\n            XCTAssertNotNil(userInfo) // Should contain the observer\n            exp.fulfill()\n        }\n\n        // Simulate network reconnection after a short delay\n        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {\n            networkMonitor.simulateNetworkChange(isConnected: true)\n        }\n\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    func testNetworkRetryStrategyCancelsPreviousObserver() {\n        let exp = expectation(description: #function)\n        let source = Source.network(URL(string: \"url\")!)\n        let networkMonitor = TestNetworkMonitor(isConnected: false)\n        let retry = NetworkRetryStrategy(\n            timeoutInterval: 30,\n            networkMonitor: networkMonitor\n        )\n\n        let context = RetryContext(\n            source: source,\n            error: .responseError(reason: .URLSessionError(error: E()))\n        )\n\n        // First retry attempt - should create an observer\n        retry.retry(context: context) { decision in\n            // This should not be called since network is disconnected initially\n            XCTFail(\"First callback should not be called immediately when network is disconnected\")\n        }\n\n        // Second retry attempt - should cancel previous observer\n        retry.retry(context: context) { decision in\n            guard case RetryDecision.retry(let userInfo) = decision else {\n                XCTFail(\"The second decision should be `retry`\")\n                return\n            }\n            XCTAssertNotNil(userInfo)\n            exp.fulfill()\n        }\n\n        // Simulate network reconnection\n        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {\n            networkMonitor.simulateNetworkChange(isConnected: true)\n        }\n\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n}\n\nprivate struct E: Error {}\n\nfinal class ImmediateStopRetryStrategy: RetryStrategy, @unchecked Sendable {\n\n    let queue = DispatchQueue(label: \"com.onevcat.KingfisherTests.ImmediateStopRetryStrategy\")\n    var _count = 0\n    var count: Int {\n        get { queue.sync { _count } }\n        set { queue.sync { _count = newValue } }\n    }\n\n    func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) {\n        count += 1\n        retryHandler(.stop)\n    }\n}\n\nfinal class StubRetryStrategy: RetryStrategy, @unchecked Sendable {\n\n    let queue = DispatchQueue(label: \"com.onevcat.KingfisherTests.StubRetryStrategy\")\n    var _count = 0\n    var count: Int {\n        get { queue.sync { _count } }\n        set { queue.sync { _count = newValue } }\n    }\n\n    func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) {\n\n        if count == 0 {\n            XCTAssertNil(context.userInfo)\n        } else {\n            XCTAssertEqual(context.userInfo as! Int, count)\n        }\n\n        XCTAssertEqual(context.retriedCount, count)\n\n        count += 1\n        if count == 3 {\n            retryHandler(.stop)\n        } else {\n            retryHandler(.retry(userInfo: count))\n        }\n    }\n}\n\n// MARK: - Test Network Monitoring Implementations\n\n/// A test implementation of NetworkMonitoring that allows controlling network state for testing.\nfinal class TestNetworkMonitor: @unchecked Sendable, NetworkMonitoring {\n    private let queue = DispatchQueue(label: \"com.onevcat.KingfisherTests.TestNetworkMonitor\", attributes: .concurrent)\n    private var _isConnected: Bool\n    private var observers: [TestNetworkObserver] = []\n\n    var isConnected: Bool {\n        get { queue.sync { _isConnected } }\n        set { queue.sync(flags: .barrier) { _isConnected = newValue } }\n    }\n\n    init(isConnected: Bool = true) {\n        self._isConnected = isConnected\n    }\n\n    func observeConnectivity(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void) -> NetworkObserver {\n        let observer = TestNetworkObserver(\n            timeoutInterval: timeoutInterval,\n            callback: callback,\n            monitor: self\n        )\n\n        queue.sync(flags: .barrier) {\n            observers.append(observer)\n        }\n\n        return observer\n    }\n\n    /// Simulates network state change and notifies all observers.\n    func simulateNetworkChange(isConnected: Bool) {\n        queue.sync(flags: .barrier) {\n            _isConnected = isConnected\n            let activeObservers = observers\n            observers.removeAll()\n\n            DispatchQueue.main.async {\n                activeObservers.forEach { $0.notify(isConnected: isConnected) }\n            }\n        }\n    }\n\n    /// Removes an observer from the list.\n    func removeObserver(_ observer: TestNetworkObserver) {\n        queue.sync(flags: .barrier) {\n            observers.removeAll { $0 === observer }\n        }\n    }\n}\n\n/// Test implementation of NetworkObserver for testing purposes.\nfinal class TestNetworkObserver: @unchecked Sendable, NetworkObserver {\n    let timeoutInterval: TimeInterval?\n    let callback: @Sendable (Bool) -> Void\n    private weak var monitor: TestNetworkMonitor?\n    private var timeoutWorkItem: DispatchWorkItem?\n    private let queue = DispatchQueue(label: \"com.onevcat.KingfisherTests.TestNetworkObserver\", qos: .utility)\n\n    init(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void, monitor: TestNetworkMonitor) {\n        self.timeoutInterval = timeoutInterval\n        self.callback = callback\n        self.monitor = monitor\n\n        // Set up timeout if specified\n        if let timeoutInterval = timeoutInterval {\n            let workItem = DispatchWorkItem { [weak self] in\n                self?.notify(isConnected: false)\n            }\n            timeoutWorkItem = workItem\n            queue.asyncAfter(deadline: .now() + timeoutInterval, execute: workItem)\n        }\n    }\n\n    func notify(isConnected: Bool) {\n        queue.async { [weak self] in\n            guard let self else { return }\n\n            // Cancel timeout if we're notifying\n            timeoutWorkItem?.cancel()\n            timeoutWorkItem = nil\n\n            // Remove from monitor\n            monitor?.removeObserver(self)\n\n            // Call the callback\n            DispatchQueue.main.async {\n                self.callback(isConnected)\n            }\n        }\n    }\n\n    func cancel() {\n        queue.async { [weak self] in\n            guard let self else { return }\n\n            // Cancel timeout\n            timeoutWorkItem?.cancel()\n            timeoutWorkItem = nil\n\n            // Remove from monitor\n            monitor?.removeObserver(self)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/StorageExpirationTests.swift",
    "content": "//\n//  StorageExpirationTests.swift\n//  Kingfisher\n//\n//  Created by onevcat on 2018/11/12.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport XCTest\n@testable import Kingfisher\n\nclass StorageExpirationTests: XCTestCase {\n\n    func testExpirationNever() {\n        let e = StorageExpiration.never\n        XCTAssertEqual(e.estimatedExpirationSinceNow, .distantFuture)\n        XCTAssertEqual(e.timeInterval, .infinity)\n        XCTAssertFalse(e.isExpired)\n    }\n\n    func testExpirationSeconds() {\n        let e = StorageExpiration.seconds(100)\n        XCTAssertEqual(\n            e.estimatedExpirationSinceNow.timeIntervalSince1970,\n            Date().timeIntervalSince1970 + 100,\n            accuracy: 0.1)\n        XCTAssertEqual(e.timeInterval, 100)\n        XCTAssertFalse(e.isExpired)\n    }\n    \n    func testExpirationDays() {\n        let e = StorageExpiration.days(1)\n        let oneDayInSecond = TimeInterval(TimeConstants.secondsInOneDay)\n        XCTAssertEqual(\n            e.estimatedExpirationSinceNow.timeIntervalSince1970,\n            Date().timeIntervalSince1970 + oneDayInSecond,\n            accuracy: 0.1)\n        XCTAssertEqual(e.timeInterval, oneDayInSecond, accuracy: 0.1)\n        XCTAssertFalse(e.isExpired)\n    }\n    \n    func testExpirationDate() {\n        let oneDayInSecond = TimeInterval(TimeConstants.secondsInOneDay)\n        let targetDate = Date().addingTimeInterval(oneDayInSecond)\n        let e = StorageExpiration.date(targetDate)\n        XCTAssertEqual(\n            e.estimatedExpirationSinceNow.timeIntervalSince1970,\n            Date().timeIntervalSince1970 + oneDayInSecond,\n            accuracy: 0.1)\n        XCTAssertEqual(e.timeInterval, oneDayInSecond, accuracy: 0.1)\n        XCTAssertFalse(e.isExpired)\n    }\n    \n    func testAlreadyExpired() {\n        let e = StorageExpiration.expired\n        XCTAssertTrue(e.isExpired)\n        XCTAssertEqual(e.estimatedExpirationSinceNow, .distantPast)\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/StringExtensionTests.swift",
    "content": "//\n//  StringExtensionTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 16/8/14.\n//  Copyright © 2019 Wei Wang. All rights reserved.\n//\n\nimport XCTest\n@testable import Kingfisher\n\nclass StringExtensionTests: XCTestCase {\n    func testStringSHA256() {\n        let s = \"hello\"\n        XCTAssertEqual(s.kf.sha256, \"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\")\n    }\n}\n"
  },
  {
    "path": "Tests/KingfisherTests/UIButtonExtensionTests.swift",
    "content": "//\n//  UIButtonExtensionTests.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 15/4/17.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\n#if canImport(UIKit)\nimport UIKit\nimport XCTest\n@testable import Kingfisher\n\nclass UIButtonExtensionTests: XCTestCase {\n\n    var button: UIButton!\n    \n    override class func setUp() {\n        super.setUp()\n        LSNocilla.sharedInstance().start()\n    }\n    \n    override class func tearDown() {\n        LSNocilla.sharedInstance().stop()\n        super.tearDown()\n    }\n    \n    override func setUp() {\n        super.setUp()\n        button = UIButton()\n        KingfisherManager.shared.downloader = ImageDownloader(name: \"testDownloader\")\n        KingfisherManager.shared.defaultOptions = [.waitForCache]\n        \n        cleanDefaultCache()\n    }\n    \n    override func tearDown() {\n        LSNocilla.sharedInstance().clearStubs()\n        button = nil\n        cleanDefaultCache()\n        KingfisherManager.shared.defaultOptions = .empty\n        super.tearDown()\n    }\n\n    @MainActor func testDownloadAndSetImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n\n        var progressBlockIsCalled = false\n\n        KF.url(url)\n            .onProgress { _, _ in\n                progressBlockIsCalled = true\n            }\n            .onSuccess { result in\n                XCTAssertTrue(progressBlockIsCalled)\n\n                XCTAssertTrue(result.image.renderEqual(to: testImage))\n                XCTAssertTrue(self.button.image(for: .normal)!.renderEqual(to: testImage))\n\n                XCTAssertEqual(result.cacheType, .none)\n\n                exp.fulfill()\n            }\n            .set(to: button, for: .normal)\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testDownloadAndSetBackgroundImage() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        stub(url, data: testImageData, length: 123)\n        \n        var progressBlockIsCalled = false\n        KF.url(url)\n            .onProgress { _, _ in\n                progressBlockIsCalled = true\n            }\n            .onSuccess { result in\n                XCTAssertTrue(progressBlockIsCalled)\n\n                XCTAssertTrue(result.image.renderEqual(to: testImage))\n                XCTAssertTrue(self.button.backgroundImage(for: .normal)!.renderEqual(to: testImage))\n\n                XCTAssertEqual(result.cacheType, .none)\n\n                exp.fulfill()\n            }\n            .setBackground(to: button, for: .normal)\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testCancelImageTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n\n        KF.url(url)\n            .onFailure { error in\n                XCTAssertTrue(error.isTaskCancelled)\n                delay(0.1) { exp.fulfill() }\n            }\n            .set(to: button, for: .highlighted)\n        \n        self.button.kf.cancelImageDownloadTask()\n        _ = stub.go()\n\n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testCancelBackgroundImageTask() {\n        let exp = expectation(description: #function)\n        let url = testURLs[0]\n        let stub = delayedStub(url, data: testImageData)\n\n        KF.url(url)\n            .onFailure { error in\n                XCTAssertTrue(error.isTaskCancelled)\n                delay(0.1) { exp.fulfill() }\n            }\n            .setBackground(to: button, for: .highlighted)\n        \n        self.button.kf.cancelBackgroundImageDownloadTask()\n        _ = stub.go()\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testSettingNilURL() {\n        let exp = expectation(description: #function)\n        \n        let url: URL? = nil\n        button.kf.setBackgroundImage(with: url, for: .normal, completionHandler:  { result in\n            XCTAssertNil(result.value)\n            XCTAssertNotNil(result.error)\n            guard case .imageSettingError(reason: .emptySource) = result.error! else {\n                XCTFail()\n                return\n            }\n            exp.fulfill()\n        })\n        \n        waitForExpectations(timeout: 3, handler: nil)\n    }\n    \n    @MainActor func testSettingNonWorkingImageWithFailureImage() {\n        let expectation = self.expectation(description: \"wait for downloading image\")\n        let url = testURLs[0]\n        stub(url, errorCode: 404)\n        let state = UIControl.State()\n\n        KF.url(url)\n            .onFailureImage(testImage)\n            .onFailure { error in\n                XCTAssertEqual(testImage, self.button.image(for: state))\n                expectation.fulfill()\n            }\n            .set(to: button, for: state)\n        XCTAssertNil(button.image(for: state))\n        waitForExpectations(timeout: 5, handler: nil)\n    }\n    \n    @MainActor func testSettingNonWorkingBackgroundImageWithFailureImage() {\n        let expectation = self.expectation(description: \"wait for downloading image\")\n        let url = testURLs[0]\n        stub(url, errorCode: 404)\n        let state = UIControl.State()\n\n        KF.url(url)\n            .onFailureImage(testImage)\n            .onFailure { error in\n                XCTAssertEqual(testImage, self.button.backgroundImage(for: state))\n                expectation.fulfill()\n            }\n            .setBackground(to: button, for: state)\n\n        XCTAssertNil(button.backgroundImage(for: state))\n        waitForExpectations(timeout: 5, handler: nil)\n\n    }\n}\n#endif\n"
  },
  {
    "path": "Tests/KingfisherTests/Utils/StubHelpers.swift",
    "content": "//\n//  StubHelpers.swift\n//  Kingfisher\n//\n//  Created by Wei Wang on 2018/10/12.\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\"), to deal\n//  in the Software without restriction, including without limitation the rights\n//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//  copies of the Software, and to permit persons to whom the Software is\n//  furnished to do so, subject to the following conditions:\n//\n//  The above copyright notice and this permission notice shall be included in\n//  all copies or substantial portions of the Software.\n//\n//  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//  THE SOFTWARE.\n\nimport Foundation\n\n@discardableResult\nfunc stub(_ url: URL,\n          data: Data,\n          statusCode: Int = 200,\n          length: Int? = nil,\n          headers: [String: String] = [:]\n) -> LSStubResponseDSL {\n    var stubResult = stubRequest(\"GET\", url.absoluteString as NSString)\n        .andReturn(statusCode)?\n        .withHeaders(headers)?\n        .withBody(data as NSData)\n    if let length = length {\n        stubResult = stubResult?.withHeader(\"Content-Length\", \"\\(length)\")\n    }\n    return stubResult!\n}\n\nfunc delayedStub(_ url: URL,\n                 data: Data,\n                 statusCode: Int = 200,\n                 length: Int? = nil,\n                 headers: [String: String] = [:]\n) -> LSStubResponseDSL {\n    let result = stub(url, data: data, statusCode: statusCode, length: length, headers: headers)\n    return result.delay()!\n}\n\nfunc stub(_ url: URL, errorCode: Int) {\n    let error = NSError(domain: \"stubError\", code: errorCode, userInfo: nil)\n    stub(url, error: error)\n}\n\nfunc stub(_ url: URL, error: any Error) {\n    return stubRequest(\"GET\", url.absoluteString as NSString).andFailWithError(error)\n}\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "<!-- Generated: 2025-06-15 10:30:00 UTC -->\n\n# Kingfisher Architecture Documentation\n\n## High-Level System Organization\n\nKingfisher is a sophisticated image loading and caching library for Apple platforms, designed with a modular architecture that promotes separation of concerns and extensibility. At its core, the library employs a coordinator pattern where `KingfisherManager` serves as the central orchestrator, managing the flow between network operations, caching layers, and image processing pipelines. The architecture leverages protocol-oriented design principles, with all functionality exposed through a `.kf` namespace wrapper that provides a clean, chainable API surface.\n\nThe system is built on three fundamental pillars: downloading, caching, and processing. The `ImageDownloader` handles all network operations with support for authentication, retries, and progressive loading. The `ImageCache` implements a dual-layer caching strategy combining memory and disk storage for optimal performance. The `ImageProcessor` protocol enables a flexible transformation pipeline where multiple processors can be chained together. These components work in concert through a sophisticated options system (`KingfisherOptionsInfo`) that allows fine-grained control over every aspect of the image loading process.\n\nCross-platform compatibility is achieved through extensive use of conditional compilation and type aliases, allowing the same codebase to support iOS, macOS, tvOS, watchOS, and visionOS. The library provides both UIKit/AppKit extensions and dedicated SwiftUI components (`KFImage`, `KFAnimatedImage`), ensuring seamless integration regardless of the UI framework being used.\n\n## Component Map\n\n### Core Components\n\n| Component | Location | Purpose |\n|-----------|----------|---------|\n| **KingfisherManager** | `Sources/General/KingfisherManager.swift` | Central coordinator managing image retrieval, caching, and processing workflows |\n| **ImageDownloader** | `Sources/Networking/ImageDownloader.swift` | Handles all network operations for downloading images |\n| **ImageCache** | `Sources/Cache/ImageCache.swift` | Dual-layer caching system with memory and disk storage |\n| **ImageProcessor** | `Sources/Image/ImageProcessor.swift` | Protocol and implementations for image transformation pipeline |\n| **KF** | `Sources/General/KF.swift` | Builder pattern entry point for fluent API |\n| **Source** | `Sources/General/ImageSource/Source.swift` | Represents image data sources (network/provider) |\n| **Resource** | `Sources/General/ImageSource/Resource.swift` | Protocol for cacheable resources with key/URL |\n\n### Storage Layer\n\n| Component | Location | Purpose |\n|-----------|----------|---------|\n| **MemoryStorage** | `Sources/Cache/MemoryStorage.swift` | In-memory cache implementation with LRU eviction |\n| **DiskStorage** | `Sources/Cache/DiskStorage.swift` | File-based cache with expiration and size limits |\n| **CacheSerializer** | `Sources/Cache/CacheSerializer.swift` | Handles image data serialization for cache storage |\n\n### Networking Layer\n\n| Component | Location | Purpose |\n|-----------|----------|---------|\n| **SessionDelegate** | `Sources/Networking/SessionDelegate.swift` | URLSession delegate for download management |\n| **SessionDataTask** | `Sources/Networking/SessionDataTask.swift` | Wrapper for URLSessionDataTask with cancellation |\n| **ImagePrefetcher** | `Sources/Networking/ImagePrefetcher.swift` | Preloads images for improved performance |\n| **RequestModifier** | `Sources/Networking/RequestModifier.swift` | Protocol for modifying URL requests |\n| **RetryStrategy** | `Sources/Networking/RetryStrategy.swift` | Configurable retry logic for failed downloads |\n\n### UI Integration\n\n| Component | Location | Purpose |\n|-----------|----------|---------|\n| **ImageView+Kingfisher** | `Sources/Extensions/ImageView+Kingfisher.swift` | UIImageView/NSImageView extensions |\n| **KFImage** | `Sources/SwiftUI/KFImage.swift` | SwiftUI image component |\n| **KFAnimatedImage** | `Sources/SwiftUI/KFAnimatedImage.swift` | SwiftUI animated image support |\n| **AnimatedImageView** | `Sources/Views/AnimatedImageView.swift` | GIF animation support view |\n\n## Key Files\n\n### KingfisherManager.swift (Lines 107-420)\nThe heart of the library, containing:\n- `shared` singleton instance (line 113)\n- `retrieveImage()` main entry point (lines 196-210, 233-248)\n- Cache lookup logic (lines 400-403)\n- Download coordination (lines 415-418)\n- Retry and alternative source handling (lines 306-385)\n\n### ImageDownloader.swift (Lines 35-150)\nNetwork layer implementation:\n- `ImageLoadingResult` struct for download results (lines 36-58)\n- `DownloadTask` class for cancellable downloads (lines 65-102)\n- URLSession management and request handling\n\n### ImageCache.swift (Lines 52-200)\nCaching infrastructure:\n- `CacheType` enum defining cache levels (lines 52-72)\n- Memory and disk cache coordination\n- Cache key generation and expiration logic\n\n### KF.swift (Lines 50-100)\nBuilder pattern implementation:\n- Static factory methods for creating builders (lines 56-99)\n- Fluent API entry points for different source types\n\n### ImageProcessor.swift (Lines 37-100)\nProcessing pipeline:\n- `ImageProcessItem` enum for input types (lines 37-46)\n- `ImageProcessor` protocol definition (lines 49-76)\n- Processor chaining via `append()` (lines 85-95)\n\n### KingfisherOptionsInfo.swift (Lines 43-250)\nConfiguration system:\n- Option items enumeration with associated values\n- Cache, downloader, and processor configuration\n- Transition and placeholder settings\n\n## Data Flow\n\n### 1. Image Request Initiation\n\n```\nUIImageView.kf.setImage(with: url)\n    │\n    └─> ImageView+Kingfisher.swift (line 77-87)\n        │\n        └─> KingfisherManager.retrieveImage()\n            Sources/General/KingfisherManager.swift (line 196)\n```\n\n### 2. Cache Lookup\n\n```\nKingfisherManager.retrieveImage()\n    │\n    └─> retrieveImageFromCache() (line 400)\n        │\n        ├─> ImageCache.retrieveImage()\n        │   Sources/Cache/ImageCache.swift\n        │   │\n        │   ├─> MemoryStorage.value(forKey:)\n        │   │   Sources/Cache/MemoryStorage.swift\n        │   │\n        │   └─> DiskStorage.value(forKey:)\n        │       Sources/Cache/DiskStorage.swift\n        │\n        └─> [Cache Hit] → completionHandler(.success)\n            [Cache Miss] → Continue to download\n```\n\n### 3. Network Download\n\n```\nKingfisherManager.loadAndCacheImage() (line 415)\n    │\n    └─> ImageDownloader.downloadImage()\n        Sources/Networking/ImageDownloader.swift\n        │\n        ├─> SessionDelegate.downloadTask()\n        │   Sources/Networking/SessionDelegate.swift\n        │\n        └─> URLSession.dataTask()\n            │\n            └─> CompletionHandler with ImageLoadingResult\n```\n\n### 4. Image Processing\n\n```\nDownloaded Data\n    │\n    └─> ImageProcessor.process() (line 434)\n        Sources/Image/ImageProcessor.swift\n        │\n        ├─> DefaultImageProcessor (if none specified)\n        │\n        └─> Custom processors chain\n            │\n            └─> Processed KFCrossPlatformImage\n```\n\n### 5. Cache Storage\n\n```\nProcessed Image\n    │\n    └─> KingfisherManager.cacheImage() (line 459)\n        │\n        ├─> ImageCache.store() (line 482)\n        │   │\n        │   ├─> MemoryStorage.store()\n        │   │   In-memory cache with cost calculation\n        │   │\n        │   └─> DiskStorage.store()\n        │       File system with expiration\n        │\n        └─> completionHandler(.success(RetrieveImageResult))\n            │\n            └─> UI Update on main queue\n```\n\n### 6. Error Handling and Retry\n\n```\nDownload/Processing Error\n    │\n    └─> RetryStrategy.retry() (line 362)\n        Sources/Networking/RetryStrategy.swift\n        │\n        ├─> [Retry] → startNewRetrieveTask() (line 306)\n        │\n        └─> [No Retry] → Check alternative sources (line 334)\n            │\n            ├─> [Alternative exists] → Start new task\n            │\n            └─> [No alternatives] → completionHandler(.failure)\n```\n\nThis architecture enables Kingfisher to efficiently handle image loading with features like progressive downloading, multiple cache layers, flexible processing pipelines, and robust error handling, all while maintaining a clean and intuitive API surface for developers."
  },
  {
    "path": "docs/build-system.md",
    "content": "<!-- Generated: 2025-06-15 12:00:00 UTC -->\n\n# Kingfisher Build System Documentation\n\n## Overview\n\nKingfisher uses a dual build system approach supporting both Swift Package Manager and Fastlane/CocoaPods for maximum flexibility and distribution options.\n\n### Primary Build Tools\n\n- **Swift Package Manager** (`Package.swift`) - Modern dependency management and building\n- **Fastlane** (`fastlane/Fastfile`) - Automated testing, building, and release workflows\n- **CocoaPods** (`Kingfisher.podspec`) - Legacy distribution and integration\n- **GitHub Actions** (`.github/workflows/`) - Continuous integration and testing\n\n### Key Configuration Files\n\n```\n.\n├── Package.swift                    # Swift Package Manager configuration\n├── Kingfisher.podspec              # CocoaPods specification\n├── Gemfile                         # Ruby dependencies for Fastlane/CocoaPods\n├── fastlane/\n│   ├── Fastfile                    # Fastlane automation workflows\n│   └── actions/                    # Custom Fastlane actions\n└── .github/workflows/\n    ├── build.yaml                  # CI build workflow\n    └── test.yaml                   # CI test workflow\n```\n\n## Build Workflows\n\n### Building with Swift Package Manager\n\n```bash\n# Build for default platform\nswift build\n\n# Build with specific Swift version\nswift build -Xswiftc -swift-version -Xswiftc 5\n\n# Build for release\nswift build -c release\n\n# Build and run tests\nswift test\n\n# Generate Xcode project (if needed)\nswift package generate-xcodeproj\n```\n\n### Building with Fastlane\n\nFirst, install dependencies:\n```bash\n# Install Ruby dependencies\nbundle install\n```\n\nCommon build commands:\n```bash\n# Run all platform tests (iOS, macOS, tvOS, watchOS)\nbundle exec fastlane tests\n\n# Build for CI with specific destination\nDESTINATION=\"platform=iOS Simulator,name=iPhone 15,OS=17.5\" bundle exec fastlane build_ci\n\n# Test for CI with specific destination  \nDESTINATION=\"platform=macOS\" bundle exec fastlane test_ci\n\n# Build specific platform\nbundle exec fastlane build destination:\"platform=iOS Simulator,name=iPhone 15\"\n\n# Lint both CocoaPods and SPM\nbundle exec fastlane lint\n```\n\n### Release Process\n\nThe release workflow automates versioning, tagging, and distribution:\n\n```bash\n# Full release (tests, lint, version bump, GitHub release, CocoaPods push)\nbundle exec fastlane release version:X.X.X\n\n# Skip tests during release\nbundle exec fastlane release version:X.X.X skip_tests:true\n\n# Create XCFramework for distribution\nbundle exec fastlane xcframework version:X.X.X\n```\n\nRelease steps performed:\n1. Ensures clean git state and correct branch\n2. Runs all tests (unless skipped)\n3. Lints CocoaPods spec and SPM package\n4. Updates version in all configuration files\n5. Extracts and updates changelog\n6. Creates signed git tag\n7. Builds XCFramework for all platforms\n8. Creates GitHub release with assets\n9. Pushes to CocoaPods trunk\n\n## Platform-specific Setup\n\n### Supported Platforms\n\nFrom `Package.swift` and `Kingfisher.podspec`:\n- **iOS**: 13.0+\n- **macOS**: 10.15+\n- **tvOS**: 13.0+\n- **watchOS**: 6.0+\n- **visionOS**: 1.0+\n\n### CI Test Matrix\n\nFrom `.github/workflows/test.yaml`:\n- **Destinations**: macOS, iOS Simulator, tvOS Simulator, watchOS Simulator\n- **Xcode Versions**: 15.4, 16.2\n\n### Platform Build Commands\n\n```bash\n# macOS\nbundle exec fastlane test destination:\"platform=macOS\"\n\n# iOS Simulator\nbundle exec fastlane test destination:\"platform=iOS Simulator,name=iPhone 15,OS=17.5\"\n\n# tvOS Simulator  \nbundle exec fastlane test destination:\"platform=tvOS Simulator,name=Apple TV,OS=17.5\"\n\n# watchOS Simulator (build only, no test)\nbundle exec fastlane build destination:\"platform=watchOS Simulator,name=Apple Watch Series 9 (41mm),OS=10.5\"\n```\n\n## Reference\n\n### Build Targets\n\nFrom `Package.swift`:\n- **Library**: `Kingfisher` (single library product)\n- **Target**: `Kingfisher` (sources in `Sources/` directory)\n\n### Fastlane Lanes\n\nAvailable lanes in `fastlane/Fastfile`:\n\n| Lane | Description | Parameters |\n|------|-------------|------------|\n| `tests` | Run tests on all platforms | None |\n| `test` | Run tests on specific platform | `destination` |\n| `build` | Build for specific platform | `destination` |\n| `test_ci` | CI test lane (builds watchOS) | Uses `ENV[\"DESTINATION\"]` |\n| `build_ci` | CI build lane | Uses `ENV[\"DESTINATION\"]` |\n| `lint` | Lint CocoaPods spec and SPM | None |\n| `release` | Full release workflow | `version`, `skip_tests` (optional) |\n| `xcframework` | Build XCFramework | `version`, `swift_version`, `xcode_version` |\n\n### Environment Variables\n\n| Variable | Description | Used By |\n|----------|-------------|---------|\n| `DESTINATION` | Build/test destination | CI workflows |\n| `XCODE_VERSION` | Xcode version to use | CI workflows, Fastlane |\n| `GITHUB_TOKEN` | GitHub API token | Release workflow |\n\n### Custom Fastlane Actions\n\nLocated in `fastlane/actions/`:\n- `extract_current_change_log.rb` - Extract changelog for version\n- `git_commit_all.rb` - Commit all changes\n- `sync_build_number_to_git.rb` - Sync build number with git\n- `update_change_log.rb` - Update changelog file\n\n### Troubleshooting\n\n#### Common Issues\n\n1. **Bundle install fails**\n   ```bash\n   # Update bundler\n   gem install bundler\n   \n   # Install with specific bundler version\n   bundle _2.x.x_ install\n   ```\n\n2. **Xcode version mismatch**\n   ```bash\n   # Set Xcode version explicitly\n   XCODE_VERSION=16.2 bundle exec fastlane tests\n   \n   # Or use xcode-select\n   sudo xcode-select -s /Applications/Xcode_16.2.app\n   ```\n\n3. **Simulator not found**\n   ```bash\n   # List available simulators\n   xcrun simctl list devices\n   \n   # Update destination string accordingly\n   ```\n\n4. **CocoaPods push fails**\n   ```bash\n   # Verify pod spec locally first\n   pod lib lint Kingfisher.podspec\n   \n   # Register session if needed\n   pod trunk register email@example.com\n   ```\n\n5. **Swift version issues**\n   ```bash\n   # Override Swift version in build\n   bundle exec fastlane build xcargs:\"SWIFT_VERSION=5.9\"\n   ```\n\n### Build Settings\n\nKey build settings used:\n- `BUILD_LIBRARY_FOR_DISTRIBUTION`: YES (for XCFramework)\n- `SKIP_INSTALL`: NO (for archiving)\n- `SWIFT_VERSION`: 5.0 (default, can be overridden)\n\n### Distribution Artifacts\n\nRelease builds generate:\n- `Kingfisher-{version}.xcframework.zip` - All platforms XCFramework\n- `Kingfisher-iOS-{version}.xcframework.zip` - iOS-only XCFramework\n\nBoth are code-signed with Apple Distribution certificate and uploaded to GitHub releases."
  },
  {
    "path": "docs/deployment.md",
    "content": "# Deployment Guide\n\n<!-- Generated: 2025-06-15 10:15:30 UTC -->\n\nThis document provides comprehensive guidance for deploying Kingfisher across different platforms and distribution channels.\n\n## Overview\n\nKingfisher supports multiple deployment strategies:\n\n- **CocoaPods**: Traditional CocoaPods spec deployment\n- **Swift Package Manager**: Native Swift package distribution\n- **XCFramework**: Pre-built universal frameworks\n- **GitHub Releases**: Automated release management\n\nThe deployment process is fully automated using Fastlane with comprehensive platform coverage across iOS, macOS, tvOS, watchOS, and visionOS.\n\n## Package Types\n\n### CocoaPods Distribution\n\n**Configuration File**: `/Users/onevcat/Sync/github/Kingfisher/Kingfisher.podspec`\n\n**Build Targets**:\n- iOS 13.0+\n- macOS 10.15+\n- tvOS 13.0+\n- watchOS 6.0+\n- visionOS 1.0+\n\n**Key Features**:\n- Module stability enabled (`BUILD_LIBRARY_FOR_DISTRIBUTION`)\n- Privacy manifest included (`PrivacyInfo.xcprivacy`)\n- Weak framework dependencies (SwiftUI, Combine)\n- Required frameworks (CFNetwork, Accelerate)\n\n### Swift Package Manager\n\n**Configuration File**: `/Users/onevcat/Sync/github/Kingfisher/Package.swift`\n\n**Build Targets**:\n- Single library target: `Kingfisher`\n- Source path: `Sources/`\n- Minimum Swift tools version: 5.1\n\n**Platform Support**:\n- iOS 13.0+\n- macOS 10.15+\n- tvOS 13.0+\n- watchOS 6.0+\n\n### XCFramework Distribution\n\n**Output Locations**:\n```\nbuild/\n├── Kingfisher-{version}.xcframework.zip          # All platforms\n├── Kingfisher-iOS-{version}.xcframework.zip      # iOS only\n├── Kingfisher-{version}/\n│   └── Kingfisher.xcframework/\n│       ├── ios-arm64/\n│       ├── ios-arm64_x86_64-simulator/\n│       ├── macos-arm64_x86_64/\n│       ├── tvos-arm64/\n│       ├── tvos-arm64_x86_64-simulator/\n│       ├── watchos-arm64_arm64_32_armv7k/\n│       ├── watchos-arm64_i386_x86_64-simulator/\n│       ├── xros-arm64/\n│       └── xros-arm64_x86_64-simulator/\n```\n\n**Platform-Specific Archives**:\n```\nbuild/\n├── Kingfisher-iphoneos.xcarchive/\n├── Kingfisher-iphonesimulator.xcarchive/\n├── Kingfisher-macosx.xcarchive/\n├── Kingfisher-appletvos.xcarchive/\n├── Kingfisher-appletvsimulator.xcarchive/\n├── Kingfisher-watchos.xcarchive/\n├── Kingfisher-watchsimulator.xcarchive/\n├── Kingfisher-xros.xcarchive/\n└── Kingfisher-xrsimulator.xcarchive/\n```\n\n## Platform-Specific Deployment\n\n### iOS Deployment\n- **Device**: `iphoneos` SDK\n- **Simulator**: `iphonesimulator` SDK\n- **Architectures**: arm64, x86_64 (simulator)\n- **Framework Path**: `build/Kingfisher-iOS-{version}.xcframework.zip`\n\n### macOS Deployment\n- **SDK**: `macosx`\n- **Architectures**: arm64, x86_64 (universal)\n- **Framework Structure**: Traditional bundle format with versioning\n\n### tvOS Deployment\n- **Device**: `appletvos` SDK\n- **Simulator**: `appletvsimulator` SDK\n- **Architectures**: arm64, x86_64 (simulator)\n\n### watchOS Deployment\n- **Device**: `watchos` SDK\n- **Simulator**: `watchsimulator` SDK\n- **Architectures**: arm64, arm64_32, armv7k (device), i386, x86_64, arm64 (simulator)\n\n### visionOS Deployment\n- **Device**: `xros` SDK\n- **Simulator**: `xrsimulator` SDK\n- **Architectures**: arm64, x86_64 (simulator)\n\n## Deployment Commands\n\n### Complete Release Process\n\n```bash\n# Full release workflow\nbundle exec fastlane release version:X.X.X\n\n# Skip tests during release (not recommended)\nbundle exec fastlane release version:X.X.X skip_tests:true\n```\n\n### Individual Components\n\n```bash\n# Create XCFramework only\nbundle exec fastlane xcframework version:X.X.X\n\n# Run linting\nbundle exec fastlane lint\n\n# Build for specific platform\nbundle exec fastlane build_ci\n```\n\n### CocoaPods Deployment\n\n```bash\n# Lint podspec\npod lib lint Kingfisher.podspec\n\n# Push to CocoaPods trunk (automated in release)\npod trunk push Kingfisher.podspec\n```\n\n### Swift Package Manager\n\n```bash\n# Build with SPM\nswift build\n\n# Test with SPM\nswift test\n\n# Validate package\nswift package resolve\n```\n\n## Continuous Integration\n\n### GitHub Actions Workflows\n\n**Build Workflow**: `.github/workflows/build.yaml`\n- Builds across multiple Xcode versions (15.2, 15.3, 16.0, 16.1)\n- Tests all platforms in matrix configuration\n- Uses self-hosted runners\n\n**Test Workflow**: `.github/workflows/test.yaml`\n- Runs tests on Xcode 15.4 and 16.2\n- Covers all platform destinations\n- Concurrent execution with cancellation\n\n**Matrix Configuration**:\n```yaml\ndestination: [\n  'macOS',\n  'iOS Simulator,name=iPhone 15,OS=17.5',\n  'tvOS Simulator,name=Apple TV,OS=17.5',\n  'watchOS Simulator,name=Apple Watch Series 9 (41mm),OS=10.5'\n]\n```\n\n## Release Management\n\n### Automated Release Process\n\nThe release process handles:\n\n1. **Pre-release Validation**:\n   - Git branch verification\n   - Clean git status check\n   - Comprehensive testing across platforms\n   - Podspec and SPM linting\n\n2. **Version Management**:\n   - Build number synchronization\n   - Version number increment\n   - Podspec version update\n   - Changelog extraction\n\n3. **Build Artifacts**:\n   - XCFramework creation for all platforms\n   - Code signing with Apple Distribution certificate\n   - ZIP archive creation\n\n4. **Distribution**:\n   - Git tag creation with signing\n   - GitHub release creation\n   - Asset upload (both full and iOS-only XCFrameworks)\n   - CocoaPods trunk push\n\n### Changelog Management\n\n**Configuration**: `/Users/onevcat/Sync/github/Kingfisher/pre-change.yml`\n\n**Structure**:\n```yaml\nversion: X.X.X\nname: Release Name\nfix:\n  - Bug fix descriptions with issue links\nadd:\n  - New feature descriptions\n```\n\n### Version Tagging\n\n- **Format**: Semantic versioning (X.X.X)\n- **Signing**: GPG signed tags\n- **Automation**: Integrated with release workflow\n\n## Reference\n\n### Deployment Scripts\n\n| Script Location | Purpose |\n|----------------|---------|\n| `fastlane/Fastfile` | Main deployment automation |\n| `.github/workflows/build.yaml` | CI build workflow |\n| `.github/workflows/test.yaml` | CI test workflow |\n| `Gemfile` | Ruby dependencies |\n\n### Build Output Locations\n\n| Package Type | Location |\n|-------------|----------|\n| CocoaPods | Published to CocoaPods trunk |\n| Swift Package Manager | Git repository tags |\n| XCFramework (All) | `build/Kingfisher-{version}.xcframework.zip` |\n| XCFramework (iOS) | `build/Kingfisher-iOS-{version}.xcframework.zip` |\n| GitHub Release Assets | Attached to release tags |\n\n### Environment Variables\n\n| Variable | Purpose | Required |\n|----------|---------|----------|\n| `GITHUB_TOKEN` | GitHub API authentication | Yes (release) |\n| `DESTINATION` | CI build destination | Yes (CI) |\n| `XCODE_VERSION` | Xcode version selection | Yes (CI) |\n\n### Code Signing\n\n- **Certificate**: Apple Distribution: Wei Wang (A4YJ9MRZ66)\n- **Timestamp**: Enabled for all XCFramework builds\n- **Verification**: Automated signature verification\n\n### Server Configurations\n\n**GitHub Actions**:\n- **Runner Type**: Self-hosted\n- **Concurrency**: Group-based with cancellation\n- **Shell**: `bash -leo pipefail {0}`\n\n**Ruby Environment**:\n- **Fastlane**: Latest version\n- **CocoaPods**: Latest version  \n- **xcodes**: For Xcode version management\n\nThis deployment system ensures reliable, automated distribution across all supported Apple platforms with comprehensive testing and validation at each step.\n"
  },
  {
    "path": "docs/development.md",
    "content": "# Development Guide\n\n<!-- Generated: 2025-06-15 12:40:15 UTC -->\n\n## Overview\n\nKingfisher follows a modular architecture designed for maintainability, testability, and cross-platform compatibility. The development environment emphasizes protocol-oriented programming, namespace safety, and comprehensive test coverage. The codebase is organized into distinct functional modules with clear separation of concerns, following Swift's modern concurrency patterns and maintaining compatibility across iOS, macOS, tvOS, watchOS, and visionOS platforms.\n\nThe codebase implements several sophisticated design patterns including the namespace wrapper pattern for API safety, builder patterns for fluent configuration, and options patterns for flexible customization. All components are designed to be thread-safe with explicit concurrency annotations where needed. Development follows strict code style guidelines with comprehensive documentation, ensuring consistency across the large codebase.\n\nTesting is integral to the development process, with extensive unit tests covering all major components, network mocking for reliable testing, and cross-platform validation. The build system uses Fastlane for automation, supporting multiple deployment targets and maintaining high quality standards through automated linting and testing workflows.\n\n## Code Style Conventions\n\n### File Headers and Documentation\n\nAll source files must include the standard license header:\n\n```swift\n//\n//  FileName.swift\n//  Kingfisher\n//\n//  Created by [Author] on [Date].\n//\n//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>\n//\n//  Permission is hereby granted, free of charge, to any person obtaining a copy\n//  of this software and associated documentation files (the \"Software\")...\n```\n\n### Naming Conventions\n\n- **Files**: Use PascalCase with descriptive names (`ImageProcessor.swift`, `KingfisherManager.swift`)\n- **Types**: PascalCase for classes, structs, protocols, and enums\n- **Methods/Properties**: camelCase starting with lowercase\n- **Constants**: Use `static let` for type constants, `let` for instance constants\n- **Protocols**: Use descriptive names, often ending with `-able` or describing capability\n\nExample from `/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift`:\n```swift\npublic protocol KingfisherCompatible: AnyObject { }\npublic protocol KingfisherCompatibleValue {}\n```\n\n### Cross-Platform Type Aliases\n\nKingfisher uses consistent cross-platform type aliases defined in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift`:\n\n```swift\n#if os(macOS)\npublic typealias KFCrossPlatformImage       = NSImage\npublic typealias KFCrossPlatformView        = NSView\npublic typealias KFCrossPlatformColor       = NSColor\npublic typealias KFCrossPlatformImageView   = NSImageView\npublic typealias KFCrossPlatformButton      = NSButton\n#else\npublic typealias KFCrossPlatformImage       = UIImage\npublic typealias KFCrossPlatformColor       = UIColor\n// ... additional platform-specific definitions\n#endif\n```\n\n### Sendable Compliance\n\nModern Swift concurrency is enforced throughout the codebase:\n\n```swift\npublic struct KingfisherWrapper<Base>: @unchecked Sendable {\n    public let base: Base\n    public init(_ base: Base) {\n        self.base = base\n    }\n}\n```\n\n## Common Implementation Patterns\n\n### 1. Namespace Wrapper Pattern\n\nThe core pattern used throughout Kingfisher, implemented in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift`:\n\n```swift\n/// Wrapper for Kingfisher compatible types\npublic struct KingfisherWrapper<Base>: @unchecked Sendable {\n    public let base: Base\n    public init(_ base: Base) {\n        self.base = base\n    }\n}\n\n/// Protocol for types that can use .kf namespace\npublic protocol KingfisherCompatible: AnyObject { }\n\nextension KingfisherCompatible {\n    /// Gets a namespace holder for Kingfisher compatible types\n    public var kf: KingfisherWrapper<Self> {\n        get { return KingfisherWrapper(self) }\n        set { }\n    }\n}\n\n// Usage in extensions\nextension KFCrossPlatformImage: KingfisherCompatible { }\n```\n\n### 2. Builder Pattern\n\nFluent API implementation in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/KF.swift`:\n\n```swift\npublic enum KF {\n    /// Creates a builder for a given URL\n    public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder {\n        source(url?.convertToSource(overrideCacheKey: cacheKey))\n    }\n}\n\nextension KF {\n    public class Builder: @unchecked Sendable {\n        private let source: Source?\n        private var _options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions)\n        \n        // Fluent configuration methods\n        public func placeholder(_ image: KFCrossPlatformImage?) -> Self {\n            self.placeholder = image\n            return self\n        }\n    }\n}\n```\n\n### 3. Options Pattern\n\nComprehensive options system in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/KingfisherOptionsInfo.swift`:\n\n```swift\n/// Represents the available option items\npublic enum KingfisherOptionsInfoItem: Sendable {\n    case targetCache(ImageCache)\n    case downloader(ImageDownloader)\n    case transition(ImageTransition)\n    case downloadPriority(Float)\n    case forceRefresh\n    case processor(any ImageProcessor)\n    // ... many more options\n}\n\n/// Parsed options for internal use\npublic struct KingfisherParsedOptionsInfo: Sendable {\n    public var targetCache: ImageCache? = nil\n    public var downloader: ImageDownloader? = nil\n    public var transition: ImageTransition = .none\n    // ... corresponding properties\n    \n    public init(_ info: KingfisherOptionsInfo?) {\n        guard let info = info else { return }\n        for option in info {\n            switch option {\n            case .targetCache(let value): targetCache = value\n            case .downloader(let value): downloader = value\n            // ... handle all options\n            }\n        }\n    }\n}\n```\n\n### 4. Protocol-Oriented Design\n\nExample from `/Users/onevcat/Sync/github/Kingfisher/Sources/Image/ImageProcessor.swift`:\n\n```swift\n/// Protocol for image processing\npublic protocol ImageProcessor: Sendable {\n    /// Identifier for caching and retrieval\n    var identifier: String { get }\n    \n    /// Process the input item\n    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?\n}\n\nextension ImageProcessor {\n    /// Append processors in pipeline\n    public func append(another: any ImageProcessor) -> any ImageProcessor {\n        let newIdentifier = identifier.appending(\"|>\\(another.identifier)\")\n        return GeneralProcessor(identifier: newIdentifier) { item, options in\n            if let image = self.process(item: item, options: options) {\n                return another.process(item: .image(image), options: options)\n            } else {\n                return nil\n            }\n        }\n    }\n}\n```\n\n### 5. Fluent Configuration with KFOptionSetter\n\nProtocol-based fluent API in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/KFOptionsSetter.swift`:\n\n```swift\n@MainActor\npublic protocol KFOptionSetter {\n    var options: KingfisherParsedOptionsInfo { get nonmutating set }\n    var onFailureDelegate: Delegate<KingfisherError, Void> { get }\n    var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { get }\n    var onProgressDelegate: Delegate<(Int64, Int64), Void> { get }\n}\n\nextension KFOptionSetter {\n    public func targetCache(_ cache: ImageCache) -> Self {\n        options.targetCache = cache\n        return self\n    }\n    \n    public func downloader(_ downloader: ImageDownloader) -> Self {\n        options.downloader = downloader\n        return self\n    }\n}\n```\n\n## Development Workflows\n\n### Setting Up Images for UI Components\n\n**Common pattern for UIImageView/NSImageView** (file: `/Users/onevcat/Sync/github/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift`):\n\n```swift\n// Basic usage\nimageView.kf.setImage(with: url)\n\n// With configuration\nimageView.kf.setImage(\n    with: url,\n    placeholder: placeholderImage,\n    options: [.transition(.fade(0.2)), .cacheMemoryOnly],\n    completionHandler: { result in\n        // Handle result\n    }\n)\n```\n\n**Builder pattern approach**:\n```swift\nKF.url(imageURL)\n    .placeholder(placeholderImage)\n    .fade(duration: 0.2)\n    .cacheMemoryOnly()\n    .onSuccess { result in\n        print(\"Image loaded: \\(result.image)\")\n    }\n    .set(to: imageView)\n```\n\n### Adding New Image Processors\n\n1. **Create processor conforming to ImageProcessor protocol**:\n```swift\nstruct CustomProcessor: ImageProcessor {\n    var identifier: String { \"com.example.custom\" }\n    \n    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {\n        // Implementation\n    }\n}\n```\n\n2. **Add convenience method to KFOptionSetter** (file: `/Users/onevcat/Sync/github/Kingfisher/Sources/General/KFOptionsSetter.swift`):\n```swift\nextension KFOptionSetter {\n    public func customEffect() -> Self {\n        appendProcessor(CustomProcessor())\n    }\n}\n```\n\n### Extending Platform Support\n\n**Add platform-specific extensions** (pattern from existing platform extensions):\n\n1. **Update type aliases** in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift`\n2. **Add compatibility conformance**:\n```swift\n#if os(newOS)\nextension NewOSImageView: KingfisherCompatible { }\n#endif\n```\n3. **Implement platform-specific extensions** following the pattern in existing platform files\n\n### Cache Management Tasks\n\n**Working with ImageCache** (main class: `/Users/onevcat/Sync/github/Kingfisher/Sources/Cache/ImageCache.swift`):\n\n```swift\n// Configure cache\nlet cache = ImageCache(name: \"custom\")\ncache.memoryStorage.config.totalCostLimit = 50 * 1024 * 1024 // 50MB\ncache.diskStorage.config.sizeLimit = 200 * 1024 * 1024 // 200MB\n\n// Use with options\nimageView.kf.setImage(with: url, options: [.targetCache(cache)])\n\n// Manual cache operations\ncache.store(image, forKey: key)\ncache.retrieveImage(forKey: key) { result in\n    // Handle cached image\n}\n```\n\n## Reference\n\n### File Organization\n\n```\nSources/\n├── General/           # Core managers, options, data providers\n│   ├── KingfisherManager.swift     # Central coordinator\n│   ├── KF.swift                    # Builder pattern API\n│   ├── Kingfisher.swift            # Core protocols and wrappers\n│   ├── KingfisherOptionsInfo.swift # Options system\n│   └── ImageSource/                # Data source abstractions\n├── Networking/        # Download, prefetch, session management\n│   ├── ImageDownloader.swift       # Network layer\n│   ├── ImagePrefetcher.swift       # Batch prefetching\n│   └── RetryStrategy.swift         # Retry logic\n├── Cache/            # Multi-layer caching system\n│   ├── ImageCache.swift            # Main cache interface\n│   ├── MemoryStorage.swift         # Memory cache backend\n│   └── DiskStorage.swift           # Disk cache backend\n├── Image/            # Processing, filters, formats, transitions\n│   ├── ImageProcessor.swift        # Processing protocols\n│   ├── Filter.swift                # Built-in processors\n│   └── ImageTransition.swift       # UI transition effects\n├── Extensions/       # UIKit/AppKit integration\n│   ├── ImageView+Kingfisher.swift  # Main UI extensions\n│   └── UIButton+Kingfisher.swift   # Button extensions\n├── SwiftUI/         # SwiftUI-specific components\n│   ├── KFImage.swift               # SwiftUI image component\n│   └── KFAnimatedImage.swift       # Animated SwiftUI component\n├── Utility/         # Helper utilities and extensions\n└── Views/           # Custom UI components\n```\n\n### Naming Conventions\n\n- **Manager classes**: `*Manager` (e.g., `KingfisherManager`)\n- **Data providers**: `*Provider` or `*DataProvider` (e.g., `ImageDataProvider`)\n- **Processors**: `*Processor` or `*ImageProcessor` (e.g., `BlurImageProcessor`)\n- **Extensions**: `Type+Kingfisher.swift` (e.g., `UIButton+Kingfisher.swift`)\n- **Protocols**: Descriptive names often with `-able` suffix (`KingfisherCompatible`)\n- **Internal utilities**: Plain descriptive names (`CallbackQueue`, `Result`)\n\n### Common Issues and Solutions\n\n**Thread Safety**: All public APIs are designed to be thread-safe. Use `@MainActor` for UI-related operations and `@unchecked Sendable` for wrapper types.\n\n**Memory Management**: Kingfisher uses both memory and disk caching. Configure limits appropriately:\n```swift\nImageCache.default.memoryStorage.config.totalCostLimit = 50 * 1024 * 1024\nImageCache.default.diskStorage.config.sizeLimit = 200 * 1024 * 1024\n```\n\n**Platform Differences**: Use platform-specific compilation directives and the provided cross-platform type aliases to ensure compatibility.\n\n**Testing**: Use the testing utilities in `/Users/onevcat/Sync/github/Kingfisher/Tests/KingfisherTests/KingfisherTestHelper.swift` and follow the mocking patterns established in existing tests.\n\n**Performance**: For large images, prefer `DownsamplingImageProcessor` over `ResizingImageProcessor` for better memory efficiency."
  },
  {
    "path": "docs/files.md",
    "content": "# File Catalog - Kingfisher\n\n<!-- Generated: 2025-06-15 12:00:00 UTC -->\n\n## Overview\n\nKingfisher is a modern Swift library for loading and caching images, built with a modular structure and protocol-oriented design. The project clearly separates different concerns and uses a namespace pattern (.kf) to provide a consistent API for UIKit, AppKit, and SwiftUI. Its core components include KingfisherManager as the central coordinator, ImageDownloader for handling network tasks, ImageCache for a dual-layer caching system, and ImageProcessor for managing image transformation pipelines.\n\nThe project supports multiple platforms (iOS, macOS, tvOS, watchOS, visionOS) and uses Fastlane as its main build system. It employs the XCTest framework for testing and integrates the DocC documentation system. Files are organized based on functionality, with the Sources directory divided by modules, configuration files centrally managed, and Demo projects offering complete usage examples.\n\n## Core Source Files\n\n### Primary Framework Components\n- **`Sources/General/KingfisherManager.swift`** - Central coordinator managing image loading workflow\n- **`Sources/General/KF.swift`** - Main entry point providing builder pattern API for image tasks  \n- **`Sources/General/Kingfisher.swift`** - Core framework module with KingfisherCompatible protocol\n- **`Sources/General/KingfisherOptionsInfo.swift`** - Configuration options container for all operations\n- **`Sources/General/KingfisherError.swift`** - Error handling system with detailed error types\n- **`Sources/General/KFOptionsSetter.swift`** - Options configuration builder for method chaining\n\n### Image Source and Data Providers\n- **`Sources/General/ImageSource/Resource.swift`** - URL resource definitions and transformations\n- **`Sources/General/ImageSource/Source.swift`** - Abstract image source protocols and implementations\n- **`Sources/General/ImageSource/ImageDataProvider.swift`** - Data provider protocol for custom image sources\n- **`Sources/General/ImageSource/AVAssetImageDataProvider.swift`** - AVAsset image frame extraction\n- **`Sources/General/ImageSource/PHPickerResultImageDataProvider.swift`** - Photo picker integration\n- **`Sources/General/ImageSource/LivePhotoSource.swift`** - Live Photo support implementation\n\n### Network Layer\n- **`Sources/Networking/ImageDownloader.swift`** - HTTP image downloading with session management\n- **`Sources/Networking/ImageDownloader+LivePhoto.swift`** - Live Photo downloading extensions\n- **`Sources/Networking/ImageDownloaderDelegate.swift`** - Download progress and completion delegation\n- **`Sources/Networking/SessionDelegate.swift`** - URLSession delegate handling authentication and redirects\n- **`Sources/Networking/SessionDataTask.swift`** - Custom data task wrapper with cancellation support\n- **`Sources/Networking/ImagePrefetcher.swift`** - Batch image prefetching for performance optimization\n- **`Sources/Networking/RequestModifier.swift`** - HTTP request modification protocols\n- **`Sources/Networking/ImageModifier.swift`** - Response image modification protocols\n- **`Sources/Networking/RedirectHandler.swift`** - HTTP redirect handling strategies\n- **`Sources/Networking/RetryStrategy.swift`** - Failed request retry logic and policies\n- **`Sources/Networking/AuthenticationChallengeResponsable.swift`** - Authentication challenge handling\n- **`Sources/Networking/ImageDataProcessor.swift`** - Raw image data processing pipeline\n\n### Caching System\n- **`Sources/Cache/ImageCache.swift`** - Dual-layer (memory + disk) caching coordinator\n- **`Sources/Cache/MemoryStorage.swift`** - In-memory LRU cache with automatic cleanup\n- **`Sources/Cache/DiskStorage.swift`** - Persistent disk storage with expiration policies\n- **`Sources/Cache/Storage.swift`** - Abstract storage protocols and configurations\n- **`Sources/Cache/CacheSerializer.swift`** - Image serialization for disk persistence\n- **`Sources/Cache/FormatIndicatedCacheSerializer.swift`** - Format-aware image serialization\n\n### Image Processing and Transformation\n- **`Sources/Image/Image.swift`** - Cross-platform image type definitions and utilities\n- **`Sources/Image/ImageProcessor.swift`** - Image transformation and processing pipeline\n- **`Sources/Image/Filter.swift`** - Built-in image filters (blur, tint, overlay, etc.)\n- **`Sources/Image/ImageFormat.swift`** - Image format detection and handling\n- **`Sources/Image/ImageDrawing.swift`** - Core graphics drawing utilities and extensions\n- **`Sources/Image/ImageTransition.swift`** - View transition animations for image loading\n- **`Sources/Image/ImageProgressive.swift`** - Progressive JPEG loading implementation\n- **`Sources/Image/GIFAnimatedImage.swift`** - GIF animation support and playback\n- **`Sources/Image/GraphicsContext.swift`** - Graphics context management and utilities\n- **`Sources/Image/Placeholder.swift`** - Placeholder image definitions and protocols\n\n## Platform Implementation Files\n\n### UIKit Extensions\n- **`Sources/Extensions/ImageView+Kingfisher.swift`** - UIImageView integration with .kf namespace\n- **`Sources/Extensions/UIButton+Kingfisher.swift`** - UIButton background/image loading support\n- **`Sources/Extensions/NSTextAttachment+Kingfisher.swift`** - Text attachment image loading\n\n### AppKit Extensions  \n- **`Sources/Extensions/NSButton+Kingfisher.swift`** - macOS NSButton image loading integration\n- **`Sources/Extensions/HasImageComponent+Kingfisher.swift`** - Generic image component protocol\n\n### Cross-Platform Extensions\n- **`Sources/Extensions/PHLivePhotoView+Kingfisher.swift`** - Live Photo view integration\n- **`Sources/Extensions/CPListItem+Kingfisher.swift`** - CarPlay list item support\n\n### SwiftUI Components\n- **`Sources/SwiftUI/KFImage.swift`** - Main SwiftUI image view component\n- **`Sources/SwiftUI/KFAnimatedImage.swift`** - Animated image support for SwiftUI\n- **`Sources/SwiftUI/KFImageOptions.swift`** - SwiftUI-specific configuration options\n- **`Sources/SwiftUI/KFImageProtocol.swift`** - Shared protocol for KF SwiftUI components\n- **`Sources/SwiftUI/KFImageRenderer.swift`** - SwiftUI view rendering and update logic\n- **`Sources/SwiftUI/ImageBinder.swift`** - Binding layer between SwiftUI and Kingfisher core\n- **`Sources/SwiftUI/ImageContext.swift`** - SwiftUI image loading context management\n\n### Custom Views\n- **`Sources/Views/AnimatedImageView.swift`** - Custom animated image view for GIF playback\n- **`Sources/Views/Indicator.swift`** - Loading indicator views and protocols\n\n## Build System Files\n\n### Package Management\n- **`Package.swift`** - Swift Package Manager manifest with dependencies and targets\n- **`Package@swift-5.9.swift`** - Swift 5.9 compatibility package manifest\n- **`Kingfisher.podspec`** - CocoaPods specification for distribution\n- **`Gemfile`** - Ruby dependencies for Fastlane and build tools\n- **`Gemfile.lock`** - Locked Ruby gem versions for reproducible builds\n\n### Fastlane Build Automation\n- **`fastlane/Fastfile`** - Main Fastlane configuration with lanes for testing, building, and releasing\n- **`fastlane/actions/extract_current_change_log.rb`** - Extracts current version changelog\n- **`fastlane/actions/git_commit_all.rb`** - Git commit automation with custom messages\n- **`fastlane/actions/sync_build_number_to_git.rb`** - Synchronizes build numbers with git commits\n- **`fastlane/actions/update_change_log.rb`** - Automated changelog management\n- **`fastlane/README.md`** - Fastlane documentation and usage instructions\n\n### Xcode Project Files\n- **`Kingfisher.xcodeproj/`** - Main Xcode project with targets and build settings\n- **`Kingfisher.xcworkspace/`** - Xcode workspace for integrated development\n- **`Demo/Kingfisher-Demo.xcodeproj/`** - Demo application Xcode project\n\n## Configuration Files\n\n### Framework Configuration\n- **`Sources/Info.plist`** - Framework bundle information and version metadata\n- **`Sources/PrivacyInfo.xcprivacy`** - Privacy manifest for App Store compliance\n\n### Development Assets\n- **`images/`** - Sample images for testing and demo applications\n  - `kingfisher-1.jpg` through `kingfisher-10.jpg` - Test image assets\n  - `logo.png` - Project logo and branding\n- **`Tests/KingfisherTests/dancing-banana.gif`** - GIF test asset for animation testing\n- **`Tests/KingfisherTests/single-frame.gif`** - Single-frame GIF for edge case testing\n\n### Demo Applications\n- **`Demo/Demo/Kingfisher-Demo/`** - iOS demo app showcasing all features\n  - `ViewControllers/` - Various demo screens (GIF, transitions, processors, etc.)\n  - `SwiftUIViews/` - SwiftUI demonstration views and regression tests\n- **`Demo/Demo/Kingfisher-macOS-Demo/`** - macOS demo application\n- **`Demo/Demo/Kingfisher-tvOS-Demo/`** - Apple TV demo application  \n- **`Demo/Demo/Kingfisher-watchOS-Demo/`** - watchOS demo application\n\n### Git and CI Configuration\n- **`pre-change.yml`** - Pre-commit hook configuration for code quality\n- **`.gitignore`** - Git ignore patterns for build artifacts and dependencies\n\n## Utility and Helper Files\n\n### Core Utilities\n- **`Sources/Utility/ExtensionHelpers.swift`** - Cross-platform compatibility helpers\n- **`Sources/Utility/CallbackQueue.swift`** - Thread-safe callback queue management\n- **`Sources/Utility/Box.swift`** - Reference wrapper for value types\n- **`Sources/Utility/Result.swift`** - Result type utilities and extensions\n- **`Sources/Utility/Delegate.swift`** - Weak delegate wrapper to prevent retain cycles\n- **`Sources/Utility/Runtime.swift`** - Runtime reflection and dynamic dispatch utilities\n- **`Sources/Utility/DisplayLink.swift`** - Cross-platform display link abstraction\n- **`Sources/Utility/SizeExtensions.swift`** - CGSize manipulation and calculations\n- **`Sources/Utility/String+SHA256.swift`** - String hashing for cache key generation\n\n### Test Infrastructure\n- **`Tests/KingfisherTests/KingfisherTestHelper.swift`** - Shared testing utilities and mocks\n- **`Tests/KingfisherTests/Utils/StubHelpers.swift`** - HTTP stubbing helpers for network tests\n- **`Tests/Dependency/Nocilla/`** - HTTP mocking framework for isolated testing\n\n### Documentation System\n- **`Sources/Documentation.docc/`** - DocC documentation bundle\n  - `Documentation.md` - Main documentation entry point\n  - `GettingStarted.md` - Quick start guide for new users\n  - `Tutorials/` - Step-by-step tutorials for UIKit and SwiftUI\n  - `CommonTasks/` - Task-oriented documentation for common use cases\n  - `Topics/` - Advanced topic guides (prefetching, indicators, etc.)\n  - `Resources/` - Documentation assets, images, and code samples\n\n## Reference\n\n### File Organization Patterns\n- **Modular Structure**: Core functionality separated into logical modules (General, Networking, Cache, Image, etc.)\n- **Platform Abstraction**: Cross-platform code in core modules, platform-specific extensions separate\n- **Protocol-Oriented**: Heavy use of protocols for customization and testing\n- **Namespace Pattern**: All public APIs accessed through `.kf` property extension\n\n### Naming Conventions\n- **Files**: PascalCase with descriptive names indicating functionality\n- **Protocols**: Suffix with `-able` for capabilities (e.g., `AuthenticationChallengeResponsable`)\n- **Extensions**: Platform prefix for clarity (e.g., `UIButton+Kingfisher.swift`)\n- **Tests**: Mirror source structure with `Tests` suffix\n\n### Dependency Relationships\n- **Core Dependencies**: Foundation, UIKit/AppKit conditionally imported\n- **SwiftUI Module**: Depends on core modules but isolated for optional usage\n- **Extensions**: Depend on core but platform-specific\n- **Test Dependencies**: Isolated with Nocilla for HTTP mocking\n- **Build Dependencies**: Fastlane for automation, Ruby gems for tooling\n\n### Key Integration Points\n- **KingfisherCompatible Protocol**: Entry point for all functionality via `.kf` namespace\n- **Options System**: Centralized configuration through `KingfisherOptionsInfo`\n- **Result Types**: Consistent error handling with `Result<Success, KingfisherError>`\n- **Callback Queues**: Thread-safe completion handling across all async operations"
  },
  {
    "path": "docs/project-overview.md",
    "content": "<!-- Generated: 2025-06-15 00:00:00 UTC -->\n\n# Kingfisher Project Overview\n\n## Project Purpose\n\nKingfisher is a powerful, pure-Swift library for downloading and caching images from the web. It provides an elegant, asynchronous API for managing remote images in iOS, macOS, tvOS, watchOS, and visionOS applications. The library handles the complete lifecycle of image loading - from network downloading to multi-layer caching (memory and disk), with built-in image processing capabilities and extensive platform-specific UI component integrations.\n\nThe framework follows a modular architecture with clear separation of concerns, allowing developers to use individual components (downloader, cache, processors) independently or as a unified solution. Through its namespace wrapper pattern (`.kf` property) and builder pattern (`KF.url()`), Kingfisher offers both UIKit and SwiftUI support with minimal code overhead.\n\n## Main Entry Points\n\n### Core Configuration\n- **Package.swift** - Swift Package Manager manifest defining library targets and platform requirements\n- **Kingfisher.podspec** - CocoaPods specification (version 8.3.2)\n- **Sources/General/Kingfisher.swift** - Core type definitions and protocol declarations\n- **Sources/General/KingfisherManager.swift** - Central coordinator managing download and cache operations\n\n### Primary APIs\n- **Sources/General/KF.swift** - Builder pattern entry point for fluent API\n- **Sources/Extensions/ImageView+Kingfisher.swift** - UIImageView/NSImageView extensions\n- **Sources/SwiftUI/KFImage.swift** - SwiftUI image component\n- **Sources/SwiftUI/KFAnimatedImage.swift** - SwiftUI animated image support\n\n## Technology Stack\n\n### Core Components\n- **Image Downloading**: `Sources/Networking/ImageDownloader.swift` - URLSession-based networking layer\n- **Cache System**: \n  - `Sources/Cache/ImageCache.swift` - Dual-layer cache coordinator\n  - `Sources/Cache/MemoryStorage.swift` - In-memory cache implementation\n  - `Sources/Cache/DiskStorage.swift` - Persistent disk storage\n- **Image Processing**: `Sources/Image/ImageProcessor.swift` - Transformation pipeline with filters\n- **Format Support**: `Sources/Image/ImageFormat.swift` - Multi-format detection (JPEG, PNG, GIF, WebP)\n\n### Platform Integrations\n- **UIKit Extensions**: `Sources/Extensions/UIButton+Kingfisher.swift`, `Sources/Extensions/NSTextAttachment+Kingfisher.swift`\n- **SwiftUI Components**: `Sources/SwiftUI/KFImageProtocol.swift`, `Sources/SwiftUI/ImageBinder.swift`\n- **Specialized Views**: `Sources/Views/AnimatedImageView.swift`, `Sources/Extensions/PHLivePhotoView+Kingfisher.swift`\n\n### Build & Testing\n- **Fastlane**: `fastlane/Fastfile` - Primary build automation\n- **Test Suite**: `Tests/KingfisherTests/` - XCTest-based unit tests with Nocilla HTTP stubbing\n- **Documentation**: `Sources/Documentation.docc/` - DocC integrated documentation\n\n## Platform Support\n\n### Minimum Requirements\n- **Swift**: 5.9+ (Swift 6 strict concurrency ready)\n- **UIKit/AppKit**: \n  - iOS 13.0+ (`#if os(iOS)`)\n  - macOS 10.15+ (`#if os(macOS)`)\n  - tvOS 13.0+ (`#if os(tvOS)`)\n  - watchOS 6.0+ (`#if os(watchOS)`)\n  - visionOS 1.0+ (`#if os(visionOS)`)\n- **SwiftUI**: iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+\n\n### Platform-Specific Files\n- **macOS**: `Sources/Extensions/NSButton+Kingfisher.swift` - NSButton image loading\n- **iOS/tvOS**: `Sources/Extensions/UIButton+Kingfisher.swift` - UIButton extensions\n- **watchOS**: `Sources/Extensions/WKInterfaceImage+Kingfisher.swift` - WatchKit support\n- **CarPlay**: `Sources/Extensions/CPListItem+Kingfisher.swift` - CarPlay list items (iOS 14.0+)\n- **tvOS**: `Sources/Extensions/TVMonogramView+Kingfisher.swift` - Apple TV monogram views"
  },
  {
    "path": "docs/testing.md",
    "content": "<!-- Generated: 2025-06-15 08:30:00 UTC -->\n\n# Kingfisher Testing Documentation\n\n## Overview\n\nKingfisher's test suite is located in the `Tests/KingfisherTests/` directory and provides comprehensive coverage for all major components of the library. The test suite uses XCTest framework with custom helper utilities and the Nocilla dependency for HTTP request stubbing.\n\n### Test Infrastructure\n\n- **Test Framework**: XCTest\n- **Network Mocking**: Nocilla (located at `Tests/Dependency/Nocilla/`)\n- **Test Helper**: `Tests/KingfisherTests/KingfisherTestHelper.swift`\n- **Stub Utilities**: `Tests/KingfisherTests/Utils/StubHelpers.swift`\n- **Test Assets**: \n  - `Tests/KingfisherTests/dancing-banana.gif` - Animated GIF for testing\n  - `Tests/KingfisherTests/single-frame.gif` - Single frame GIF for testing\n\n## Test Categories\n\n### 1. Core Component Tests\n\n**Cache Layer Tests**\n- `Tests/KingfisherTests/ImageCacheTests.swift` - Tests for the main ImageCache functionality\n- `Tests/KingfisherTests/MemoryStorageTests.swift` - Memory cache specific tests\n- `Tests/KingfisherTests/DiskStorageTests.swift` - Disk storage specific tests\n- `Tests/KingfisherTests/StorageExpirationTests.swift` - Cache expiration policy tests\n\n**Networking Tests**\n- `Tests/KingfisherTests/ImageDownloaderTests.swift` - Image downloading and session management\n- `Tests/KingfisherTests/ImagePrefetcherTests.swift` - Batch image prefetching functionality\n- `Tests/KingfisherTests/DataReceivingSideEffectTests.swift` - Data processing side effects\n\n**Manager Tests**\n- `Tests/KingfisherTests/KingfisherManagerTests.swift` - Central coordinator tests\n- `Tests/KingfisherTests/KingfisherOptionsInfoTests.swift` - Configuration options tests\n\n### 2. Image Processing Tests\n\n- `Tests/KingfisherTests/ImageProcessorTests.swift` - Image transformation pipeline tests\n- `Tests/KingfisherTests/ImageDrawingTests.swift` - Image drawing and rendering tests\n- `Tests/KingfisherTests/ImageExtensionTests.swift` - Core image extensions tests\n- `Tests/KingfisherTests/ImageModifierTests.swift` - Request modifier tests\n\n### 3. UI Integration Tests\n\n**UIKit Tests**\n- `Tests/KingfisherTests/ImageViewExtensionTests.swift` - UIImageView extension tests\n- `Tests/KingfisherTests/UIButtonExtensionTests.swift` - UIButton extension tests\n\n**AppKit Tests**\n- `Tests/KingfisherTests/NSButtonExtensionTests.swift` - NSButton extension tests (macOS)\n\n### 4. Specialized Feature Tests\n\n- `Tests/KingfisherTests/LivePhotoSourceTests.swift` - Live Photo support tests\n- `Tests/KingfisherTests/ImageDataProviderTests.swift` - Custom data provider tests\n- `Tests/KingfisherTests/RetryStrategyTests.swift` - Network retry strategy tests\n- `Tests/KingfisherTests/StringExtensionTests.swift` - String utility extension tests\n\n## Running Tests\n\n### Using Fastlane\n\n```bash\n# Install dependencies first\nbundle install\n\n# Run all tests across all platforms\nbundle exec fastlane tests\n\n# Expected output:\n# [08:30:00]: ------------------------------\n# [08:30:00]: --- Step: default_platform ---\n# [08:30:00]: ------------------------------\n# [08:30:00]: Driving the lane 'ios tests' 🚀\n# [08:30:01]: ------------------\n# [08:30:01]: --- Step: scan ---\n# [08:30:01]: ------------------\n# [08:30:01]: Running Tests: ▸ Touching Kingfisher.framework\n# [08:30:45]: Test Succeeded\n\n# Run tests for specific platform\nbundle exec fastlane test destination:\"platform=iOS Simulator,name=iPhone 15\"\nbundle exec fastlane test destination:\"platform=macOS\"\nbundle exec fastlane test destination:\"platform=tvOS Simulator,name=Apple TV\"\n\n# CI-specific test command (used in continuous integration)\nDESTINATION=\"platform=iOS Simulator,name=iPhone 15,OS=17.5\" bundle exec fastlane test_ci\n\n# Build only (for watchOS where full testing isn't supported)\nbundle exec fastlane build destination:\"platform=watchOS Simulator,name=Apple Watch Series 9 (41mm)\"\n```\n\n### Using Xcode\n\n```bash\n# Open workspace in Xcode\nopen Kingfisher.xcworkspace\n\n# Then use Xcode's test navigator or press Cmd+U to run all tests\n# Or use xcodebuild directly:\nxcodebuild test -workspace Kingfisher.xcworkspace -scheme Kingfisher -destination \"platform=iOS Simulator,name=iPhone 15\"\n```\n\n## Test File Organization Reference\n\n### Directory Structure\n```\nTests/\n├── Dependency/\n│   └── Nocilla/              # HTTP stubbing framework\n│       ├── LICENSE\n│       ├── README.md\n│       └── Nocilla/          # Nocilla source files\n│           ├── Categories/   # NSData and NSString extensions\n│           ├── DSL/          # Domain-specific language for stubbing\n│           ├── Diff/         # Request diff utilities\n│           ├── Hooks/        # HTTP client hooks (NSURLSession, etc.)\n│           ├── Matchers/     # Request matching logic\n│           ├── Model/        # HTTP request/response models\n│           └── Stubs/        # Stub implementation\n└── KingfisherTests/\n    ├── Info.plist\n    ├── KingfisherTestHelper.swift      # Main test helper utilities\n    ├── KingfisherTests-Bridging-Header.h  # Objective-C bridging\n    ├── Utils/\n    │   └── StubHelpers.swift           # Network stubbing helpers\n    ├── dancing-banana.gif              # Animated test image\n    ├── single-frame.gif                # Static test image\n    └── *Tests.swift                    # Individual test files\n```\n\n### Build System Test Targets\n\n**Xcode Scheme**: `Kingfisher.xcscheme`\n- Configured for testing on all supported platforms\n- Includes code coverage collection\n- Uses parallel testing when available\n\n**Fastlane Configuration**: `fastlane/Fastfile`\n- `tests` lane: Runs tests on all platforms\n- `test_ci` lane: CI-specific testing with environment-based destination\n- `test` lane: Core test execution with scan action\n- `build` lane: Build-only verification (used for watchOS)\n\n**Platform Test Destinations**:\n- iOS: `platform=iOS Simulator,name=iPhone 15,OS=17.5`\n- macOS: `platform=macOS`\n- tvOS: `platform=tvOS Simulator,name=Apple TV,OS=17.5`\n- watchOS: `platform=watchOS Simulator,name=Apple Watch Series 9 (41mm),OS=10.5` (build only)\n\n### Test Helper Utilities\n\nThe `KingfisherTestHelper.swift` provides:\n- Pre-encoded test image data in various formats (PNG, JPEG, GIF, HEIC, MOV)\n- Test URLs and keys for stubbing\n- Cache cleanup utilities (`cleanDefaultCache`, `clearCaches`)\n- Image comparison with tolerance (`renderEqual`)\n- Timing utilities (`delay`)\n- Platform-specific test helpers\n\nThe `StubHelpers.swift` provides:\n- `stub()` - Create HTTP response stubs with custom data and headers\n- `delayedStub()` - Create delayed response stubs for timing tests\n- Network error stubbing capabilities"
  },
  {
    "path": "fastlane/Fastfile",
    "content": "fastlane_version \"1.37.0\"\n\ndefault_platform :ios\n\nplatform :ios do\n  desc \"Runs all the tests\"\n  lane :tests do\n    test(destination: \"platform=macOS\")\n    test(destination: \"platform=iOS Simulator,name=iPhone 16,OS=18.5\")\n    test(destination: \"platform=tvOS Simulator,name=Apple TV,OS=18.5\")\n    build(destination: \"platform=watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.5\")\n  end\n    \n  lane :test_ci do\n    if ENV[\"DESTINATION\"].include? \"watchOS\" then\n        build(destination: ENV[\"DESTINATION\"])\n    else\n        test(destination: ENV[\"DESTINATION\"])\n    end\n  end\n\n  lane :build_ci do\n    build(destination: ENV[\"DESTINATION\"])\n  end\n\n  lane :test do |options|\n    scan(\n      scheme: \"Kingfisher\", \n      clean: true, \n      xcargs: \"SWIFT_VERSION=5.0\",\n      destination: options[:destination]\n    )\n  end\n\n  lane :build do |options|\n    xcodebuild(\n      workspace: \"Kingfisher.xcworkspace\",\n      configuration: \"Debug\",\n      scheme: \"Kingfisher\",\n      destination: options[:destination],\n      build: true,\n      build_settings: {\n        \"SWIFT_VERSION\" => \"5.0\"\n      }\n    )\n  end\n\n  desc \"Lint\"\n  lane :lint do\n    pod_lib_lint\n    spm\n  end\n  \n  desc \"Release new version\"\n  lane :release do |options|\n    target_version = options[:version]\n    raise \"The version is missed. Use `fastlane release version:{version_number}`.`\" if target_version.nil?\n    \n    ensure_git_branch\n    ensure_git_status_clean\n    \n    skip_tests =  options[:skip_tests]\n    tests unless skip_tests\n    \n    lint\n\n    sync_build_number_to_git\n    increment_version_number(version_number: target_version)\n    version_bump_podspec(path: \"Kingfisher.podspec\", version_number: target_version)\n    \n    log = extract_current_change_log(version: target_version)\n    release_log = update_change_log(log: log)\n    \n    git_commit_all(message: \"Bump version to #{target_version}\")\n    \n    Actions.sh(\"git tag -s #{target_version} -m ''\")\n    \n    push_to_git_remote\n    \n    xcframework(version: target_version)\n    set_github_release(\n      repository_name: \"onevcat/Kingfisher\",\n      api_token: ENV['GITHUB_TOKEN'],\n      name: release_log[:title],\n      tag_name: target_version,\n      description: release_log[:text],\n      upload_assets: [\n        \"build/Kingfisher-#{target_version}.xcframework.zip\",\n        \"build/Kingfisher-iOS-#{target_version}.xcframework.zip\"\n      ]\n    )\n    \n    pod_push\n  end\n\n  lane :xcframework do |options|\n    version = options[:version]\n    swift_version = options[:swift_version] || \"5.0\"\n    xcode_version = options[:xcode_version] || \"26.2.0\"\n\n    xcodes(version: xcode_version, select_for_current_build_only: true)\n    FileUtils.rm_rf '../build'\n\n    # Define platform to SDKs mapping\n    PLATFORM_SDKS = {\n      all: [\n        \"macosx\",\n        \"iphoneos\", \"iphonesimulator\",\n        \"appletvos\", \"appletvsimulator\",\n        \"watchos\", \"watchsimulator\",\n        \"xros\", \"xrsimulator\"\n      ],\n      ios: [\"iphoneos\", \"iphonesimulator\"]\n    }\n\n    def create_archives(sdks, swift_version)\n      frameworks = {}\n      sdks.each do |sdk|\n        archive_path = \"build/Kingfisher-#{sdk}.xcarchive\"\n        xcodebuild(\n          archive: true,\n          archive_path: archive_path,\n          scheme: \"Kingfisher\",\n          sdk: sdk,\n          build_settings: {\n            \"BUILD_LIBRARY_FOR_DISTRIBUTION\" => \"YES\",\n            \"SKIP_INSTALL\" => \"NO\",\n            \"SWIFT_VERSION\" => swift_version\n          }\n        )\n\n        framework_path = \"#{archive_path}/Products/Library/Frameworks/Kingfisher.framework\"\n        dsym_path = \"#{Dir.pwd}/../#{archive_path}/dSYMs/Kingfisher.framework.dSYM\"\n        frameworks[framework_path] = { dsyms: dsym_path }\n      end\n      frameworks\n    end\n\n    def create_and_package_xcframework(frameworks, output_name, version)\n      output_base_name = if output_name.empty?\n        \"Kingfisher-#{version}\"\n      else\n        \"Kingfisher-#{output_name}-#{version}\"\n      end\n      \n      output_xcframework_path = \"build/#{output_base_name}/Kingfisher.xcframework\"\n\n      create_xcframework(\n        frameworks_with_dsyms: frameworks,\n        output: output_xcframework_path\n      )\n\n      Actions.sh(\"codesign --timestamp -v --sign 'Apple Distribution: Wei Wang (A4YJ9MRZ66)' ../build/#{output_base_name}/Kingfisher.xcframework\")\n\n      zip(\n        path: output_xcframework_path,\n        output_path: \"build/#{output_base_name}.xcframework.zip\",\n        symlinks: true\n      )\n    end\n\n    # Create full platform xcframework\n    all_frameworks = create_archives(PLATFORM_SDKS[:all], swift_version)\n    create_and_package_xcframework(all_frameworks, \"\", version)\n\n    # Create iOS only xcframework\n    ios_frameworks = create_archives(PLATFORM_SDKS[:ios], swift_version)\n    create_and_package_xcframework(ios_frameworks, \"iOS\", version)\n  end\n\n  before_all do |lane|\n    xcode_version = ENV[\"XCODE_VERSION\"] || \"26.2.0\"\n    xcodes(version: xcode_version, select_for_current_build_only: true)\n  end\n\n  after_all do |lane|\n  \n  end\n\n  error do |lane, exception|\n  \n  end\nend\n"
  },
  {
    "path": "fastlane/actions/extract_current_change_log.rb",
    "content": "module Fastlane\n  module Actions\n    class ExtractCurrentChangeLogAction < Action\n      require 'yaml'\n      def self.run(params)\n        yaml = File.read(params[:file])\n        data = YAML.load(yaml)\n        version = data[\"version\"]\n        raise \"The version should match in the input file\".red unless (version and version == params[:version])\n\n        title = \"#{version}\"\n        title = title + \" - #{data[\"name\"]}\" if (data[\"name\"] and not data[\"name\"].empty?)\n\n        return {:title => title, :version => version, :add => data[\"add\"], :fix => data[\"fix\"], :remove => data[\"remove\"]}\n      end\n\n      #####################################################\n      # @!group Documentation\n      #####################################################\n\n      def self.description\n        \"Extract change log information for a specified version.\"\n      end\n\n      def self.details\n        \"This action will check input version and change log. If everything goes well, the change log info will be returned.\"\n      end\n\n      def self.available_options\n        [\n          FastlaneCore::ConfigItem.new(key: :version,\n                                       env_name: \"KF_EXTRACT_CURRENT_CHANGE_LOG_VERSION\",\n                                       description: \"The target version which is needed to be extract\",\n                                       verify_block: proc do |value|\n                                          raise \"No version number is given, pass using `version: 'version_number'`\".red unless (value and not value.empty?)\n                                       end),\n          FastlaneCore::ConfigItem.new(key: :file,\n                                       env_name: \"KF_EXTRACT_CURRENT_CHANGE_LOG_PRECHANGE_FILE\",\n                                       description: \"Create a development certificate instead of a distribution one\",\n                                       default_value: \"pre-change.yml\")\n        ]\n      end\n\n      def self.return_value\n        \"An object contains change log infomation. {version: }\"\n      end\n\n      def self.is_supported?(platform)\n        true\n      end\n\n      def self.authors\n        [\"onevcat\"]\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "fastlane/actions/git_commit_all.rb",
    "content": "module Fastlane\n  module Actions\n    class GitCommitAllAction < Action\n      def self.run(params)\n          Action.sh \"git add -A\"\n          Actions.sh \"git commit -am \\\"#{params[:message]}\\\"\"\n      end\n\n      #####################################################\n      # @!group Documentation\n      #####################################################\n\n      def self.description\n        \"Commit all unsaved changes to git.\"\n      end\n\n      def self.available_options\n        [\n          FastlaneCore::ConfigItem.new(key: :message,\n                                       env_name: \"FL_GIT_COMMIT_ALL\",\n                                       description: \"The git message for the commit\",\n                                       is_string: true)\n        ]\n      end\n\n      def self.authors\n        # So no one will ever forget your contribution to fastlane :) You are awesome btw!\n        [\"onevcat\"]\n      end\n\n      def self.is_supported?(platform)\n        true\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "fastlane/actions/sync_build_number_to_git.rb",
    "content": "module Fastlane\n  module Actions\n    module SharedValues\n      KF_BUILD_NUMBER = :BUILD_NUMBER\n    end\n    class SyncBuildNumberToGitAction < Action\n      def self.is_git?\n        Actions.sh 'git rev-parse HEAD'\n        return true\n      rescue\n        return false\n      end\n        \n      def self.run(params)\n        if is_git?\n          command = 'git rev-list HEAD --count'\n        else\n          raise \"Not in a git repository.\"\n        end\n      build_number = (Actions.sh command).strip\n      Fastlane::Actions::IncrementBuildNumberAction.run(build_number: build_number)\n      Actions.lane_context[SharedValues::KF_BUILD_NUMBER] = build_number\n      end\n\n      def self.output\n        [\n          ['KF_BUILD_NUMBER', 'The new build number']\n        ]\n      end\n      #####################################################\n      # @!group Documentation\n      #####################################################\n\n      def self.description\n        \"Set the build version of your project to the same number of your total git commit count\"\n      end\n\n      def self.authors\n        [\"onevcat\"]\n      end\n\n      def self.is_supported?(platform)\n        [:ios, :mac].include? platform\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "fastlane/actions/update_change_log.rb",
    "content": "module Fastlane\n  module Actions\n    class UpdateChangeLogAction < Action\n      def self.run(params)\n          log = params[:log]\n          raise \"Invalid log object\".red unless !log[:title].empty? and !log[:version].empty?\n\n          readme = File.read(params[:changelogfile])\n          log_text = \"## [#{log[:title]}](https://github.com/onevcat/Kingfisher/releases/tag/#{log[:version]}) (#{Time.now.strftime(\"%Y-%m-%d\")})\\n\\n\"\n\n          des = \"\"\n          add = log[:add].map { |i| \"* #{i}\" }.join(\"\\n\") unless log[:add].nil?\n          des = des + \"#### Add\\n#{add}\\n\\n\" unless add.nil? or add.empty?\n\n          fix = log[:fix].map { |i| \"* #{i}\" }.join(\"\\n\") unless log[:fix].nil?\n          des = des + \"#### Fix\\n#{fix}\\n\\n\" unless fix.nil? or fix.empty?\n          \n          remove = log[:remove].map { |i| \"* #{i}\" }.join(\"\\n\") unless log[:remove].nil?\n          des = des + \"#### Remove\\n#{remove}\\n\\n\" unless remove.nil? or remove.empty?\n\n          log_text = log_text + des\n\n          File.open(params[:changelogfile], 'w') { |file| file.write(readme.sub(\"-----\", \"-----\\n\\n#{log_text}---\")) }\n\n          return {:title => log[:title], :text => des}\n      end\n\n      #####################################################\n      # @!group Documentation\n      #####################################################\n\n      def self.description\n        \"Update the change log file with the content of log\"\n      end\n\n      def self.details\n        \"Generally speaking, the log is return value of extract_current_change_log action\"\n      end\n\n      def self.available_options\n        [\n          FastlaneCore::ConfigItem.new(key: :log,\n                                       env_name: \"KF_UPDATE_CHANGE_LOG_LOG\",\n                                       description: \"Change log extracted by pre change log file\",\n                                       is_string: false\n                                       ),\n          FastlaneCore::ConfigItem.new(key: :changelogfile,\n                                       env_name: \"KF_UPDATE_CHANGE_LOG_CHANGE_LOG_FILE\",\n                                       description: \"The change log file, if not set, CHANGELOG.md will be used\",\n                                       default_value: \"CHANGELOG.md\")\n        ]\n      end\n\n      def self.authors\n        [\"onevcat\"]\n      end\n\n      def self.is_supported?(platform)\n        true\n      end\n    end\n  end\nend\n"
  }
]