Repository: onevcat/Kingfisher Branch: master Commit: 3fe88ce2de62 Files: 332 Total size: 2.0 MB Directory structure: gitextract_n_piy7rv/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE.md │ └── workflows/ │ ├── build.yaml │ └── test.yaml ├── .gitignore ├── .ruby-version ├── .spi.yml ├── AGENTS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Demo/ │ ├── Demo/ │ │ ├── Kingfisher-Demo/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Base.lproj/ │ │ │ │ └── Main.storyboard │ │ │ ├── Extensions/ │ │ │ │ └── UIViewController+KingfisherOperation.swift │ │ │ ├── Images.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ ├── LaunchScreen.storyboard │ │ │ ├── Resources/ │ │ │ │ └── ImageLoader.swift │ │ │ ├── SwiftUIViews/ │ │ │ │ ├── AnimatedImageDemo.swift │ │ │ │ ├── GeometryReaderDemo.swift │ │ │ │ ├── GridDemo.swift │ │ │ │ ├── LazyVStackDemo.swift │ │ │ │ ├── ListDemo.swift │ │ │ │ ├── LoadTransitionDemo.swift │ │ │ │ ├── LoadingFailureDemo.swift │ │ │ │ ├── MainView.swift │ │ │ │ ├── PhotosPickerDemo.swift │ │ │ │ ├── ProgressiveJPEGDemo.swift │ │ │ │ ├── Regression/ │ │ │ │ │ ├── Issue1998View.swift │ │ │ │ │ ├── Issue2035View.swift │ │ │ │ │ ├── Issue2295View.swift │ │ │ │ │ └── Issue2352View.swift │ │ │ │ ├── SingleViewDemo.swift │ │ │ │ ├── SizingAnimationDemo.swift │ │ │ │ └── TransitionViewDemo.swift │ │ │ └── ViewControllers/ │ │ │ ├── AVAssetImageGeneratorViewController.swift │ │ │ ├── AutoSizingTableViewController.swift │ │ │ ├── DetailImageViewController.swift │ │ │ ├── GIFHeavyViewController.swift │ │ │ ├── GIFViewController.swift │ │ │ ├── HighResolutionCollectionViewController.swift │ │ │ ├── ImageCollectionViewCell.swift │ │ │ ├── ImageDataProviderCollectionViewController.swift │ │ │ ├── IndicatorCollectionViewController.swift │ │ │ ├── InfinityCollectionViewController.swift │ │ │ ├── LivePhotoViewController.swift │ │ │ ├── MainViewController.swift │ │ │ ├── NetworkMetricsViewController.swift │ │ │ ├── NormalLoadingViewController.swift │ │ │ ├── OrientationImagesViewController.swift │ │ │ ├── PHPickerResultViewController.swift │ │ │ ├── ProcessorCollectionViewController.swift │ │ │ ├── ProgressiveJPEGViewController.swift │ │ │ ├── SwiftUIViewController.swift │ │ │ ├── TextAttachmentViewController.swift │ │ │ └── TransitionViewController.swift │ │ ├── Kingfisher-macOS-Demo/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── Main.storyboard │ │ │ ├── Cell.xib │ │ │ ├── GIFHeavyViewController.swift │ │ │ ├── Info.plist │ │ │ ├── SwiftUIViewController.swift │ │ │ └── ViewController.swift │ │ ├── Kingfisher-tvOS-Demo/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── App Icon & Top Shelf Image.brandassets/ │ │ │ │ │ ├── App Icon - Large.imagestack/ │ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── App Icon - Small.imagestack/ │ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Top Shelf Image.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ └── LaunchImage.launchimage/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── Main.storyboard │ │ │ └── Info.plist │ │ ├── Kingfisher-watchOS-Demo/ │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── Interface.storyboard │ │ │ └── Info.plist │ │ └── Kingfisher-watchOS-Demo Extension/ │ │ ├── Assets.xcassets/ │ │ │ └── README__ignoredByTemplate__ │ │ ├── ExtensionDelegate.swift │ │ ├── Info.plist │ │ └── InterfaceController.swift │ ├── Kingfisher-Demo.entitlements │ └── Kingfisher-Demo.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ └── Kingfisher-Demo.xcscheme ├── Gemfile ├── Kingfisher.json ├── Kingfisher.podspec ├── Kingfisher.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ ├── xcbaselines/ │ │ └── D1ED2D3E1AD2D09F00CFC3EB.xcbaseline/ │ │ ├── 74237B0B-7981-4A24-B6C4-95F4A5E7727F.plist │ │ └── Info.plist │ └── xcschemes/ │ └── Kingfisher.xcscheme ├── Kingfisher.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ ├── IDETemplateMacros.plist │ ├── IDEWorkspaceChecks.plist │ ├── Kingfisher.xcscmblueprint │ └── WorkspaceSettings.xcsettings ├── LICENSE ├── Package.swift ├── Package@swift-5.9.swift ├── README-LLM.md ├── README.md ├── Sources/ │ ├── Cache/ │ │ ├── CacheSerializer.swift │ │ ├── DiskStorage.swift │ │ ├── FormatIndicatedCacheSerializer.swift │ │ ├── ImageCache.swift │ │ ├── MemoryStorage.swift │ │ └── Storage.swift │ ├── Documentation.docc/ │ │ ├── CommonTasks/ │ │ │ ├── CommonTasks.md │ │ │ ├── CommonTasks_Cache.md │ │ │ ├── CommonTasks_Downloader.md │ │ │ ├── CommonTasks_Processor.md │ │ │ └── CommonTasks_Serializer.md │ │ ├── Documentation.md │ │ ├── GettingStarted.md │ │ ├── MigrationGuide/ │ │ │ ├── Migration-To-6.md │ │ │ ├── Migration-To-7.md │ │ │ └── Migration-To-8.md │ │ ├── MigrationGuide.md │ │ ├── Resources/ │ │ │ └── code-files/ │ │ │ ├── 01-SampleCell-1.swift │ │ │ ├── 01-SampleCell-2.swift │ │ │ ├── 01-SampleCell-3.swift │ │ │ ├── 01-ViewController-1.swift │ │ │ ├── 01-ViewController-10.swift │ │ │ ├── 01-ViewController-11.swift │ │ │ ├── 01-ViewController-12.swift │ │ │ ├── 01-ViewController-13.swift │ │ │ ├── 01-ViewController-2.swift │ │ │ ├── 01-ViewController-3.swift │ │ │ ├── 01-ViewController-4.swift │ │ │ ├── 01-ViewController-5.swift │ │ │ ├── 01-ViewController-6-0.swift │ │ │ ├── 01-ViewController-6.swift │ │ │ ├── 01-ViewController-7.swift │ │ │ ├── 01-ViewController-8.swift │ │ │ ├── 01-ViewController-9.swift │ │ │ ├── 02-ContentView-1.swift │ │ │ ├── 02-ContentView-10.swift │ │ │ ├── 02-ContentView-11.swift │ │ │ ├── 02-ContentView-2.swift │ │ │ ├── 02-ContentView-3.swift │ │ │ ├── 02-ContentView-4.swift │ │ │ ├── 02-ContentView-5.swift │ │ │ ├── 02-ContentView-6.swift │ │ │ ├── 02-ContentView-7.swift │ │ │ ├── 02-ContentView-8.swift │ │ │ └── 02-ContentView-9.swift │ │ ├── Topics/ │ │ │ ├── Topic_ImageDataProvider.md │ │ │ ├── Topic_Indicator.md │ │ │ ├── Topic_LivePhoto.md │ │ │ ├── Topic_LowDataMode.md │ │ │ ├── Topic_PerformanceTips.md │ │ │ ├── Topic_Prefetch.md │ │ │ └── Topic_Retry.md │ │ └── Tutorials/ │ │ ├── GettingStartedSwiftUI.tutorial │ │ ├── GettingStartedUIKit.tutorial │ │ └── Tutorials.tutorial │ ├── Extensions/ │ │ ├── CPListItem+Kingfisher.swift │ │ ├── HasImageComponent+Kingfisher.swift │ │ ├── ImageView+Kingfisher.swift │ │ ├── NSButton+Kingfisher.swift │ │ ├── NSTextAttachment+Kingfisher.swift │ │ ├── PHLivePhotoView+Kingfisher.swift │ │ └── UIButton+Kingfisher.swift │ ├── General/ │ │ ├── ImageSource/ │ │ │ ├── AVAssetImageDataProvider.swift │ │ │ ├── ImageDataProvider.swift │ │ │ ├── LivePhotoSource.swift │ │ │ ├── PHPickerResultImageDataProvider.swift │ │ │ ├── PhotosPickerItemImageDataProvider.swift │ │ │ ├── Resource.swift │ │ │ └── Source.swift │ │ ├── KF.swift │ │ ├── KFOptionsSetter.swift │ │ ├── Kingfisher.swift │ │ ├── KingfisherError.swift │ │ ├── KingfisherManager+LivePhoto.swift │ │ ├── KingfisherManager.swift │ │ └── KingfisherOptionsInfo.swift │ ├── Image/ │ │ ├── Filter.swift │ │ ├── GIFAnimatedImage.swift │ │ ├── GraphicsContext.swift │ │ ├── Image.swift │ │ ├── ImageDrawing.swift │ │ ├── ImageFormat.swift │ │ ├── ImageProcessor.swift │ │ ├── ImageProgressive.swift │ │ ├── ImageTransition.swift │ │ └── Placeholder.swift │ ├── Info.plist │ ├── Networking/ │ │ ├── AuthenticationChallengeResponsable.swift │ │ ├── ImageDataProcessor.swift │ │ ├── ImageDownloader+LivePhoto.swift │ │ ├── ImageDownloader.swift │ │ ├── ImageDownloaderDelegate.swift │ │ ├── ImageModifier.swift │ │ ├── ImagePrefetcher.swift │ │ ├── NetworkMetrics.swift │ │ ├── NetworkMonitor.swift │ │ ├── RedirectHandler.swift │ │ ├── RequestModifier.swift │ │ ├── RetryStrategy.swift │ │ ├── SessionDataTask.swift │ │ └── SessionDelegate.swift │ ├── PrivacyInfo.xcprivacy │ ├── SwiftUI/ │ │ ├── ImageBinder.swift │ │ ├── ImageContext.swift │ │ ├── KFAnimatedImage.swift │ │ ├── KFImage.swift │ │ ├── KFImageOptions.swift │ │ ├── KFImageProtocol.swift │ │ └── KFImageRenderer.swift │ ├── Utility/ │ │ ├── Box.swift │ │ ├── CallbackQueue.swift │ │ ├── Delegate.swift │ │ ├── DisplayLink.swift │ │ ├── ExtensionHelpers.swift │ │ ├── Result.swift │ │ ├── Runtime.swift │ │ ├── SizeExtensions.swift │ │ └── String+SHA256.swift │ └── Views/ │ ├── AnimatedImageView.swift │ └── Indicator.swift ├── Tests/ │ ├── Dependency/ │ │ └── Nocilla/ │ │ ├── LICENSE │ │ ├── Nocilla/ │ │ │ ├── Categories/ │ │ │ │ ├── NSData+Nocilla.h │ │ │ │ ├── NSData+Nocilla.m │ │ │ │ ├── NSString+Nocilla.h │ │ │ │ └── NSString+Nocilla.m │ │ │ ├── DSL/ │ │ │ │ ├── LSHTTPRequestDSLRepresentation.h │ │ │ │ ├── LSHTTPRequestDSLRepresentation.m │ │ │ │ ├── LSStubRequestDSL.h │ │ │ │ ├── LSStubRequestDSL.m │ │ │ │ ├── LSStubResponseDSL.h │ │ │ │ └── LSStubResponseDSL.m │ │ │ ├── Diff/ │ │ │ │ ├── LSHTTPRequestDiff.h │ │ │ │ └── LSHTTPRequestDiff.m │ │ │ ├── Hooks/ │ │ │ │ ├── ASIHTTPRequest/ │ │ │ │ │ ├── ASIHTTPRequestStub.h │ │ │ │ │ ├── ASIHTTPRequestStub.m │ │ │ │ │ ├── LSASIHTTPRequestAdapter.h │ │ │ │ │ ├── LSASIHTTPRequestAdapter.m │ │ │ │ │ ├── LSASIHTTPRequestHook.h │ │ │ │ │ └── LSASIHTTPRequestHook.m │ │ │ │ ├── LSHTTPClientHook.h │ │ │ │ ├── LSHTTPClientHook.m │ │ │ │ ├── NSURLRequest/ │ │ │ │ │ ├── LSHTTPStubURLProtocol.h │ │ │ │ │ ├── LSHTTPStubURLProtocol.m │ │ │ │ │ ├── LSNSURLHook.h │ │ │ │ │ ├── LSNSURLHook.m │ │ │ │ │ ├── NSURLRequest+DSL.h │ │ │ │ │ ├── NSURLRequest+DSL.m │ │ │ │ │ ├── NSURLRequest+LSHTTPRequest.h │ │ │ │ │ └── NSURLRequest+LSHTTPRequest.m │ │ │ │ └── NSURLSession/ │ │ │ │ ├── LSNSURLSessionHook.h │ │ │ │ └── LSNSURLSessionHook.m │ │ │ ├── LSNocilla.h │ │ │ ├── LSNocilla.m │ │ │ ├── Matchers/ │ │ │ │ ├── LSDataMatcher.h │ │ │ │ ├── LSDataMatcher.m │ │ │ │ ├── LSMatcheable.h │ │ │ │ ├── LSMatcher.h │ │ │ │ ├── LSMatcher.m │ │ │ │ ├── LSRegexMatcher.h │ │ │ │ ├── LSRegexMatcher.m │ │ │ │ ├── LSStringMatcher.h │ │ │ │ ├── LSStringMatcher.m │ │ │ │ ├── NSData+Matcheable.h │ │ │ │ ├── NSData+Matcheable.m │ │ │ │ ├── NSRegularExpression+Matcheable.h │ │ │ │ ├── NSRegularExpression+Matcheable.m │ │ │ │ ├── NSString+Matcheable.h │ │ │ │ └── NSString+Matcheable.m │ │ │ ├── Model/ │ │ │ │ ├── LSHTTPBody.h │ │ │ │ ├── LSHTTPRequest.h │ │ │ │ └── LSHTTPResponse.h │ │ │ ├── Nocilla.h │ │ │ └── Stubs/ │ │ │ ├── LSStubRequest.h │ │ │ ├── LSStubRequest.m │ │ │ ├── LSStubResponse.h │ │ │ └── LSStubResponse.m │ │ └── README.md │ └── KingfisherTests/ │ ├── DataReceivingSideEffectTests.swift │ ├── DiskStorageTests.swift │ ├── ImageCacheTests.swift │ ├── ImageDataProviderTests.swift │ ├── ImageDownloaderTests.swift │ ├── ImageDrawingTests.swift │ ├── ImageExtensionTests.swift │ ├── ImageModifierTests.swift │ ├── ImagePrefetcherTests.swift │ ├── ImageProcessorTests.swift │ ├── ImageViewExtensionTests.swift │ ├── Info.plist │ ├── KingfisherManagerTests.swift │ ├── KingfisherOptionsInfoTests.swift │ ├── KingfisherTestHelper.swift │ ├── KingfisherTests-Bridging-Header.h │ ├── LivePhotoSourceTests.swift │ ├── MemoryStorageTests.swift │ ├── NSButtonExtensionTests.swift │ ├── PixelFormatDecodingTests.swift │ ├── PixelFormats/ │ │ ├── gradient-10b-displayp3-alpha.heic │ │ ├── gradient-10b-srgb-alpha.heic │ │ └── gradient-10b-srgb-opaque.heic │ ├── RetryStrategyTests.swift │ ├── StorageExpirationTests.swift │ ├── StringExtensionTests.swift │ ├── UIButtonExtensionTests.swift │ └── Utils/ │ └── StubHelpers.swift ├── docs/ │ ├── architecture.md │ ├── build-system.md │ ├── deployment.md │ ├── development.md │ ├── files.md │ ├── project-overview.md │ └── testing.md └── fastlane/ ├── Fastfile └── actions/ ├── extract_current_change_log.rb ├── git_commit_all.rb ├── sync_build_number_to_git.rb └── update_change_log.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: onevcat open_collective: kingfisher ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Check List Thanks for considering to open an issue. Before you submit your issue, please confirm these boxes are checked. - [ ] 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. - [ ] I have searched in [existing issues](https://github.com/onevcat/Kingfisher/issues?utf8=✓&q=is%3Aissue), but did not find a same one. - [ ] 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. ### Issue Description #### What [Tell us about the issue] #### Reproduce [The steps to reproduce this issue. What is the url you were trying to load, where did you put your code, etc.] #### Other Comment [Add anything else here] ================================================ FILE: .github/workflows/build.yaml ================================================ name: build defaults: run: shell: bash -leo pipefail {0} on: push: branches: - master pull_request: types: [opened, synchronize, reopened] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-framework: name: build (Xcode ${{ matrix.xcode }}, ${{ matrix.label }}) runs-on: self-hosted strategy: matrix: include: - xcode: '16.2' destination: 'macOS' label: 'macOS' - xcode: '16.2' destination: 'iOS Simulator,name=iPhone 16,OS=18.2' label: 'iOS 18.2' - xcode: '16.2' destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=18.2' label: 'tvOS 18.2' - xcode: '16.2' destination: 'watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.2' label: 'watchOS 11.2' - xcode: '16.3' destination: 'macOS' label: 'macOS' - xcode: '16.3' destination: 'iOS Simulator,name=iPhone 16,OS=18.4' label: 'iOS 18.4' - xcode: '16.3' destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=18.4' label: 'tvOS 18.4' - xcode: '16.3' destination: 'watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.4' label: 'watchOS 11.4' - xcode: '26.0.1' destination: 'macOS' label: 'macOS' - xcode: '26.0.1' destination: 'iOS Simulator,name=iPhone 17,OS=26.0.1' label: 'iOS 26.0.1' - xcode: '26.0.1' destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.0' label: 'tvOS 26.0' - xcode: '26.0.1' destination: 'watchOS Simulator,name=Apple Watch Series 11 (42mm),OS=26.0' label: 'watchOS 26.0' - xcode: '26.1.1' destination: 'macOS' label: 'macOS' - xcode: '26.1.1' destination: 'iOS Simulator,name=iPhone 17,OS=26.1' label: 'iOS 26.1' - xcode: '26.1.1' destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.1' label: 'tvOS 26.1' - xcode: '26.1.1' destination: 'watchOS Simulator,name=Apple Watch Series 11 (42mm),OS=26.1' label: 'watchOS 26.1' - xcode: '26.2' destination: 'macOS' label: 'macOS' - xcode: '26.2' destination: 'iOS Simulator,name=iPhone 17,OS=26.2' label: 'iOS 26.2' - xcode: '26.2' destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.2' label: 'tvOS 26.2' - xcode: '26.2' destination: 'watchOS Simulator,name=Apple Watch Series 11 (42mm),OS=26.2' label: 'watchOS 26.2' steps: - uses: actions/checkout@v4 - name: Install Gems run: bundle install - name: Build framework env: DESTINATION: platform=${{ matrix.destination }} XCODE_VERSION: ${{ matrix.xcode }} FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: '60' FASTLANE_XCODEBUILD_SETTINGS_RETRIES: '4' run: bundle exec fastlane build_ci ================================================ FILE: .github/workflows/test.yaml ================================================ name: test defaults: run: shell: bash -leo pipefail {0} on: push: branches: - master pull_request: types: [opened, synchronize, reopened] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: run-tests: name: test (Xcode ${{ matrix.xcode }}, ${{ matrix.label }}) runs-on: self-hosted strategy: matrix: include: - xcode: '16.4' destination: 'macOS' label: 'macOS' - xcode: '16.4' destination: 'iOS Simulator,name=iPhone 16,OS=18.5' label: 'iOS 18.5' - xcode: '16.4' destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=18.5' label: 'tvOS 18.5' - xcode: '16.4' destination: 'watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.5' label: 'watchOS 11.5' - xcode: '26.3' destination: 'macOS' label: 'macOS' - xcode: '26.3' destination: 'iOS Simulator,name=iPhone 17,OS=26.2' label: 'iOS 26.2' - xcode: '26.3' destination: 'tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.2' label: 'tvOS 26.2' - xcode: '26.3' destination: 'watchOS Simulator,name=Apple Watch Series 11 (42mm),OS=26.2' label: 'watchOS 26.2' steps: - uses: actions/checkout@v4 - name: Install Gems run: bundle install - name: Run tests env: DESTINATION: platform=${{ matrix.destination }} XCODE_VERSION: ${{ matrix.xcode }} FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: '60' FASTLANE_XCODEBUILD_SETTINGS_RETRIES: '4' run: bundle exec fastlane test_ci ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io # Xcode build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.xcuserstate *.hmap *.ipa .swiftpm # AppCode .idea # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control # # Pods/ # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build Kingfisher.framework.zip # macOS .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk images/logo.sketch # fastlane specific fastlane/report.xml # deliver temporary files fastlane/Preview.html # snapshot generated screenshots fastlane/screenshots/**/*.png fastlane/screenshots/screenshots.html # scan temporary files fastlane/test_output test_output fastlane/.env pre-change.yml .build fastlane/README.md /Kingfisher-TestImages .bundle/ vendor/ .vscode/settings.json # Claude local settings .claude/ .claude/settings.local.json .claude/CLAUDE.local.md .claude/*.local.* .codex/ # xcode-build-server files buildServer.json .compile ================================================ FILE: .ruby-version ================================================ 3.3.6 ================================================ FILE: .spi.yml ================================================ version: 1 builder: configs: - documentation_targets: [Kingfisher] ================================================ FILE: AGENTS.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Documentation You 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. There 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. ## Build and Development Commands ### Primary Build System This project uses **Fastlane** for primary build automation. Key commands: ```bash # Install dependencies bundle install # Run all tests across platforms (iOS, macOS, tvOS, watchOS) bundle exec fastlane tests # Run specific platform tests bundle exec fastlane test destination:"platform=iOS Simulator,name=iPhone 16" # Build for specific platform bundle exec fastlane build destination:"platform=iOS Simulator,name=iPhone 16" # Lint CocoaPods spec and Swift Package Manager bundle exec fastlane lint ``` ### Release Process ```bash # Full release workflow (tests, linting, versioning, GitHub release, CocoaPods push) bundle exec fastlane release version:X.X.X ``` ## Architecture Overview @docs/architecture.md Kingfisher is a modular image loading and caching library with clear separation of concerns: ### Core Components Flow 1. **KingfisherManager** (`Sources/General/KingfisherManager.swift`) - Central coordinator 2. **ImageDownloader** (`Sources/Networking/ImageDownloader.swift`) - Network layer 3. **ImageCache** (`Sources/Cache/ImageCache.swift`) - Dual-layer caching (memory + disk) 4. **ImageProcessor** (`Sources/Image/ImageProcessor.swift`) - Image transformation pipeline ### Key Architectural Patterns - **Protocol-oriented design** with `KingfisherCompatible` protocol - **Namespace wrapper pattern** - All functionality accessed via `.kf` property - **Builder pattern** - `KF.url()...` method chaining - **Options pattern** - `KingfisherOptionsInfo` for configuration ### Module Structure ``` Sources/ ├── General/ # Core managers, options, data providers ├── Networking/ # Download, prefetch, session management ├── Cache/ # Multi-layer caching system ├── Image/ # Processing, filters, formats, transitions ├── Extensions/ # UIKit/AppKit/SwiftUI integration ├── SwiftUI/ # SwiftUI-specific components ├── Utility/ # Helper utilities and extensions └── Views/ # Custom UI components ``` ### Integration Points - **UIKit**: Extensions for `UIImageView`, `UIButton` via `.kf` namespace - **SwiftUI**: `KFImage` and `KFAnimatedImage` components - **Cross-platform**: Extensive conditional compilation for iOS/macOS/tvOS/watchOS/visionOS ## Platform Support - **UIKit/AppKit**: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+ - **SwiftUI**: iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+ - **Swift**: 5.9+ (with Swift 6 strict concurrency support) ## Testing ### Test Structure - **Location**: `Tests/KingfisherTests/` - **Framework**: XCTest with custom `KingfisherTestHelper` - **Network mocking**: Uses Nocilla dependency for HTTP stubbing - **Test assets**: `dancing-banana.gif`, `single-frame.gif` ### Running Tests ```bash # All platforms (preferred) bundle exec fastlane tests # Single platform via destination bundle exec fastlane test destination:"platform=iOS Simulator,name=iPhone 15" ``` ## Documentation System Provides a DocC-based documentation for framework users: - **DocC integration** with comprehensive tutorials and API docs - **Location**: `Sources/Documentation.docc/` - **Online**: Swift Package Index hosted documentation - **Tutorials**: Both UIKit and SwiftUI getting started guides available ================================================ FILE: CHANGELOG.md ================================================ # Change Log ----- ## [8.8.0 - Background Relief](https://github.com/onevcat/Kingfisher/releases/tag/8.8.0) (2026-03-04) #### Add * 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 * Add `KFAnimatedImage.purgeFramesOnBackground(_:)` to expose background frame purging in SwiftUI. [#2484](https://github.com/onevcat/Kingfisher/pull/2484) @WZBbiao #### Fix * 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 * Fix `AnimatedImageView` deinit compatibility for older Swift 6 toolchains without isolated deinit support. [#2485](https://github.com/onevcat/Kingfisher/pull/2485) @onevcat * Apply `retryStrategy` in `ImagePrefetcher` load path so retry options also work during prefetching. [#2487](https://github.com/onevcat/Kingfisher/pull/2487) @TastyHeadphones * Fix non-Sendable `RetryDecision` capture warning in ImagePrefetcher retry flow under Swift 6 concurrency checks. [#2488](https://github.com/onevcat/Kingfisher/pull/2488) @onevcat --- ## [8.7.0 - Async Expedition](https://github.com/onevcat/Kingfisher/releases/tag/8.7.0) (2026-02-18) #### Add * 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 * Add optional `cacheKey` parameter for `PhotosPickerItemImageDataProvider` and `PHPickerResultImageDataProvider` for better cache control. [#2479](https://github.com/onevcat/Kingfisher/pull/2479) @onevcat * Support using an `OperationQueue` or equivalent interface in `CallbackQueue` for custom processing queue control. [#2474](https://github.com/onevcat/Kingfisher/pull/2474) @onevcat #### Fix * {"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"} * Fix a race condition crash in `ImagePrefetcher.handleComplete` when iterating sources during concurrent mutation. [#2465](https://github.com/onevcat/Kingfisher/pull/2465) @erichoracek * 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 * Fix a crash when accessing `KingfisherWrapper.shared` in unit tests. [#2450](https://github.com/onevcat/Kingfisher/pull/2450) @maxchuquimia * Call async modifier start callback before resume to ensure proper callback timing. [#2462](https://github.com/onevcat/Kingfisher/pull/2462) @onevcat * Remove ActorBox and harden background task cleanup to fix Sendable/main actor issues. [#2459](https://github.com/onevcat/Kingfisher/pull/2459) @onevcat * Mark `ImagePrefetcher` callback types as `@Sendable` to fix Swift 6 concurrency warnings. * 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 --- ## [8.6.2 - High Fidelity](https://github.com/onevcat/Kingfisher/releases/tag/8.6.2) (2025-11-17) #### Fix * 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 --- ## [8.6.1 - Atomic](https://github.com/onevcat/Kingfisher/releases/tag/8.6.1) (2025-10-27) #### Fix * Fix non-atomic task creation for concurrent same-URL requests to prevent callback loss. [#2444](https://github.com/onevcat/Kingfisher/pull/2444) @darkbrewx --- ## [8.6.0 - Retryfisher](https://github.com/onevcat/Kingfisher/releases/tag/8.6.0) (2025-10-05) #### Add * Add network retry strategy with configurable retry logic for failed downloads. [#2439](https://github.com/onevcat/Kingfisher/pull/2439) @komkovla * Add network metrics collection for download tasks with download speed measurement and timestamp handling. [#2416](https://github.com/onevcat/Kingfisher/pull/2416) @darkbrewx #### Fix * Fix crash on setting indicator on macOS 26 by changing default indicator style. [#2442](https://github.com/onevcat/Kingfisher/pull/2442) @onevcat * Fix retain cycle in ImageDownloader when transferring network metrics. [#2419](https://github.com/onevcat/Kingfisher/pull/2419) @darkbrewx * Upgrade to the latest Xcode recommended settings for improved build configuration. [#2417](https://github.com/onevcat/Kingfisher/pull/2417) @onevcat * Update CI script to make it work with Xcode 26. [#2442](https://github.com/onevcat/Kingfisher/pull/2442) @onevcat --- ## [8.5.0 - Transition Dancer](https://github.com/onevcat/Kingfisher/releases/tag/8.5.0) (2025-07-15) #### Add * Add SwiftUI native transition support for KFImage with `loadTransition(_:animation:)` method. [#2410](https://github.com/onevcat/Kingfisher/pull/2410) @darkbrewx @onevcat #### Fix * Fix documentation for `loadDiskFileSynchronously` in SwiftUI components to clarify default synchronous behavior. [#2411](https://github.com/onevcat/Kingfisher/pull/2411) @pinkjuice66 @onevcat * Fix BorderImageProcessor.identifier implementation. [#2409](https://github.com/onevcat/Kingfisher/pull/2409) @teameh --- ## [8.4.0 - Failure Fisher](https://github.com/onevcat/Kingfisher/releases/tag/8.4.0) (2025-07-03) #### Add * 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 #### Fix * Fix Sendable warnings in Xcode 26 with stricter concurrency checking. [#2400](https://github.com/onevcat/Kingfisher/pull/2400) @onevcat * Fix test timing issue in ImageCacheTests for CI stability. [#2401](https://github.com/onevcat/Kingfisher/pull/2401) @onevcat * Optimize CI workflow to avoid duplicate runs on pull requests. [#2402](https://github.com/onevcat/Kingfisher/pull/2402) @onevcat --- ## [8.3.3 - Swift Harmony](https://github.com/onevcat/Kingfisher/releases/tag/8.3.3) (2025-06-22) #### Add * Add Carthage support for both watchOS and iOS platforms [#2399](https://github.com/onevcat/Kingfisher/pull/2399) @wolfcon #### Fix * Fix Swift Task Continuation Misuse issue with Swift 6 compatible solution [#2398](https://github.com/onevcat/Kingfisher/pull/2398) @VladimirHorky @onevcat * Fix ThumbnailImageDataProvider image orientation issue [#2396](https://github.com/onevcat/Kingfisher/pull/2396) @gongzhang @onevcat * Remove incorrect watchOS available declaration [#2382](https://github.com/onevcat/Kingfisher/pull/2382) @wolfcon @onevcat --- ## [8.3.2 - Tariffisher](https://github.com/onevcat/Kingfisher/releases/tag/8.3.2) (2025-04-10) #### Fix * Memory cache cleanning timer will now be correctly set when the cache configuration is set. [#2376](https://github.com/onevcat/Kingfisher/issues/2376) @erincolkan * 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 * Refactoring on cache file name method in `DiskStorage`. [#2374](https://github.com/onevcat/Kingfisher/issues/2374) @NeoSelf1 --- ## [8.3.1 - Potential Cache Deadlock](https://github.com/onevcat/Kingfisher/releases/tag/8.3.1) (2025-03-15) #### Fix * 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) --- ## [8.3.0 - Progressive Loading Improvement](https://github.com/onevcat/Kingfisher/releases/tag/8.3.0) (2025-03-04) #### Add * 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) #### Fix * Solves a memory leak when using progressive JPEG loading. @onevcat @james-app @Adobels [#2368](https://github.com/onevcat/Kingfisher/pull/2368) * 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 * 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)] --- ## [8.2.0 - Snake Year](https://github.com/onevcat/Kingfisher/releases/tag/8.2.0) (2025-02-05) #### Add * Add a `ThumbnailImageDataProvider` to get a thumbnail image from a URL directly with `CGImageSourceCreateThumbnailAtIndex`. @onevcat [#2349](https://github.com/onevcat/Kingfisher/pull/2349) * 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) #### Fix * 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) * 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) --- ## [8.1.4 - Avoid Recreation](https://github.com/onevcat/Kingfisher/releases/tag/8.1.4) (2025-01-29) #### Fix * 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) --- ## [8.1.3 - Failing Size](https://github.com/onevcat/Kingfisher/releases/tag/8.1.3) (2024-12-17) #### Fix * 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) --- ## [8.1.2 - Data Racing](https://github.com/onevcat/Kingfisher/releases/tag/8.1.2) (2024-12-07) #### Fix * 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) --- ## [8.1.1 - Clean Completion](https://github.com/onevcat/Kingfisher/releases/tag/8.1.1) (2024-11-20) #### Fix * 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 --- ## [8.1.0 - Live Photo](https://github.com/onevcat/Kingfisher/releases/tag/8.1.0) (2024-10-13) #### Add * 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 * 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 #### Fix * Necessary `@MainActor` annotations for `ImageTransition.custom` member. [#2300](https://github.com/onevcat/Kingfisher/pull/2300) @mlight3 --- ## [8.0.3 - Animated Image Hitting](https://github.com/onevcat/Kingfisher/releases/tag/8.0.3) (2024-09-21) #### Fix * A regression of iOS 18 that the `KFAnimatedImage` does not receive user interaction. [#2295](https://github.com/onevcat/Kingfisher/issues/2295) @onevcat @danieldaquino --- ## [8.0.2 - Blur Scale](https://github.com/onevcat/Kingfisher/releases/tag/8.0.2) (2024-09-21) #### Fix * 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 --- ## [8.0.1 - Old Friends Matter](https://github.com/onevcat/Kingfisher/releases/tag/8.0.1) (2024-09-18) #### Fix * 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) --- ## [8.0.0 - 8.0.0 - Version 8](https://github.com/onevcat/Kingfisher/releases/tag/8.0.0) (2024-09-17) #### Add * 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 * 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 * 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 * Refined documentation and beautified tutorials with DocC. [#2160](https://github.com/onevcat/Kingfisher/pull/2160) @onevcat #### Fix * 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 * 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 * Rewrite the blur rendering method without deprecated `UIGraphicsBeginImageContextWithOptions`. [#2274](https://github.com/onevcat/Kingfisher/pull/2274) @onevcat * Apply existential any to protocol for Swift 6. [#2283](https://github.com/onevcat/Kingfisher/pull/2283) @qwerty3345 --- ## [7.12.0 - Lucky Seven](https://github.com/onevcat/Kingfisher/releases/tag/7.12.0) (2024-06-10) #### Add * 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 * A new `PHPickerResultImageDataProvider` for loading and caching images from `PHPickerResult`. [#2233](https://github.com/onevcat/Kingfisher/pull/2233) @nuomi1 * 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 #### Fix * Some improvements for documentation grammar and typos. [#2236](https://github.com/onevcat/Kingfisher/pull/2236) @FlyingCaiChong * 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 * 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 --- ## [7.11.0 - visionOS for CocoaPods](https://github.com/onevcat/Kingfisher/releases/tag/7.11.0) (2024-02-12) #### Add * 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 * A name for background task started for image cache cleanup. [#2201](https://github.com/onevcat/Kingfisher/pull/2201) @antohisorin --- ## [7.10.2 - GIF crash fix](https://github.com/onevcat/Kingfisher/releases/tag/7.10.2) (2024-01-11) #### Fix * An issue that loading the same GIF image in differnet image views may crash the app. [#2194](https://github.com/onevcat/Kingfisher/pull/2194) * A build script issue that exported the xcframeworks does not have the correct cert signing. [#2179](https://github.com/onevcat/Kingfisher/pull/2179) * 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) --- ## [7.10.1 - Compilation & Infinity](https://github.com/onevcat/Kingfisher/releases/tag/7.10.1) (2023-12-09) #### Fix * 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 * 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 --- ## [7.10.0 - Privacy Manifest](https://github.com/onevcat/Kingfisher/releases/tag/7.10.0) (2023-10-29) #### Add * 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 * Enable the modulemap generation and `-Swift.h` header again for ObjC compatibility. [#2138](https://github.com/onevcat/Kingfisher/pull/2138) @yev-kanivets #### Fix * Use the trait collection to determine animated image scale, instead of the deprecated `UIScreen` API. [#2157](https://github.com/onevcat/Kingfisher/pull/2157) @hyun99999 * 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 * 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 * Some documentation and CI impro/vements. --- ## [7.9.1 - Lastest Xcode 15 beta](https://github.com/onevcat/Kingfisher/releases/tag/7.9.1) (2023-08-26) #### Fix * 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 * 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 * 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) --- ## [7.9.0 - visionOS & Xcode 15](https://github.com/onevcat/Kingfisher/releases/tag/7.9.0) (2023-07-29) #### Add * 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) * 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) * 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) * 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 #### Fix * Some documentation fixes. --- ## [7.8.1 - Animated <3 Processor](https://github.com/onevcat/Kingfisher/releases/tag/7.8.1) (2023-06-19) #### Fix * Now the animated image creation from disk cache will use the input processor correctly. [#2099](https://github.com/onevcat/Kingfisher/pull/2099) @yeatse --- ## [7.8.0 - ImageSource Protocol](https://github.com/onevcat/Kingfisher/releases/tag/7.8.0) (2023-06-13) #### Add * Introduce a custom image source provider to enable third-party image processors to utilize `AnimatedImageView`. [#2094](https://github.com/onevcat/Kingfisher/pull/2094) @yeatse #### Fix * 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 --- ## [7.7.0 - The Last Chance](https://github.com/onevcat/Kingfisher/releases/tag/7.7.0) (2023-05-20) #### Add * 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 #### Fix * 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 * Apply access limitation to the internal `Source.Identifier`. [#2074](https://github.com/onevcat/Kingfisher/pull/2074) @iwill-hwang --- ## [7.6.2 - Fix Dead Loop](https://github.com/onevcat/Kingfisher/releases/tag/7.6.2) (2023-02-23) #### Fix * 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 * 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 --- ## [7.6.1 - Strict for Compiling](https://github.com/onevcat/Kingfisher/releases/tag/7.6.1) (2023-02-13) #### Fix * 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 --- ## [7.6.0 - Content Configuration](https://github.com/onevcat/Kingfisher/releases/tag/7.6.0) (2023-02-05) #### Add * 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) * 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 --- ## [7.5.0 - Aggressive New Year](https://github.com/onevcat/Kingfisher/releases/tag/7.5.0) (2023-01-08) #### Add * 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). #### Fix * Now loading images from local disk also respects the `backgroundDecode` option. [#2009](https://github.com/onevcat/Kingfisher/pull/2009) --- ## [7.4.1 - Maple Days](https://github.com/onevcat/Kingfisher/releases/tag/7.4.1) (2022-10-26) #### Fix * A rare crash from `_UIImageCGImageContent` when loading GIF files on iOS 15 or later. [#2004](https://github.com/onevcat/Kingfisher/pull/2004) * Now the dSYM symbols are contained inside the xcframework bundle instead of as standalone files. [#1998](https://github.com/onevcat/Kingfisher/pull/1998) * 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) --- ## [7.4.0 - Summer Ends](https://github.com/onevcat/Kingfisher/releases/tag/7.4.0) (2022-10-05) #### Add * A `data` property in `RetrieveImageResult` for reading the original data when an image loading is done. [#1986](https://github.com/onevcat/Kingfisher/pull/1986) * An async `data` getter in `ImageDataProvider`. More async methods are on the way. [#1989](https://github.com/onevcat/Kingfisher/pull/1989) #### Fix * 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 --- ## [7.3.2 - Align Layout](https://github.com/onevcat/Kingfisher/releases/tag/7.3.2) (2022-08-10) #### Fix * 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) --- ## [7.3.1 - Empty Not Void](https://github.com/onevcat/Kingfisher/releases/tag/7.3.1) (2022-07-31) #### Fix * An issue that `EmptyView` as `KFImage` placeholder fails loading of the image. [#1973](https://github.com/onevcat/Kingfisher/pull/1973) [@damian-rzeszot] --- ## [7.3.0 - Progressive Progress](https://github.com/onevcat/Kingfisher/releases/tag/7.3.0) (2022-07-06) #### Add * 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 * 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 #### Fix * A potential crash in `AnimatedImageView` that releasing on another thread. [#1956](https://github.com/onevcat/Kingfisher/pull/1956) @ufosky * A few internal clean up and removal of unused code. [#1958](https://github.com/onevcat/Kingfisher/pull/1958) @idrougge #### Remove * 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. --- ## [7.2.4 - Removing DocC plugin](https://github.com/onevcat/Kingfisher/releases/tag/7.2.4) (2022-06-15) #### Fix * 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 --- ## [7.2.3 - Track Transform](https://github.com/onevcat/Kingfisher/releases/tag/7.2.3) (2022-06-09) #### Fix * 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 * 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! --- ## [7.2.2 - Rainy Season](https://github.com/onevcat/Kingfisher/releases/tag/7.2.2) (2022-05-08) #### Fix * Loading an animated images from cache now respects the received options. [#1935](https://github.com/onevcat/Kingfisher/pull/1935) @uclort --- ## [7.2.1 - Spring Earth](https://github.com/onevcat/Kingfisher/releases/tag/7.2.1) (2022-04-11) #### Fix * Align `requestModifier` parameter with `AsyncImageDownloadRequestModifier` to allow async request changing. [#1918](https://github.com/onevcat/Kingfisher/pull/1918) @KKirsten * 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 * Give correct cache key for local urls in its conformance of `Resource`. [#1914](https://github.com/onevcat/Kingfisher/pull/1914) @onevcat * Reset placeholder image when loading fails. [#1925](https://github.com/onevcat/Kingfisher/pull/1925) @PJ-LT * Fix several typos and grammar. [#1926](https://github.com/onevcat/Kingfisher/pull/1926) @johnmckerrell [#1927](https://github.com/onevcat/Kingfisher/pull/1927) @SunsetWan --- ## [7.2.0 - End of the tunnel](https://github.com/onevcat/Kingfisher/releases/tag/7.2.0) (2022-02-27) #### Add * 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) #### Fix * 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) * Fix wrong key override when a local resource created by `ImageResource`'s initializer. [#1903](https://github.com/onevcat/Kingfisher/pull/1903) --- ## [7.1.2 - Cold Days](https://github.com/onevcat/Kingfisher/releases/tag/7.1.2) (2021-12-07) #### Fix * Lacking of `diskStoreWriteOptions` from `KFOptionSetter`. Now it supports to be set in a chainable way. [#1862](https://github.com/onevcat/Kingfisher/issues/1862) @ignotusverum * A duplicated nested `Radius` type which prevents the framework being used in Playground. [#1872](https://github.com/onevcat/Kingfisher/pull/1872) * 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 * 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) * Now the image cache calculats the cost animated images correctly with all frames. [#1881](https:://github.com/onevcat/Kingfisher/pull/1881) @pal-aspringfield * Remove CarPlay support when building against macCatalyst, which is not properly conditionally supported. [#1876](https://github.com/onevcat/Kingfisher/pull/1876) --- ## [7.1.1 - Double Ninth](https://github.com/onevcat/Kingfisher/releases/tag/7.1.1) (2021-10-16) #### Fix * 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 * Setting image to an `AnimatedImageView` now correctly replaces its layer contents. [#1836](https://github.com/onevcat/Kingfisher/issues/1836) @phantomato --- ## [7.1.0 - Autumn Patch](https://github.com/onevcat/Kingfisher/releases/tag/7.1.0) (2021-10-12) #### Add * 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 #### Fix * An Xcode issue that not recognizes iOS 15 availability checking for Apple Silicon. [#1822](https://github.com/onevcat/Kingfisher/pull/1822) from @enoktate * Add `onFailureImage` modifier back to `KFImage`, which was unexpected removed while upgrading. [#1829](https://github.com/onevcat/Kingfisher/pull/1829) from @skellock * 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 * 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 * 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 * 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 --- ## [7.0.0 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0) (2021-09-21) #### Add * 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) * Mark `ImageCache.retrieveImageInMemoryCache(forKey:options:)` as `open` to expose a correct override entry point to outside. [#1703](https://github.com/onevcat/Kingfisher/pull/1703) * 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) * A `KFAnimatedImage` type to display a GIF image in SwiftUI. [#1705](https://github.com/onevcat/Kingfisher/pull/1705) * 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) * Now `KFAnimatedImage` also supports `configure` modifier so you can set options to the underhood `AnimatedImageView`. [#1768](https://github.com/onevcat/Kingfisher/pull/1768) * Expose `AnimatedImageView` fields to allow consumers to observe GIF progress. [#1789](https://github.com/onevcat/Kingfisher/pull/1789) @AttilaTheFun * 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 #### Fix * Uses `UIGraphicsImageRenderer` on iOS and tvOS for better image drawing. [#1706](https://github.com/onevcat/Kingfisher/pull/1706) * 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 * 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) * 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) * Some minor performance improvement. [#1739](https://github.com/onevcat/Kingfisher/pull/1739) @fuyoufang * 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 * Respect transition for SwiftUI view when using `KFImage`. [#1767](https://github.com/onevcat/Kingfisher/pull/1767) * A type of `AuthenticationChallengeResponsable`. Now use `AuthenticationChallengeResponsible` instead. [#1780](https://github.com/onevcat/Kingfisher/pull/1780) @fakerlogic * An issue that `AnimatedImageView` dose not change the `tintColor` for templated images. [#1786](https://github.com/onevcat/Kingfisher/pull/1786) @leonpesdk * A crash when loading a GIF image in iOS 13 and below. [#1805](https://github.com/onevcat/Kingfisher/pull/1805/) @leonpesdk #### Remove * 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) * 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) --- ## [7.0.0-beta.4 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0-beta.4) (2021-09-16) #### Add * 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) #### Fix * A crash when loading a GIF image in iOS 13 and below. [#1805](https://github.com/onevcat/Kingfisher/pull/1805/files) --- ## [7.0.0-beta.3 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0-beta.3) (2021-08-29) #### Add * Now `KFAnimatedImage` also supports `configure` modifier so you can set options to the underhood `AnimatedImageView`. [#1768](https://github.com/onevcat/Kingfisher/pull/1768) * Expose `AnimatedImageView` fields to allow consumers to observe GIF progress. [#1789](https://github.com/onevcat/Kingfisher/pull/1789) #### Fix * Respect transition for SwiftUI view when using `KFImage`. [#1767](https://github.com/onevcat/Kingfisher/pull/1767) * A type of `AuthenticationChallengeResponsable`. Now use `AuthenticationChallengeResponsible` instead. [#1780](https://github.com/onevcat/Kingfisher/pull/1780/files) * An issue that `AnimatedImageView` dose not change the `tintColor` for templated images. [#1786](https://github.com/onevcat/Kingfisher/pull/1786) --- ## [7.0.0-beta.2 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0-beta.2) (2021-08-02) #### Fix - `LocalFileImageDataProvider` now loads data in a background queue by default. This prevents loading performance issue when the loading is created on main thread. [#1764] --- ## [7.0.0-beta.1 - Version 7](https://github.com/onevcat/Kingfisher/releases/tag/7.0.0-beta.1) (2021-07-27) #### Add * 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) * Mark `ImageCache.retrieveImageInMemoryCache(forKey:options:)` as `open` to expose a correct override entry point to outside. [#1703](https://github.com/onevcat/Kingfisher/pull/1703) * 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) * A `KFAnimatedImage` type to display a GIF image in SwiftUI. [#1705](https://github.com/onevcat/Kingfisher/pull/1705) * 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) #### Fix * Uses `UIGraphicsImageRenderer` on iOS and tvOS for better image drawing. [#1706](https://github.com/onevcat/Kingfisher/pull/1706) * An issue that prevents Kingfisher compiling on mac Catalyst target in some certain of Xcode versions. [#1692](https://github.com/onevcat/Kingfisher/pull/1692) * 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) * 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) * Some minor performance improvement. [#1739](https://github.com/onevcat/Kingfisher/pull/1739) #### Remove * Drop support for iOS 10, macOS 10.13, tvOS 10 and watch OS 3. * 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) --- ## [6.3.0 - Open To Better](https://github.com/onevcat/Kingfisher/releases/tag/6.3.0) (2021-04-21) #### Add * 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) * 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) * 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) #### Fix * Now the GIF continues to play in a collection view cell with highlight support. [#1685](https://github.com/onevcat/Kingfisher/pull/1685) * 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) --- ## [6.2.1 - Spring Release Fix](https://github.com/onevcat/Kingfisher/releases/tag/6.2.1) (2021-03-09) #### Fix * 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. --- ## [6.2.0 - Spring Release](https://github.com/onevcat/Kingfisher/releases/tag/6.2.0) (2021-03-08) #### Add * 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) * 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) * 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) * 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. #### Fix * 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) * 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) --- ## [6.1.1 - SwiftUI Issues](https://github.com/onevcat/Kingfisher/releases/tag/6.1.1) (2021-02-17) #### Fix * Remove unnecessary queue dispatch when setting image result. This prevents image flickering when some situation. [#1615](https://github.com/onevcat/Kingfisher/pull/1615) * 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) * 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) * 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) * 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) * Performance improvement for images with an non-up orientation in Exif when displaying in `KFImage`. [#1629](https://github.com/onevcat/Kingfisher/pull/1629) --- ## [6.1.0 - SwiftUI Rework](https://github.com/onevcat/Kingfisher/releases/tag/6.1.0) (2021-02-01) #### Add * 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) * 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) #### Fix * 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) * 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) * 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) --- ## [6.0.1 - Bind & Hug](https://github.com/onevcat/Kingfisher/releases/tag/6.0.1) (2021-01-05) #### Fix * Start the binder again when `KFImage` initialized, to keep the same behavior as previous versions. [#1594](https://github.com/onevcat/Kingfisher/issues/1594) --- ## [6.0.0 - New Year 2021](https://github.com/onevcat/Kingfisher/releases/tag/6.0.0) (2021-01-03) #### Add * 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) ```swift // Old way imageView.kf.setImage( with: url, placeholder: localImage, options: [.transition(.fade(1)), .loadDiskFileSynchronously], progressBlock: { receivedSize, totalSize in print("progressBlock") }, completionHandler: { result in print(result) } ) // New way KF.url(url) .placeholder(localImage) .fade(duration: 1) .loadDiskFileSynchronously() .onProgress { _ in print("progressBlock") } .onSuccess { result in print(result) } .onFailure { err in print("Error: \(err)") } .set(to: imageView) ``` * 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) * Add support for `TVMonogramView` on tvOS. [#1571](https://github.com/onevcat/Kingfisher/pull/1571) * 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) * An `AsyncImageDownloadRequestModifier` to support modifying the request in an asynchronous way. [#1589](https://github.com/onevcat/Kingfisher/pull/1589/files) * 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) #### Fix * An issue that importing AppKit wrongly in a macCatalyst build. [#1547](https://github.com/onevcat/Kingfisher/pull/1547/commits/096498f7798a6fd34c70efc6f80014dfc6d8a9b7) #### Remove * 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). * 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) --- ## [5.15.8 - KFImage handler](https://github.com/onevcat/Kingfisher/releases/tag/5.15.8) (2020-11-27) #### Fix * An issue caused the `onSuccess` handler not be called when the image is already cached. [#1570](https://github.com/onevcat/Kingfisher/pull/1570) --- ## [5.15.7 - Cancel Lock](https://github.com/onevcat/Kingfisher/releases/tag/5.15.7) (2020-10-29) #### Fix * 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) --- ## [5.15.6 - ImageBinder Callback](https://github.com/onevcat/Kingfisher/releases/tag/5.15.6) (2020-10-11) #### Fix * 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) --- ## [5.15.5 - Cancelling Fix](https://github.com/onevcat/Kingfisher/releases/tag/5.15.5) (2020-09-29) #### Fix * A possible fix for the crashes when cancelling a huge amount of image tasks too fast. [#1537] --- ## [5.15.4 - Farewell Objective-C (CocoaPods)](https://github.com/onevcat/Kingfisher/releases/tag/5.15.4) (2020-09-24) #### Fix * 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) --- ## [5.15.3 - Farewell Objective-C](https://github.com/onevcat/Kingfisher/releases/tag/5.15.3) (2020-09-21) #### Fix * 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) * 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) --- ## [5.15.2 - Xcode 11 Revived](https://github.com/onevcat/Kingfisher/releases/tag/5.15.2) (2020-09-19) #### Fix * 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) --- ## [5.15.1 - SwiftUI Layout](https://github.com/onevcat/Kingfisher/releases/tag/5.15.1) (2020-09-16) #### Fix * 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) --- ## [5.15.0 - Video and Text Attachment](https://github.com/onevcat/Kingfisher/releases/tag/5.15.0) (2020-08-17) #### Add * 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) * New extension methods on `NSTextAttachment` to load an image from network for an attachment. [#1495](https://github.com/onevcat/Kingfisher/pull/1495) * A general clear cache method which combines clearing for memory cache and disk cache. [#1494](https://github.com/onevcat/Kingfisher/pull/1494) #### Fix * Now the sample app has a new look and supports dark mode, finally. [#1496](https://github.com/onevcat/Kingfisher/pull/1496) --- ## [5.14.1 - Summer Fix](https://github.com/onevcat/Kingfisher/releases/tag/5.14.1) (2020-07-06) #### Fix * 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) * 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) * A fix to workaround on implicitly initializer of queue that might cause a crash. [#1449](https://github.com/onevcat/Kingfisher/issues/1449) * Improve the disk cache performance by avoiding unnecessary disk operations. [#1480](https://github.com/onevcat/Kingfisher/pull/1480) --- ## [5.14.0 - Retry Strategy](https://github.com/onevcat/Kingfisher/releases/tag/5.14.0) (2020-05-13) #### Add * A `.retryStrategy` option and associated `RetryStrategy` to define a highly customizable retry mechanism in Kingfisher. [#1424] * 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) * 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) * Add an `isLoaded` binding to `KFImage` to follow SwiftUI pattern better. [#1429](https://github.com/onevcat/Kingfisher/pull/1429) #### Fix * An issue that `.imageModifier` option not working on an `ImageProvider` provided image. [#1435](https://github.com/onevcat/Kingfisher/pull/1435) * 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) --- ## [5.13.4 - Build Configurations](https://github.com/onevcat/Kingfisher/releases/tag/5.13.4) (2020-04-11) #### Fix * 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) --- ## [5.13.3 - Dynamic SPM](https://github.com/onevcat/Kingfisher/releases/tag/5.13.3) (2020-04-01) #### Fix * Allows Carthage to build this library for macOS. [#1413](https://github.com/onevcat/Kingfisher/pull/1413) * Explicitly specify to build as a dynamic framework for Swift Package Manager. [#1420](https://github.com/onevcat/Kingfisher/pull/1420) --- ## [5.13.2 - KFImage Orientation](https://github.com/onevcat/Kingfisher/releases/tag/5.13.2) (2020-02-28) #### Fix * An issue for `KFImage` when resizing images with different EXIF orientation other than top. [#1396](https://github.com/onevcat/Kingfisher/pull/1396) * A race condition when setting `CacheCallbackCoordinator` state. [#1394](https://github.com/onevcat/Kingfisher/pull/1394) * Move an `@objc` attribute to prevent warnings in Xcode 11.4. --- ## [5.13.1 - Internal Warning](https://github.com/onevcat/Kingfisher/releases/tag/5.13.1) (2020-02-17) #### Fix * 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) --- ## [5.13.0 - New Year 2020](https://github.com/onevcat/Kingfisher/releases/tag/5.13.0) (2020-01-17) #### Add * Mark `DefaultCacheSerializer` as `public` and enables the ability of original data caching. [#1373](https://github.com/onevcat/Kingfisher/pull/1373/) * Add image compression quality parameter to `DefaultCacheSerializer`. [#1372](https://github.com/onevcat/Kingfisher/pull/1372/) * A new `contentURL` property in `ImageDataProvider` to provide a URL when it makes sense. [#1386](https://github.com/onevcat/Kingfisher/pull/1386/) #### Fix * Now, local file URLs can be loaded as `Resource`s without converted to `LocalFileImageDataProvider` explicitly. [#1386](https://github.com/onevcat/Kingfisher/pull/1386/) --- ## [5.12.0 - White Overflow](https://github.com/onevcat/Kingfisher/releases/tag/5.12.0) (2019-12-13) #### Add * 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) #### Fix * 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) * The build config for SwiftUI sub-pod now only applies to the KingfisherSwiftUI scheme. [#1368](https://github.com/onevcat/Kingfisher/pull/1368) --- ## [5.11.0 - macCatalyst](https://github.com/onevcat/Kingfisher/releases/tag/5.11.0) (2019-11-30) #### Add * Support macCatalyst platform when building with Carthage. [#1356](https://github.com/onevcat/Kingfisher/pull/1356) #### Fix * Fix an issue that image orientation not correctly applied when an image processor used. [#1358](https://github.com/onevcat/Kingfisher/pull/1358) --- ## [5.10.1 - Repeat Count](https://github.com/onevcat/Kingfisher/releases/tag/5.10.1) (2019-11-20) #### Fix * 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) * Make sure to skip disk cache when `fromMemoryCacheOrRefresh` set. [#1351](https://github.com/onevcat/Kingfisher/pull/1351) * Fix a issue which prevents building with Xcode 10. [#1353](https://github.com/onevcat/Kingfisher/pull/1353) --- ## [5.10.0 - Rex Rabbit](https://github.com/onevcat/Kingfisher/releases/tag/5.10.0) (2019-11-17) #### Add * 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) #### Fix * 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) * 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) * An issue that causes customize indicator not being placed with correct size. [#1345](https://github.com/onevcat/Kingfisher/pull/1345) * Performance improvement for loading progressive images. [#1332](https://github.com/onevcat/Kingfisher/pull/1332) --- ## [5.9.0 - Combination](https://github.com/onevcat/Kingfisher/releases/tag/5.9.0) (2019-10-24) #### Add * Introduce a `|>` operator for combining image processors. [#1320](https://github.com/onevcat/Kingfisher/pull/1320) #### Fix * Improve performance of reading task identifier when handling downloading side effect. [#1310](https://github.com/onevcat/Kingfisher/pull/1310) * Improve some type conversion to boost building. [#1321](https://github.com/onevcat/Kingfisher/pull/1321) --- ## [5.8.3 - Carthage Cache](https://github.com/onevcat/Kingfisher/releases/tag/5.8.3) (2019-10-09) #### Fix * Generate Objective-C header to make carthage cache work again. [#1308](https://github.com/onevcat/Kingfisher/pull/1308) --- ## [5.8.2 - Game of Thrones](https://github.com/onevcat/Kingfisher/releases/tag/5.8.2) (2019-10-04) #### Fix * Fix broken semantic versioning introduced by 5.8.0. [#1304](https://github.com/onevcat/Kingfisher/pull/1304) * 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) * Now project uses KingfisherSwiftUI with Swift Package Manager can be archived correctly. [#1300](https://github.com/onevcat/Kingfisher/pull/1300) --- ## [5.8.1 - Borderless](https://github.com/onevcat/Kingfisher/releases/tag/5.8.1) (2019-09-27) #### Fix * Remove the unexpected border in `KFImage` while loading the image. [#1293](https://github.com/onevcat/Kingfisher/pull/1293) --- ## [5.8.0 - Xcode 11 & SwiftUI](https://github.com/onevcat/Kingfisher/releases/tag/5.8.0) (2019-09-25) #### Add * Add support for Swift Package Manager. Now you can build and use Kingfisher with SPM under Xcode 11 and use it in all targets. * Add support for iPad Apps for Mac. You can use Kingfisher's UIKit extensions (like `UIImage` and `UIImageView`) on a catalyst project. * 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`. * Add support for building as a binary framework. A zipped file containing `xcframework` and related dSYMs is provided in the release page. * A `diskCacheAccessExtendingExpiration` option to give more control of disk cache extending behavior. [#1287](https://github.com/onevcat/Kingfisher/pull/1287) * Combine all targets into one. Now Kingfisher is a cross-platform target and you need to specify an SDK to build it. #### Fix * 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. * A potential thread issue in `taskIdentifier` which might cause a crash when using data provider. [#1276](https://github.com/onevcat/Kingfisher/pull/1276) * 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) --- ## [5.7.1 - Thread Things](https://github.com/onevcat/Kingfisher/releases/tag/5.7.1) (2019-08-11) #### Fix * Setting `runLoopMode` for `AnimatedImageView` will trigger animation restart normally. [#1253](https://github.com/onevcat/Kingfisher/pull/1253) * A possible thread issue when removing storage object from memory cache by the cache policy. [#1255](https://github.com/onevcat/Kingfisher/pull/1255) * Manipulating on `AnimateImageView`'s frame array is now thread safe. [#1257](https://github.com/onevcat/Kingfisher/pull/1257) --- ## [5.7.0 - Summer Bird](https://github.com/onevcat/Kingfisher/releases/tag/5.7.0) (2019-07-03) #### Add * Mark `cacheFileURL(forKey:)` of `DiskStorage` to public. [#1214](https://github.com/onevcat/Kingfisher/issues/1214) * Mark `KingfisherManager` initializer to public so other dependencies can customize the manager behavior. [#1216](https://github.com/onevcat/Kingfisher/issues/1216) #### Fix * Performance improvement on progressive JPEG scanning. [#1218](https://github.com/onevcat/Kingfisher/pull/1218) * Fix a potential thread issue when checking progressive JPEG. [#1220](https://github.com/onevcat/Kingfisher/pull/1220) #### Remove * The deprecated `Result` extensions for Swift 4 back compatibility are removed. [#1224](https://github.com/onevcat/Kingfisher/pull/1224) --- ## [5.6.0 - The Sands of Time](https://github.com/onevcat/Kingfisher/releases/tag/5.6.0) (2019-06-11) #### Add * 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) * 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) #### Fix * Resizing performance for animated images should be improved dramatically. [#1189](https://github.com/onevcat/Kingfisher/pull/1189) * A small optimization on MD5 calculation for image file cache key. [#1183](https://github.com/onevcat/Kingfisher/pull/1183) --- ## [5.5.0 - Progressive JPEG](https://github.com/onevcat/Kingfisher/releases/tag/5.5.0) (2019-05-17) #### Add * 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) * 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) #### Fix * Apply to some modern Swift syntax, which may also improve internal performance a bit. [#1181](https://github.com/onevcat/Kingfisher/pull/1181) --- ## [5.4.0 - Accio Support](https://github.com/onevcat/Kingfisher/releases/tag/5.4.0) (2019-04-24) #### Add * Add support for building project with [Accio](https://github.com/JamitLabs/Accio) (and Swift Package Manager). [#1153](https://github.com/onevcat/Kingfisher/pull/1153) #### Fix * Now `maxCachePeriodInSecond` of cache would treat 0 as expiring correctly. [#1160](https://github.com/onevcat/Kingfisher/pull/1160) * Normalization of image now returns an image with `.up` as orientation. [#1163](https://github.com/onevcat/Kingfisher/pull/1163) --- ## [5.3.1 - Prefetching Thread](https://github.com/onevcat/Kingfisher/releases/tag/5.3.1) (2019-03-28) #### Fix * Some thread issues which may cause crash when loading images by `ImagePrefetcher`. [#1150](https://github.com/onevcat/Kingfisher/pull/1150) * Setting a negative value by the deprecated `maxCachePeriodInSecond` API now expires the cache correctly. [#1145](https://github.com/onevcat/Kingfisher/pull/1145) --- ## [5.3.0 - Prefetching Sources](https://github.com/onevcat/Kingfisher/releases/tag/5.3.0) (2019-03-24) #### Add * Now `ImagePretcher` also supports using `Source` as fetching target. [#1142](https://github.com/onevcat/Kingfisher/pull/1142) * An option to skip file name hashing when storing image to disk cashe. [#1140](https://github.com/onevcat/Kingfisher/pull/1140) * Supports multiple Swift versions for CocoaPods 1.7.0. #### Fix * 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) * 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) * A regression which causes stack overflow when using `ImagePretcher` to load huge ammount of images. [#1143](https://github.com/onevcat/Kingfisher/pull/1143) --- ## [5.2.0 - Swift 5.0](https://github.com/onevcat/Kingfisher/releases/tag/5.2.0) (2019-02-27) #### Add * 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) #### Fix * A possible dead lock when using `ImagePretcher` heavily in another thread. [#1122](https://github.com/onevcat/Kingfisher/pull/1122) * Redesign `Result` type based on Swift `Result` in standard library. Deprecate `value` and `error` getter for `Kingfisher.Result`. --- ## [5.1.1 - Racing](https://github.com/onevcat/Kingfisher/releases/tag/5.1.1) (2019-02-11) #### Fix * Deprecate incorrect `ImageCache` initializer with `path` parameter. Now use the `cacheDirectoryURL` version for clearer implemetation. [#1114](https://github.com/onevcat/Kingfisher/pull/1114/) * Fix a race condition when setting download delegate from multiple `ImagePrefetcher`s. [#1109](https://github.com/onevcat/Kingfisher/issues/1109) * Now `directoryURL` of disk storage backend is marked as public correctly. [#1108](https://github.com/onevcat/Kingfisher/issues/1108) --- ## [5.1.0 - Redirecting & Racing](https://github.com/onevcat/Kingfisher/releases/tag/5.1.0) (2019-01-12) #### Add * Add a `ImageDownloadRedirectHandler` for intercepting HTTP request which redirects. [#1072](https://github.com/onevcat/Kingfisher/pull/1072) #### Fix * Some thread racing when downloading and resetting images in the same image view. [#1089](https://github.com/onevcat/Kingfisher/pull/1089) --- ## [5.0.1 - Interweave](https://github.com/onevcat/Kingfisher/releases/tag/5.0.1) (2018-12-17) #### Fix * Retrieving images from cache now respect options `callbackQueue` setting. [#1066](https://github.com/onevcat/Kingfisher/issues/1066) * A crash when passing zero or negative size to `DownsamplingImageProcessor`. [#1073](https://github.com/onevcat/Kingfisher/issues/1073) --- ## [5.0.0 - Reborn](https://github.com/onevcat/Kingfisher/releases/tag/5.0.0) (2018-12-08) #### Add * 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. * 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. * 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. * Image cache of memory storage would be purged automatically in a certain time interval. This reduce memory pressure for other parts of your app. * 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. * Now the `ImageCache` can receive only raw `Data` object and cache it as needed. * A `KingfisherParsedOptionsInfo` type to parse `KingfisherOptionsInfoItem`s in related API. This improves reusability and performance when handling options in Kingfisher. * An option to specify whether an image should also prefetched to memory cache or not. * An option to make the disk file loading synchronously instead of in its own I/O queue. * 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. * 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. * 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. * A `DownsamplingImageProcessor` for downsampling an image to a given size before loading it to memory. * 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`. * 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. #### Fix * Now CommonCrypto from system is used to calculate file name from cache key, instead of using a customized hash algorithm. * An issue which causes `ImageDownloader` crashing when a lot of downloading tasks running at the same time. * All operations like image pretching and data receiving should now be performed in non-UI threads correctly. * Now `KingfisherCompatible` uses struct for `kf` namespacing for better performance. --- ## [4.10.1 - Time Machine](https://github.com/onevcat/Kingfisher/releases/tag/4.10.1) (2018-11-03) #### Fix * Add Swift 4 compatibility back. * Increase watchOS target to 3.0 in podspec. --- ## [4.10.0 - Swift 4.2](https://github.com/onevcat/Kingfisher/releases/tag/4.10.0) (2018-09-20) #### Add * Support for building with Xcode 10 and Swift 4.2. This version requires Xcode 10 or later with Swift 4.2 compiler. #### Fix * Improve performance when an invalide HTTP status code received. [#985](https://github.com/onevcat/Kingfisher/pull/985) --- ## [4.9.0 - Patience is a Virtue](https://github.com/onevcat/Kingfisher/releases/tag/4.9.0) (2018-09-04) #### Add * Add a `waitForCache` option to allowing cache callback called after cache operation finishes. [#963](https://github.com/onevcat/Kingfisher/pull/963) #### Fix * Animated image now will recognize `.once` and `.finite(1)` the same thing. [#982](https://github.com/onevcat/Kingfisher/pull/982) * Replace class-only protocol keyword with AnyObject as Swift convention. [#983](https://github.com/onevcat/Kingfisher/pull/983) * A wrong cache callback parameter when storing cache with background decoding. [#986](https://github.com/onevcat/Kingfisher/pull/986) * Access `downloadHolder` in a serial queue to avoid racing. [#984](https://github.com/onevcat/Kingfisher/pull/984) --- ## [4.8.1 - Prefetch Improvement](https://github.com/onevcat/Kingfisher/releases/tag/4.8.1) (2018-07-26) #### Fix * Fix a performance issue when prefetching images by moving related operation away from main queue. [#957](https://github.com/onevcat/Kingfisher/pull/957) * Improvement on stability of some test cases. --- ## [4.8.0 - Watch & Watching](https://github.com/onevcat/Kingfisher/releases/tag/4.8.0) (2018-05-15) #### Add * WKInterfaceImage setting image interface for watchOS. [#913](https://github.com/onevcat/Kingfisher/pull/913) * A new delegate method for watching `ImageDownloader` object completes a downloading request with success or failure. [#901](https://github.com/onevcat/Kingfisher/pull/901) #### Fix * Use newly created operation queue for downloader. * Filter.init(tranform:) is renamed to Filter.init(transform:) * Some internal minor fix on constant and typo, etc. --- ## [4.7.0 - Cancel All](https://github.com/onevcat/Kingfisher/releases/tag/4.7.0) (2018-04-06) #### Add * ImageDownloader now contains a method `cancelAll` to cancel all downloading tasks. [#894](https://github.com/onevcat/Kingfisher/pull/894) * Supports Swift 4.1 and Xcode 9.3. [#889](https://github.com/onevcat/Kingfisher/pull/889) --- ## [4.6.4 - Customize Activity Indicator](https://github.com/onevcat/Kingfisher/releases/tag/4.6.4) (2018-03-20) #### Fix * An issue caused customize activity indicator not working for Swift 4. [#872](https://github.com/onevcat/Kingfisher/issues/872) * Specify Swift compiler version explicity in pod spec file for CocoaPods 1.4. [#875](https://github.com/onevcat/Kingfisher/pull/875) --- ## [4.6.3 - Clean Demo](https://github.com/onevcat/Kingfisher/releases/tag/4.6.3) (2018-03-01) #### Fix * Move demo project out from Kingfisher framework project. [#867](https://github.com/onevcat/Kingfisher/pull/867) * An issue that caused stack overflow when prefetching too many images, while they are already cached. [#866](https://github.com/onevcat/Kingfisher/pull/866) --- ## [4.6.2 - GIF frames](https://github.com/onevcat/Kingfisher/releases/tag/4.6.2) (2018-02-14) #### Fix * Animated image view now will call finished delegate method in correct timing. [#860](https://github.com/onevcat/Kingfisher/issues/860) --- ## [4.6.1 - MD5](https://github.com/onevcat/Kingfisher/releases/tag/4.6.1) (2017-12-28) #### Fix * 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) --- ## [4.6.0 - AniBird](https://github.com/onevcat/Kingfisher/releases/tag/4.6.0) (2017-12-27) #### Add * Delegate methods for `AnimatedImageView` to inspect finishing event and/or end of an animation loop. [#829](https://github.com/onevcat/Kingfisher/pull/829) #### Fix * Minor performance improvement by `final` some classes. * Remove unnecessary `Box` type since Objective-C world takes `Any`. [#832](https://github.com/onevcat/Kingfisher/pull/832). * Some internal failing tests on earlier macOS, in which color space giving different result. --- ## [4.5.0 - Blending](https://github.com/onevcat/Kingfisher/releases/tag/4.5.0) (2017-12-05) #### Add * New image processors to blend an image. See `BlendImageProcessor` on iOS/tvOS and `CompositingImageProcessor` on macOS. [#818](https://github.com/onevcat/Kingfisher/pull/818) #### Fix * A crash when prefetching too many images in a single batch. [#692](https://github.com/onevcat/Kingfisher/issues/692) * A possible invalid redeclaration on `Array` from `AnimatedImageView`. [#819](https://github.com/onevcat/Kingfisher/pull/819) --- ## [4.4.0 - Image Modifier](https://github.com/onevcat/Kingfisher/releases/tag/4.4.0) (2017-12-01) #### Add * 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) #### Fix * Apply scale on all image based processor methods, including the existing ones from memory cache. [#813](https://github.com/onevcat/Kingfisher/issues/813) --- ## [4.3.1 - Cache Regression](https://github.com/onevcat/Kingfisher/releases/tag/4.3.1) (2017-11-21) #### Fix * A regression introduced in 4.3.0 which cause the cache not working well for processed images. --- ## [4.3.0 - Memory Or Refresh](https://github.com/onevcat/Kingfisher/releases/tag/4.3.0) (2017-11-17) #### Add * 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) #### Fix * 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) * 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) --- ## [4.2.0 - A Tale of Two Caches](https://github.com/onevcat/Kingfisher/releases/tag/4.2.0) (2017-10-22) #### Add * 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) --- ## [4.1.1 - Love Barrier Again](https://github.com/onevcat/Kingfisher/releases/tag/4.1.1) (2017-10-17) #### Fix * A potential race condition in `ImageDownloader`. [#763](https://github.com/onevcat/Kingfisher/issues/763) --- ## [4.1.0 - Data in Hand](https://github.com/onevcat/Kingfisher/releases/tag/4.1.0) (2017-09-28) #### Add * An `ImageDownloader` delegate method to provide a chance for you to check and modify the data. [#773](https://github.com/onevcat/Kingfisher/pull/773) #### Fix * 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) --- ## [4.0.1 - Swift 4](https://github.com/onevcat/Kingfisher/releases/tag/4.0.1) (2017-09-15) #### Add * 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) * 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) #### Fix * Update pod spec to use Swift 4.0 as Swift Version configuration. --- ## [4.0.0 - Swift 4](https://github.com/onevcat/Kingfisher/releases/tag/4.0.0) (2017-09-14) #### Add * 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) * 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) --- ## [3.13.1 - Evil Setting](https://github.com/onevcat/Kingfisher/releases/tag/3.13.1) (2017-09-14) #### Fix * 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) --- ## [3.13.0 - Rum Bird](https://github.com/onevcat/Kingfisher/releases/tag/3.13.0) (2017-09-12) #### Add * 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) --- ## [3.12.2 - Scaling Background Decoding](https://github.com/onevcat/Kingfisher/releases/tag/3.12.2) (2017-09-02) #### Fix * Fix an issue which causes image scale not correct when background decoding option is used. [#761](https://github.com/onevcat/Kingfisher/issues/761) --- ## [3.12.1 - Placeholder](https://github.com/onevcat/Kingfisher/releases/tag/3.12.1) (2017-08-30) #### Add * 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) --- ## [3.12.0 - Placeholder](https://github.com/onevcat/Kingfisher/releases/tag/3.12.0) (2017-08-30) #### Add * 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) --- ## [3.11.0 - Task Auth](https://github.com/onevcat/Kingfisher/releases/tag/3.11.0) (2017-08-16) #### Add * A task based authentication challenge handler for some auth methods like HTTP Digest. [#742](https://github.com/onevcat/Kingfisher/issues/742) #### Fix * 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) --- ## [3.10.4 - Indicator Size](https://github.com/onevcat/Kingfisher/releases/tag/3.10.4) (2017-07-26) #### Fix * Respect image and custom indicator size. Now Kingfisher will not resize the indicators to the image size for you automatically. --- ## [3.10.3 - ProMotion](https://github.com/onevcat/Kingfisher/releases/tag/3.10.3) (2017-07-06) #### Fix * 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) --- ## [3.10.2 - Missing Boys](https://github.com/onevcat/Kingfisher/releases/tag/3.10.2) (2017-06-16) #### Fix * Now the processed images result from a cache original image could be cached correctly. [#711](https://github.com/onevcat/Kingfisher/issues/711) * Some internal minor clean up. --- ## [3.10.1 - Order, order!](https://github.com/onevcat/Kingfisher/releases/tag/3.10.1) (2017-06-04) #### Fix * Change an inline function order to make Swift 3.0 compiler happy. [#700](https://github.com/onevcat/Kingfisher/issues/700) --- ## [3.10.0 - Hot Bird](https://github.com/onevcat/Kingfisher/releases/tag/3.10.0) (2017-06-03) #### Add * 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. * 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) * A `FormatIndicatedCacheSerializer` to serialize the image into a certain format (`png`, `jpg` or `gif`). [#693](https://github.com/onevcat/Kingfisher/issues/693) #### Fix * 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) * Fix the showing/hiding logic for activity indicator in image view to make them independent from race condition. * A possible race condition that accessing downloading fetch load conccurently. * Invalidate the download session when the downloader gets released. It might cause problem if you were using your own downloader instance. * Some internal stability improvement. --- ## [3.9.1 - Compatibility](https://github.com/onevcat/Kingfisher/releases/tag/3.9.1) (2017-05-13) #### Fix * Fix a problem which prevents building under Xcode 8.2 / Swift 3.0. [#677](https://github.com/onevcat/Kingfisher/issues/677) --- ## [3.9.0 - Follow the Rules](https://github.com/onevcat/Kingfisher/releases/tag/3.9.0) (2017-05-11) #### Add * 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) #### Fix * 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`. * Deprecate `requestsUsePipeling` in `ImageDownloader` since there was a typo. Now use `requestsUsePipelining` instead. [#673](https://github.com/onevcat/Kingfisher/pull/673) * Some internal improvement for private APIs. --- ## [3.8.0 - Prowess](https://github.com/onevcat/Kingfisher/releases/tag/3.8.0) (2017-05-10) #### Add * 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) --- ## [3.7.2 - Never Do Things by Halves](https://github.com/onevcat/Kingfisher/releases/tag/3.7.2) (2017-05-09) #### Fix * 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) --- ## [3.7.1 - GIF is Animated](https://github.com/onevcat/Kingfisher/releases/tag/3.7.1) (2017-05-08) #### Fix * 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) --- ## [3.7.0 - Summer Bird](https://github.com/onevcat/Kingfisher/releases/tag/3.7.0) (2017-05-04) #### Add * A delegate method in `ImageDownloaderDelegate` to notify starting of a downloading progress. #### Fix * Better documentation for `Resource` parameter in image setting extension. --- ## [3.6.2 - Naughty CGImage](https://github.com/onevcat/Kingfisher/releases/tag/3.6.2) (2017-04-11) #### Fix * 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) * 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) --- ## [3.6.1 - Some Optimization](https://github.com/onevcat/Kingfisher/releases/tag/3.6.1) (2017-04-01) #### Fix * Fix warnings when build Kingfisher in Swift 3.1 compiler. [#632](https://github.com/onevcat/Kingfisher/pull/632) * Wrong size when decoding images with a passed-in scale option. [#633](https://github.com/onevcat/Kingfisher/pull/633) * Speed up MD5 calculation by turing to a pure Swift implementation. [#636](https://github.com/onevcat/Kingfisher/pull/636) * Host docs directly in GitHub. [#641](https://github.com/onevcat/Kingfisher/pull/641) --- ## [3.6.0 - Cropping](https://github.com/onevcat/Kingfisher/releases/tag/3.6.0) (2017-03-26) #### Add * A built-in image processor to crop images with a targeted size and anchor. [#465](https://github.com/onevcat/Kingfisher/issues/465) --- ## [3.5.2 - Bad Apple](https://github.com/onevcat/Kingfisher/releases/tag/3.5.2) (2017-03-09) #### Fix * An issue which causes app crashing while folder enumerating encountered an error in `ImageCache`. [#620](https://github.com/onevcat/Kingfisher/pull/620) --- ## [3.5.1 - Fast is better than slow](https://github.com/onevcat/Kingfisher/releases/tag/3.5.1) (2017-03-01) #### Fix * A minor improvement on slow compiling time due to a method in `Image`. [#611](https://github.com/onevcat/Kingfisher/issues/611) --- ## [3.5.0 - New age, new content](https://github.com/onevcat/Kingfisher/releases/tag/3.5.0) (2017-02-21) #### Add * 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) #### Fix * 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) --- ## [3.4.0 - Spring is here](https://github.com/onevcat/Kingfisher/releases/tag/3.4.0) (2017-02-11) #### Add * 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) #### Fix * 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) * 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) * 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) --- ## [3.3.4 - Cancellation means a new start!](https://github.com/onevcat/Kingfisher/releases/tag/3.3.4) (2017-02-04) #### Fix * Now `cancel` on a `RetrieveImageTask` will work properly even when the downloading not started. [#578](https://github.com/onevcat/Kingfisher/pull/578) * Use modern float constant of pi. [#576](https://github.com/onevcat/Kingfisher/pull/576) --- ## [3.3.3 - Xcode 8.0 is not dead yet](https://github.com/onevcat/Kingfisher/releases/tag/3.3.3) (2017-01-30) #### Fix * A type inference to make Kingfisher compiles on Xcode 8.0 again. [#572](https://github.com/onevcat/Kingfisher/issues/572) --- ## [3.3.2 - Upside Down](https://github.com/onevcat/Kingfisher/releases/tag/3.3.2) (2017-01-23) #### Fix * An issue which causes the background decoded images drawn upside down. --- ## [3.3.1 - Lunar Eve](https://github.com/onevcat/Kingfisher/releases/tag/3.3.1) (2017-01-21) #### Add * Expose default `pngRepresentation`, `jpegRepresentation` and `gifRepresentation` as public. [#560](https://github.com/onevcat/Kingfisher/pull/560) * Support unlimited disk cache duration. [#566](https://github.com/onevcat/Kingfisher/pull/566) #### Fix * A mismatch of CG image component when creating `CGContext` for blur filter. [#567](https://github.com/onevcat/Kingfisher/pull/567) * Remove test images from repo to keep slim. [#568](https://github.com/onevcat/Kingfisher/pull/568) --- ## [3.3.0 - Lunar Eve](https://github.com/onevcat/Kingfisher/releases/tag/3.3.0) (2017-01-21) #### Add * Expose default `pngRepresentation`, `jpegRepresentation` and `gifRepresentation` as public. [#560](https://github.com/onevcat/Kingfisher/pull/560) * Support unlimited disk cache duration. [#566](https://github.com/onevcat/Kingfisher/pull/566) #### Fix * A mismatch of CG image component when creating `CGContext` for blur filter. [#567](https://github.com/onevcat/Kingfisher/pull/567) * Remove test images from repo to keep slim. [#568](https://github.com/onevcat/Kingfisher/pull/568) --- ## [3.2.4 - Love SPM again](https://github.com/onevcat/Kingfisher/releases/tag/3.2.4) (2016-12-22) #### Fix * A problem that causes framework cannot be compiled by Swift Package Manager. [#547](https://github.com/onevcat/Kingfisher/issues/547) * Removed an unused parameter from round corner image API. [#548](https://github.com/onevcat/Kingfisher/issues/548) --- ## [3.2.3 - LI ZHENG](https://github.com/onevcat/Kingfisher/releases/tag/3.2.3) (2016-12-20) #### Fix * An issue which caused processed images igoring exif orientation information. [#535](https://github.com/onevcat/Kingfisher/issues/535) --- ## [3.2.2 - Faster GIF](https://github.com/onevcat/Kingfisher/releases/tag/3.2.2) (2016-12-02) #### Fix * 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) * 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) * Fix some typos. [#523](https://github.com/onevcat/Kingfisher/issues/523) --- ## [3.2.1 - Helper Helps](https://github.com/onevcat/Kingfisher/releases/tag/3.2.1) (2016-11-14) #### Add * 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) * Mark the empty task for downloader as `public`. [#508](https://github.com/onevcat/Kingfisher/issues/508) #### Fix * 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) --- ## [3.2.0 - Quiet](https://github.com/onevcat/Kingfisher/releases/tag/3.2.0) (2016-11-07) #### Add * 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) * A disk cache path closure to let you fully customize the disk cache path. [#499](https://github.com/onevcat/Kingfisher/pull/499) #### Fix * 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) --- ## [3.1.4 - CIImageProcessor with Data](https://github.com/onevcat/Kingfisher/releases/tag/3.1.4) (2016-10-19) #### Fix * Fix a problem that `CIImageProcessor` not get called when feeding data to the processor. [#485](https://github.com/onevcat/Kingfisher/issues/485) --- ## [3.1.3 - Collocalia](https://github.com/onevcat/Kingfisher/releases/tag/3.1.3) (2016-10-06) #### Fix * A compiling time issue. Now the compile time of Kingfisher should drop dramatically. [#467](https://github.com/onevcat/Kingfisher/pull/467) * 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) #### Remove * requestModifier of `ImageDownloader` is removed to prevent leading to misunderstanding. --- ## [3.1.1 - Kingfisher likes more](https://github.com/onevcat/Kingfisher/releases/tag/3.1.1) (2016-09-28) #### Fix * 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) * 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) * ImageCache could accept a path extension as key now. [#456](https://github.com/onevcat/Kingfisher/pull/456) --- ## [3.1.0 - Namespace](https://github.com/onevcat/Kingfisher/releases/tag/3.1.0) (2016-09-21) #### Add * 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) #### Fix * Mark `AnimateImageView` to open so you can extend this class again. [#442](https://github.com/onevcat/Kingfisher/pull/442) * Update demo code to adopt iOS 10 prefetching cell feature and new cell life cycle. [#447](https://github.com/onevcat/Kingfisher/issues/447) #### Remove * Since `kf` namespace is added, all original `kf_` prefix methods are marked as deprecated. --- ## [3.0.1 - New Age - Swift 3](https://github.com/onevcat/Kingfisher/releases/tag/3.0.1) (2016-09-14) #### Add * 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. * 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) * 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. * 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. * New indicator type. Now you should be able to use your own indicators. [#430](https://github.com/onevcat/Kingfisher/pull/430) * ImageDownloadRequestModifier. Use this protocol to modify requests being sent to your server. #### Fix * 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. * 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`. #### Remove * KingfisherManager.init is removed since you should never create your own manager. * cachedImageExistsforURL in `ImageCache` is removed since it introduced unnecessary coupling. Use `isImageCached` instead. * requestModifier` is removed. Use `.requestModifier` and pass a `ImageDownloadRequestModifier`. * kf_showIndicatorWhenLoading is removed since we have a better and flexible way to use indicator by `kf_indicatorType`. --- ## [3.0.0 - New Age - Swift 3](https://github.com/onevcat/Kingfisher/releases/tag/3.0.0) (2016-09-14) #### Add * 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. * 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) * 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. * 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. * New indicator type. Now you should be able to use your own indicators. [#430](https://github.com/onevcat/Kingfisher/pull/430) * ImageDownloadRequestModifier. Use this protocol to modify requests being sent to your server. #### Fix * 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. * 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`. --- ## [2.6.0 - Indicator Customization](https://github.com/onevcat/Kingfisher/releases/tag/2.6.0) (2016-09-12) #### Add * Support for different types of indicators, including gif images. [#425](https://github.com/onevcat/Kingfisher/pull/425) --- ## [2.5.1 - Prefetcher Trap](https://github.com/onevcat/Kingfisher/releases/tag/2.5.1) (2016-09-06) #### Fix * Fix a possible trap of range making in prefetcher. [#422](https://github.com/onevcat/Kingfisher/pull/422) --- ## [2.5.0 - Swift 2.3](https://github.com/onevcat/Kingfisher/releases/tag/2.5.0) (2016-08-29) #### Add * Support for Swift 2.3 --- ## [2.4.3 - Longer Cache](https://github.com/onevcat/Kingfisher/releases/tag/2.4.3) (2016-08-17) #### Fix * 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) --- ## [2.4.2 - Optional Welcome](https://github.com/onevcat/Kingfisher/releases/tag/2.4.2) (2016-07-10) #### Add * Accept `nil` as valid URL parameter for image view's extension methods. #### Fix * The completion handler of image view setting method will not be called any more if `self` is released. * Improve empty task so some performance improvment could be achieved. * 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. * Removed Implicit Unwrapping of CacheType that caused crashes if the image is not cached. --- ## [2.4.1 - Force Transition](https://github.com/onevcat/Kingfisher/releases/tag/2.4.1) (2016-05-10) #### Add * 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) --- ## [2.4.0 - Animate Me](https://github.com/onevcat/Kingfisher/releases/tag/2.4.0) (2016-05-04) #### Add * 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) #### Fix * An issue which may cause iOS app crasing when switching background/foreground multiple times. [#309](https://github.com/onevcat/Kingfisher/pull/309) * Change license of String+MD5.swift to a more precise one. [#302](https://github.com/onevcat/Kingfisher/issues/302) --- ## [2.3.1 - Pod Me up](https://github.com/onevcat/Kingfisher/releases/tag/2.3.1) (2016-04-22) #### Fix * Exclude NSButton extension from no related target. [#292](https://github.com/onevcat/Kingfisher/pull/292) --- ## [2.3.0 - Warmly Welcome](https://github.com/onevcat/Kingfisher/releases/tag/2.3.0) (2016-04-21) #### Add * Add support for App Extension target. [#290](https://github.com/onevcat/Kingfisher/pull/290) * Add support for NSButton. [#287](https://github.com/onevcat/Kingfisher/pull/287) --- ## [2.2.2 - Spring Bird II](https://github.com/onevcat/Kingfisher/releases/tag/2.2.2) (2016-04-06) #### Fix * Add default values to optional parameters, which should be a part of 2.2.1. [#284](https://github.com/onevcat/Kingfisher/issues/284) --- ## [2.2.1 - Spring Bird](https://github.com/onevcat/Kingfisher/releases/tag/2.2.1) (2016-04-06) #### Fix * A memory leak caused by closure based Generator. [#281](https://github.com/onevcat/Kingfisher/pull/281) * Remove duplicated APIs since auto completion gets improved in Swift 2.2. [#283](https://github.com/onevcat/Kingfisher/pull/283) * Enable all recongnized format for `UIImage`. [#278](https://github.com/onevcat/Kingfisher/pull/278) --- ## [2.2.0 - Open Source Swift](https://github.com/onevcat/Kingfisher/releases/tag/2.2.0) (2016-03-24) #### Add * 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. #### Fix * A trivial issue that a context holder should not exist when decoding images background. --- ## [2.1.0 - Prefetching](https://github.com/onevcat/Kingfisher/releases/tag/2.1.0) (2016-03-10) #### Add * 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) * 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) * An API (`cachePathForKey(:)`) to get real path for a specified key in a cache. [#256](https://github.com/onevcat/Kingfisher/pull/256) #### Fix * 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) * A potential crash in `ImageCache` when an empty image is passed into. --- ## [2.0.4 - Sorry Pipelining](https://github.com/onevcat/Kingfisher/releases/tag/2.0.4) (2016-02-27) #### Fix * 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) * Image transition now allows user interaction. [#252](https://github.com/onevcat/Kingfisher/pull/252) --- ## [2.0.3 - Holiday Issues](https://github.com/onevcat/Kingfisher/releases/tag/2.0.3) (2016-02-17) #### Fix * A memory leak caused by retain cycle of downloader session and its delegate. [#235](https://github.com/onevcat/Kingfisher/issues/235) * 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) * Fix warnings when the latest version of SwiftLint is used. [#189](https://github.com/onevcat/Kingfisher/issues/189#issuecomment-185205010) --- ## [2.0.2 - Single Frame GIF](https://github.com/onevcat/Kingfisher/releases/tag/2.0.2) (2016-02-14) #### Fix * An issue which causes GIF images with only one frame failing to be loaded correctly. [#231](https://github.com/onevcat/Kingfisher/issues/231) --- ## [2.0.1 - Disk is back](https://github.com/onevcat/Kingfisher/releases/tag/2.0.1) (2016-01-28) #### Fix * An issue which causes the downloaded image not cached in disk. [#224](https://github.com/onevcat/Kingfisher/pull/224) --- ## [2.0.0 - Kingfisher 2](https://github.com/onevcat/Kingfisher/releases/tag/2.0.0) (2016-01-23) #### Add * OS X support. Now Kingfisher can work seamlessly for `NSImage`. [#201](https://github.com/onevcat/Kingfisher/pull/201) * watchOS 2.x support. [#210](https://github.com/onevcat/Kingfisher/pull/210) * Swift Package Manager support. [#218](https://github.com/onevcat/Kingfisher/issues/218) * 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) * API for changing download priority of image download task after the download started. [#73](https://github.com/onevcat/Kingfisher/issues/73) * You can cancel image or background image downloading task now for button as well. [#205](https://github.com/onevcat/Kingfisher/issues/205) #### Fix * A potential thread issue when asking for cache state right after downloading finished. * Improve MD5 calculating speed. [#220](https://github.com/onevcat/Kingfisher/pull/220) * The scale was not correct when processing GIF files. --- ## [1.9.3](https://github.com/onevcat/Kingfisher/releases/tag/1.9.3) (2016-01-22) #### Fix * Stop indicator animation when loading failed. [#215](https://github.com/onevcat/Kingfisher/issues/215) --- ## [1.9.2 - IOIOIO](https://github.com/onevcat/Kingfisher/releases/tag/1.9.2) (2016-01-14) #### Fix * A potential issue causes image cache checking method not working when the image just stored. * Better performance and image quality when storing images with original data. --- ## [1.9.1 - You happy, I happy](https://github.com/onevcat/Kingfisher/releases/tag/1.9.1) (2016-01-04) #### Fix * Making SwiftLint happy when building with Carthage. #189 --- ## [1.9.0 - What a Task](https://github.com/onevcat/Kingfisher/releases/tag/1.9.0) (2015-12-31) #### Add * Download methods in `ImageDownloader` now returns a cancelable task. So you can cancel the downloading process when using downloader separately. * Add a cancelling method in image view extension for easier cancel operation. * Mark some properties of downloading task as public. #### Fix * Cancelling of image downloading now triggers completion handler with `NSURLErrorCancelled` correctly now. --- ## [1.8.5 - Single Dog](https://github.com/onevcat/Kingfisher/releases/tag/1.8.5) (2015-12-16) #### Fix * Use single url session to download images. * Ignore and return error immediately for empty URL. * Internal update for testing stability and code style. --- ## [1.8.4 - GIF is GIF](https://github.com/onevcat/Kingfisher/releases/tag/1.8.4) (2015-12-12) #### Fix * Opt out the normalization and decoding for GIF, which would lead an issue that the GIF images missing. * Proper cost count for GIF image. --- ## [1.8.3 - Internal beauty](https://github.com/onevcat/Kingfisher/releases/tag/1.8.3) (2015-12-05) #### Fix * Fix for code base styles and formats. --- ## [1.8.2 - Path matters](https://github.com/onevcat/Kingfisher/releases/tag/1.8.2) (2015-11-19) #### Add * 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). --- ## [1.8.1 - Transition needs rest](https://github.com/onevcat/Kingfisher/releases/tag/1.8.1) (2015-11-13) #### Fix * Only apply transition when images are downloaded. It will not show transition animation now if images loaded from either memory or disk cache now. * Code style. --- ## [1.8.0 - Bigger is Better](https://github.com/onevcat/Kingfisher/releases/tag/1.8.0) (2015-11-07) #### Add * Support for tvOS. Now enjoy downloading and cacheing images in the tvOS. #### Fix * An issue which causes images not stored properly if the original data is not supplied. #142 --- ## [1.7.1 - EXIF is JPEG!](https://github.com/onevcat/Kingfisher/releases/tag/1.7.1) (2015-10-27) #### Fix * EXIF JPEG support which was broken in 1.7.0. * Correct timing of completion handler for use case with transition of UIImageView extension. --- ## [1.7.0 - Kingfisher with animation](https://github.com/onevcat/Kingfisher/releases/tag/1.7.0) (2015-10-25) #### Add * GIF support. Now you can download and show an animated GIF by Kingfisher `UIImageView` extension. #### Fix * Type safe options. * A potential retain of cache in loading task. --- ## [1.6.1 - No More Blinking](https://github.com/onevcat/Kingfisher/releases/tag/1.6.1) (2015-10-09) #### Fix * The blinking when reloading images in a cell. * Indicator is now in center of image view. --- ## [1.6.0 - Transition](https://github.com/onevcat/Kingfisher/releases/tag/1.6.0) (2015-09-19) #### Add * Add transition option. You can now use some view transition (like fade in) easier. #### Fix * Image data presenting when storing in disk. --- ## [1.5.0 - Swift 2.0](https://github.com/onevcat/Kingfisher/releases/tag/1.5.0) (2015-09-10) #### Add * Support for Swift 2.0. #### Fix * Remove the disk retrieve task canceling temporarily since there is an issue in Xcode 7 beta. * Remove support for watchOS since it now requires a separated framework. It will be added later as a standalone library instead a fat one. --- ## [1.4.5 - Key decoupling](https://github.com/onevcat/Kingfisher/releases/tag/1.4.5) (2015-08-14) #### Fix * Added resource APIs so you can specify a cacheKey for an image. The default implementation will use the URL string as key. --- ## [1.4.4 - Bug fix release](https://github.com/onevcat/Kingfisher/releases/tag/1.4.4) (2015-08-07) #### Fix * Explicitly type casting in ImageCache. #86 --- ## [1.4.3](https://github.com/onevcat/Kingfisher/releases/tag/1.4.0) (2015-08-06) #### Fix * Fix orientation of PNG files. * Indicator hiding logic. --- ## [1.4.2 - Scaling](https://github.com/onevcat/Kingfisher/releases/tag/1.4.0) (2015-07-09) #### Add * Support for store and decode with scale parameter. #### Fix * A retain cycle which prevents image retrieving task releasing. --- ## [1.4.1](https://github.com/onevcat/Kingfisher/releases/tag/1.4.1) (2015-05-12) #### Fix * Fix library dependency to weak link for WatchKit. --- ## [1.4.0 - Hello, Apple Watch](https://github.com/onevcat/Kingfisher/releases/tag/1.4.0) (2015-05-11) #### Add * Apple Watch support and category on `WKInterfaceImage`. --- ## [1.3.1](https://github.com/onevcat/Kingfisher/releases/tag/1.3.1) (2015-05-06) #### Fix * Fix tests for CI. --- ## [1.3.0 - 304? What is 304?](https://github.com/onevcat/Kingfisher/releases/tag/1.3.0) (2015-05-01) #### Add * ImageDownloaderDelegate for getting information from response. * A cacheType key in completion handler to let you know which does the image come from. * A notification when disk images are cleaned due to image expired or size exceeded. #### Fix * Changed `ForceRefresh` behavior to respect server response when got a 304. * Documentation and test coverage. --- ## [1.2.0 - More, I need more!](https://github.com/onevcat/Kingfisher/releases/tag/1.2.0) (2015-04-24) #### Add * 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. * Changed `Options` to `OptionsInfo` for flexible options passing. #### Fix * An issue which preventing image downloading when modifying the url of request. ### Deprecate * All extension methods with `KingfisherOptions` are deprecated now. Use `KingfisherOptionsInfo` instead. --- ## [1.1.3 - Internal is Important](https://github.com/onevcat/Kingfisher/releases/tag/1.1.3) (2015-04-23) #### Fix * Update the naming convention used in internal queues, for easier debug purpose. * Fix some tests. --- ## [1.1.2 - Who cares disk size](https://github.com/onevcat/Kingfisher/releases/tag/1.1.1) (2015-04-21) #### Add * API for calculation total disk cache size. * API for modifying request before sending it. * Handle challenge when accessing a server trust site. #### Fix * Fix grammar in README. * Fix demo project to make it simpler. --- ## [1.1.1](https://github.com/onevcat/Kingfisher/releases/tag/1.1.1) (2015-04-17) #### Fix * Update PodSpec version --- ## [1.1.0 - Not only image](https://github.com/onevcat/Kingfisher/releases/tag/1.1.0) (2015-04-17) #### Add * UIButton extension. #### Fix * Fix typo in project. * Improve documentation. --- ## [1.0.0 - Kingfisher, take off](https://github.com/onevcat/Kingfisher/releases/tag/1.0.0) (2015-04-13) First public release. ================================================ FILE: CONTRIBUTING.md ================================================ # Contribute ## Introduction First, thank you for considering contributing to Kingfisher! It's people like you that make the open source community such a great community! 😊 We welcome any type of contribution, not only code. You can help with - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) - **Marketing**: writing blog posts, howto's, printing stickers, ... - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... - **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. - **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/Kingfisher). ## Your First Contribution Working 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). ## Submitting code Any 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. ## Code review process The 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. It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? ## Financial contributions We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/Kingfisher). Anyone 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. ## Questions If 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!). You can also reach us at onev@onevcat.com. ## Credits ### Contributors Thank you to all the people who have already contributed to Kingfisher! ### Backers Thank you to all our backers! [[Become a backer](https://opencollective.com/Kingfisher#backer)] ### Sponsors Thank 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)) ================================================ FILE: Demo/Demo/Kingfisher-Demo/AppDelegate.swift ================================================ // // AppDelegate.swift // Kingfisher-Demo // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { return true } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Demo/Demo/Kingfisher-Demo/Extensions/UIViewController+KingfisherOperation.swift ================================================ // // UIViewController+KingfisherOperation.swift // Kingfisher // // Created by onevcat on 2018/11/18. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher protocol MainDataViewReloadable { @MainActor func reload() } extension UITableViewController: MainDataViewReloadable { func reload() { tableView.reloadData() } } extension UICollectionViewController: MainDataViewReloadable { func reload() { collectionView.reloadData() } } protocol KingfisherActionAlertPopup { @MainActor func alertPopup(_ sender: Any) -> UIAlertController } @MainActor func cleanCacheAction() -> UIAlertAction { return UIAlertAction(title: "Clean Cache", style: .default) { _ in KingfisherManager.shared.cache.clearMemoryCache() KingfisherManager.shared.cache.clearDiskCache() } } @MainActor func reloadAction(_ reloadable: any MainDataViewReloadable) -> UIAlertAction { return UIAlertAction(title: "Reload", style: .default) { _ in reloadable.reload() } } @MainActor let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) @MainActor func createAlert(_ sender: Any, actions: [UIAlertAction]) -> UIAlertController { let alert = UIAlertController(title: "Action", message: nil, preferredStyle: .actionSheet) alert.popoverPresentationController?.barButtonItem = sender as? UIBarButtonItem alert.popoverPresentationController?.permittedArrowDirections = .any actions.forEach { alert.addAction($0) } return alert } extension UIViewController: KingfisherActionAlertPopup { @objc func alertPopup(_ sender: Any) -> UIAlertController { let alert = createAlert(sender, actions: [cleanCacheAction(), cancelAction]) if let r = self as? any MainDataViewReloadable { alert.addAction(reloadAction(r)) } return alert } } extension UIViewController { func setupOperationNavigationBar() { navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Action", style: .plain, target: self, action: #selector(performKingfisherAction)) } @objc func performKingfisherAction(_ sender: Any) { present(alertPopup(sender), animated: true) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "1x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "2x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "1x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "2x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "1x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "2x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "1x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "2x" }, { "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" }, { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 4.6.2 CFBundleSignature ???? CFBundleVersion 1244 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Demo/Demo/Kingfisher-Demo/LaunchScreen.storyboard ================================================ ================================================ FILE: Demo/Demo/Kingfisher-Demo/Resources/ImageLoader.swift ================================================ // // ImageLoader.swift // Kingfisher // // Created by onevcat on 2018/11/18. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation struct ImageLoader { static let sampleImageURLs: [URL] = { let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading" return (1...10).map { URL(string: "\(prefix)/kingfisher-\($0).jpg")! } }() static let orientationImageURLs: [URL] = { let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Orientation" return (1...16).map { URL(string: "\(prefix)/\($0).jpg")! } }() static let highResolutionImageURLs: [URL] = { let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/HighResolution" return (1...20).map { URL(string: "\(prefix)/\($0).jpg")! } }() static let gifImageURLs: [URL] = { let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF" return (1...3).map { URL(string: "\(prefix)/\($0).gif")! } }() static let progressiveImageURL: URL = { let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Progressive" return URL(string: "\(prefix)/progressive.jpg")! }() static func roseImage(index: Int) -> URL { return URL(string: "https://github.com/onevcat/Flower-Data-Set/raw/master/rose/rose-\(index).jpg")! } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/AnimatedImageDemo.swift ================================================ // // AnimatedImageDemo.swift // Kingfisher // // Created by wangxingbin on 2021/4/27. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct AnimatedImageDemo: View { @State private var index = 1 var url: URL { ImageLoader.gifImageURLs[index - 1] } var body: some View { VStack { KFAnimatedImage(url) .configure { view in view.framePreloadCount = 3 } .cacheOriginalImage() .onSuccess { r in print("suc: \(r)") } .onFailure { e in print("err: \(e)") } .placeholder { p in ProgressView(p) } .fade(duration: 1) .forceTransition() .aspectRatio(contentMode: .fill) .frame(width: 300, height: 300) .cornerRadius(20) .shadow(radius: 5) .frame(width: 320, height: 320) Button(action: { self.index = (self.index % 3) + 1 }) { Text("Next Image") } }.navigationBarTitle(Text("Basic Image"), displayMode: .inline) } } @available(iOS 14.0, *) struct AnimatedImageDemo_Previews: PreviewProvider { static var previews: some View { AnimatedImageDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/GeometryReaderDemo.swift ================================================ // // GeometryReaderDemo.swift // Kingfisher // // Created by onevcat on 2021/06/12. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct GeometryReaderDemo: View { var body: some View { GeometryReader { geo in KFImage( ImageLoader.sampleImageURLs.first ) .placeholder { ProgressView() } .forceRefresh() .resizable() .scaledToFit() .frame(width: geo.size.width) } } } @available(iOS 14.0, *) struct GeometryReaderDemo_Previews: PreviewProvider { static var previews: some View { GeometryReaderDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/GridDemo.swift ================================================ // // GridDemo.swift // Kingfisher // // Created by onevcat on 2021/03/02. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI @available(iOS 14.0, *) struct GridDemo: View { @State var columns = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ] var body: some View { ScrollView { LazyVGrid(columns: columns) { ForEach(1..<700) { i in ImageCell(index: i).frame(height: columns.count == 1 ? 300 : 150) } } }.navigationBarTitle(Text("Grid")) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button(action: { withAnimation(Animation.easeInOut(duration: 0.25)) { self.columns = Array(repeating: .init(.flexible()), count: self.columns.count % 4 + 1) } }) { Image(systemName: "square.grid.2x2") .font(.title) .foregroundColor(.primary) } } } } } @available(iOS 14.0, *) struct GridDemo_Previews: PreviewProvider { static var previews: some View { GridDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/LazyVStackDemo.swift ================================================ // // LazyVStackDemo.swift // Kingfisher // // Created by onevcat on 2021/03/02. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct LazyVStackDemo: View { @State private var singleImage = false var body: some View { ScrollView { // Checking for #1839 Toggle("Single Image", isOn: $singleImage).padding() LazyVStack { ForEach(1..<700) { i in if singleImage { KFImage.url(ImageLoader.roseImage(index: 1)) } else { ImageCell(index: i).frame(width: 300, height: 300) } } } }.navigationBarTitle(Text("Lazy Stack"), displayMode: .inline) } } @available(iOS 14.0, *) struct LazyVStackDemo_Previews: PreviewProvider { static var previews: some View { LazyVStackDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/ListDemo.swift ================================================ // // ListDemo.swift // Kingfisher // // Created by Wei Wang on 2019/06/18. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Kingfisher import SwiftUI @available(iOS 14.0, *) struct ListDemo : View { var body: some View { List(1 ..< 700) { i in ImageCell(index: i) .frame(height: 300) }.navigationBarTitle(Text("SwiftUI List"), displayMode: .inline) } } @available(iOS 14.0, *) struct ImageCell: View { var alreadyCached: Bool { ImageCache.default.isCached(forKey: url.absoluteString) } let index: Int var url: URL { URL(string: "https://github.com/onevcat/Flower-Data-Set/raw/master/rose/rose-\(index).jpg")! } var body: some View { HStack(alignment: .center) { Spacer() KFImage.url(url) .resizable() .onSuccess { r in print("Success: \(self.index) - \(r.cacheType)") } .onFailure { e in print("Error \(self.index): \(e)") } .onProgress { downloaded, total in print("\(downloaded) / \(total))") } .placeholder { HStack { Image(systemName: "arrow.2.circlepath.circle") .resizable() .frame(width: 50, height: 50) .padding(10) Text("Loading...").font(.title) } .foregroundColor(.gray) } .fade(duration: 1) .cancelOnDisappear(true) .aspectRatio(contentMode: .fit) .cornerRadius(20) Spacer() }.padding(.vertical, 12) } } @available(iOS 14.0, *) struct SwiftUIList_Previews : PreviewProvider { static var previews: some View { ListDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/LoadTransitionDemo.swift ================================================ // // LoadTransitionDemo.swift // Kingfisher // // Copyright (c) 2025 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct LoadTransitionDemo: View { @State private var imageIndex = 0 @State private var currentTransition: TransitionType = .none let columns = [ GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10) ] var body: some View { VStack(spacing: 20) { // Image display area Group { switch currentTransition { case .none: KFImage(currentTransition.url) .placeholder { placeholderView } .contentConfigure { content in content .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) } .forceTransition() .resizable() .aspectRatio(contentMode: .fit) case .fade: KFImage(currentTransition.url) .placeholder { placeholderView } .contentConfigure { content in content .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) } .forceTransition() .fade(duration: 0.5) .resizable() .aspectRatio(contentMode: .fit) default: KFImage(currentTransition.url) .placeholder { placeholderView } .contentConfigure { content in content .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) } .forceTransition() .loadTransition(currentTransition.transition, animation: currentTransition.animation) .resizable() .aspectRatio(contentMode: .fit) } } .padding(16) .frame(width: 300, height: 300) .background(Color.gray.opacity(0.3)) .cornerRadius(16) .shadow(radius: 5) Spacer() // Transition buttons LazyVGrid(columns: columns, spacing: 15) { ForEach(TransitionType.allCases, id: \.self) { type in Button(action: { // Clear cache to ensure transition is visible if let currentURL = URL(string: currentTransition.urlString) { KingfisherManager.shared.cache.removeImage(forKey: currentURL.absoluteString) } currentTransition = type }) { Text(type.rawValue) .font(.system(size: 14, weight: .medium)) .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(currentTransition == type ? Color.blue : Color.gray) .foregroundColor(.white) .cornerRadius(8) } } } .padding(.horizontal) Spacer() } .navigationBarTitle("Load Transition", displayMode: .inline) } private var placeholderView: some View { RoundedRectangle(cornerRadius: 8) .fill(Color.gray.opacity(0.2)) .overlay(ProgressView()) } enum TransitionType: String, CaseIterable { case none = "None" case fade = "Fade" case slide = "Slide" case scale = "Scale" case opacity = "Opacity" case blurReplace = "Blur" @MainActor var transition: AnyTransition { switch self { case .none, .fade: return .identity case .slide: return .slide case .scale: return .scale case .opacity: return .opacity case .blurReplace: if #available(iOS 17.0, *) { return AnyTransition(.blurReplace()) } else { return .scale // Fallback for iOS < 17 } } } var animation: Animation? { switch self { case .none, .fade: return nil case .slide: return .easeInOut(duration: 0.5) case .scale: return .spring() case .opacity: return .easeInOut(duration: 0.4) case .blurReplace: if #available(iOS 17.0, *) { return .bouncy(duration: 0.8) } else { return .spring() } } } var urlString: String { let index = TransitionType.allCases.firstIndex(of: self) ?? 0 let urls = ImageLoader.sampleImageURLs return urls[index % urls.count].absoluteString } var url: URL { URL(string: urlString)! } } } @available(iOS 14.0, *) struct LoadTransitionDemo_Previews: PreviewProvider { static var previews: some View { LoadTransitionDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/LoadingFailureDemo.swift ================================================ // // LoadingFailureDemo.swift // Kingfisher // // Created by onevcat on 2025/06/29. // // Copyright (c) 2025 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct LoadingFailureDemo: View { var url: URL { URL(string: "https://example.com")! } var warningImage: UIImage { let config = UIImage.SymbolConfiguration(pointSize: 50) return UIImage( systemName: "wrongwaysign", withConfiguration: config )! } var body: some View { VStack { KFImage(url) .onFailureView { ZStack { RoundedRectangle(cornerRadius: 20) .fill(Color.red.opacity(0.5)) Image(systemName: "exclamationmark.triangle.fill") .resizable() .frame(width: 50, height: 47) .foregroundColor(.yellow) } } .frame(width: 200, height: 200) Text("onFailureView") Spacer().frame(height: 20) } } } @available(iOS 14.0, *) struct LoadingFailureDemo_Previews: PreviewProvider { static var previews: some View { LoadingFailureDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/MainView.swift ================================================ // // MainView.swift // Kingfisher // // Created by onevcat on 2019/08/07. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct MainView: View { var body: some View { List { Section { Button( action: { KingfisherManager.shared.cache.clearMemoryCache() KingfisherManager.shared.cache.clearDiskCache() }, label: { Text("Clear Cache").foregroundColor(.blue) } ) } Section(header: Text("Demo")) { NavigationLink(destination: SingleViewDemo()) { Text("Basic Image") } NavigationLink(destination: SizingAnimationDemo()) { Text("Sizing Toggle") } NavigationLink(destination: ListDemo()) { Text("List") } NavigationLink(destination: LazyVStackDemo()) { Text("Stack") } NavigationLink(destination: GridDemo()) { Text("Grid") } NavigationLink(destination: AnimatedImageDemo()) { Text("Animated Image") } NavigationLink(destination: GeometryReaderDemo()) { Text("Geometry Reader") } NavigationLink(destination: TransitionViewDemo()) { Text("Transition") } NavigationLink(destination: LoadTransitionDemo()) { Text("Load Transition") } NavigationLink(destination: ProgressiveJPEGDemo()) { Text("Progressive JPEG") } NavigationLink(destination: LoadingFailureDemo()) { Text("Loading Failure") } NavigationLink(destination: PhotosPickerDemo()) { Text("Photos Picker") } } Section(header: Text("Regression Cases")) { NavigationLink(destination: Issue1998View()) { Text("#1998") } NavigationLink(destination: Issue2035View()) { Text("#2035") } NavigationLink(destination: Issue2295View()) { Text("#2295") } NavigationLink(destination: Issue2352View()) { Text("#2352") } } }.navigationBarTitle(Text("SwiftUI Sample")) } } @available(iOS 14.0, *) struct MainView_Previews: PreviewProvider { static var previews: some View { MainView() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/PhotosPickerDemo.swift ================================================ // // PhotosPickerDemo.swift // Kingfisher // // Created by nuomi1 on 2026/1/7. // // Copyright (c) 2026 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import Kingfisher import PhotosUI import SwiftUI struct PhotosPickerDemo: View { @State private var isPresented = false var body: some View { if #available(iOS 16.0, *) { PhotosPickerRealDemo() } else { Button { isPresented = true } label: { Text("Tap Me") } .alert(isPresented: $isPresented) { Alert(title: Text("Warning!"), message: Text("Only supports iOS 16+")) } } } } @available(iOS 16.0, *) private struct PhotosPickerRealDemo: View { @State private var pickerItem: PhotosPickerItem? var body: some View { if let pickerItem { KFImage .dataProvider(PhotosPickerItemImageDataProvider(pickerItem: pickerItem)) } PhotosPicker( "Tap Me", selection: $pickerItem, matching: .images, photoLibrary: .shared() ) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/ProgressiveJPEGDemo.swift ================================================ // // ProgressiveJPEGDemo.swift // Kingfisher // // Created by onevcat on 2025/03/03. // // Copyright (c) 2025 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Kingfisher import SwiftUI @available(iOS 14.0, *) struct ProgressiveJPEGDemo: View { @State private var totalSize: Int64? @State private var receivedSize: Int64? var body: some View { KFImage(ImageLoader.progressiveImageURL) .progressiveJPEG() .onProgress({ receivedSize, totalSize in self.totalSize = totalSize self.receivedSize = receivedSize }) .resizable() .frame(width: 300, height: 300) if let totalSize = totalSize, let receivedSize = receivedSize { Text("Received: \(receivedSize) / \(totalSize)") } } } @available(iOS 14.0, *) struct ProgressiveJPEGDemo_Previews : PreviewProvider { static var previews: some View { ProgressiveJPEGDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/Regression/Issue1998View.swift ================================================ // // SingleListDemo.swift // Kingfisher // // Created by onevcat on 2022/09/21. // // Copyright (c) 2022 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct Issue1998View: View { var body: some View { Text("This is a test case for #1988") List { ForEach(1...100, id: \.self) { idx in KFImage(ImageLoader.sampleImageURLs.first) .startLoadingBeforeViewAppear() .resizable() .frame(width: 48, height: 48) } } } } @available(iOS 14.0, *) struct SingleListDemo_Previews: PreviewProvider { static var previews: some View { Issue1998View() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/Regression/Issue2035View.swift ================================================ // // Issue2035View.swift // Kingfisher // // Created by jp20028 on 2023/02/23. // // Copyright (c) 2023 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct Issue2035View: View { var body: some View { KFImage(nil) .startLoadingBeforeViewAppear() .onSuccess { _ in print("Done") } .onFailure { err in print(err) } } } @available(iOS 14.0, *) struct Issue2035View_Previews: PreviewProvider { static var previews: some View { Issue2035View() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/Regression/Issue2295View.swift ================================================ // // Issue2295View.swift // Kingfisher // // Created by onevcat on 2024/09/21. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct Issue2295View: View { @State private var count = 0 var body: some View { Text("This is a test case for #2295") Text("Count: \(count)") ScrollView { VStack { Text("Tapping these to add count.") HStack { KFImage(ImageLoader.sampleImageURLs.first) .resizable() .frame(width: 150, height: 150) .onTapGesture { count += 1 } KFAnimatedImage(ImageLoader.sampleImageURLs.first) .frame(width: 150, height: 150) .onTapGesture { count += 1 } } } Divider() VStack { Text("These are not tappable.") HStack { KFImage(ImageLoader.sampleImageURLs.first) .resizable() .frame(width: 150, height: 150) .allowsHitTesting(false) .onTapGesture { count += 1 } KFAnimatedImage(ImageLoader.sampleImageURLs.first) .frame(width: 150, height: 150) .allowsHitTesting(false) .onTapGesture { count += 1 } } } } } } @available(iOS 14.0, *) struct Issue2295View_Previews: PreviewProvider { static var previews: some View { Issue1998View() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/Regression/Issue2352View.swift ================================================ // // Issue2352View.swift // Kingfisher // // Created by onevcat on 2025/02/04. // // Copyright (c) 2025 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct Issue2352View: View { var body: some View { List { ForEach(0..<40, id: \.self) { row in KFAnimatedImage .url( URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/refs/heads/master/DemoAppImage/GIF/jumping.gif")! ) .backgroundDecode() .scaleFactor(UIScreen.main.scale) .scaledToFill() .frame(width: 50, height: 50) .clipShape(.circle) } } } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/SingleViewDemo.swift ================================================ // // SingleViewDemo.swift // Kingfisher // // Created by Wei Wang on 2019/06/18. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Kingfisher import SwiftUI @available(iOS 14.0, *) struct SingleViewDemo : View { @State private var index = 1 @State private var blackWhite = false @State private var forceTransition = true var url: URL { URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher-\(self.index).jpg")! } var body: some View { VStack { KFImage(url) .cacheOriginalImage() .setProcessor(blackWhite ? BlackWhiteProcessor() : DefaultImageProcessor()) .onSuccess { r in print("suc: \(r)") } .onFailure { e in print("err: \(e)") } .placeholder { progress in ProgressView(progress).frame(width: 100, height: 100) .border(Color.blue) } .fade(duration: index == 1 ? 0 : 1) // Do not animate for the first image. Otherwise it causes an unwanted animation when the page is shown. .forceTransition(forceTransition) .resizable() .frame(width: 300, height: 300) .cornerRadius(20) .border(Color.red) .shadow(radius: 5) .frame(width: 320, height: 320) Button(action: { self.index = (self.index % 10) + 1 }) { Text("Next Image") } Button(action: { self.blackWhite.toggle() }) { Text("Black & White") } Toggle("Force Transition?", isOn: $forceTransition) .frame(width: 300) }.navigationBarTitle(Text("Basic Image"), displayMode: .inline) } } @available(iOS 14.0, *) struct SingleViewDemo_Previews : PreviewProvider { static var previews: some View { SingleViewDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/SizingAnimationDemo.swift ================================================ // // SizingAnimationDemo.swift // Kingfisher // // Created by onevcat on 2021/03/02. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct SizingAnimationDemo: View { @State var imageSize: CGFloat = 250 @State var isPlaying = false var body: some View { VStack { KFImage(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher-1.jpg")!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: imageSize, height: imageSize) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .frame(width: 350, height: 350) Button(action: { playButtonAction() }) { Image(systemName: self.isPlaying ? "pause.fill" : "play.fill") .font(.system(size: 60)) } } } func playButtonAction() { withAnimation(Animation.spring(response: 0.45, dampingFraction: 0.475, blendDuration: 0)) { if self.imageSize == 250 { self.imageSize = 350 } else { self.imageSize = 250 } self.isPlaying.toggle() } } } @available(iOS 14.0, *) struct SizingAnimationDemo_Previews: PreviewProvider { static var previews: some View { SizingAnimationDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/SwiftUIViews/TransitionViewDemo.swift ================================================ // // TransitionViewDemo.swift // Kingfisher // // Created by onevcat on 2021/08/03. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(iOS 14.0, *) struct TransitionViewDemo: View { @State private var showDetails = false var body: some View { VStack { Button(showDetails ? "Hide" : "Show") { withAnimation { showDetails.toggle() } } if showDetails { KFImage(ImageLoader.sampleImageURLs.first) .transition(.slide) } Spacer() }.frame(height: 500) } } @available(iOS 14.0, *) struct TransitionViewDemo_Previews: PreviewProvider { static var previews: some View { TransitionViewDemo() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/AVAssetImageGeneratorViewController.swift ================================================ // // AVAssetImageGeneratorViewController.swift // Kingfisher // // Created by onevcat on 2020/08/09. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import AVKit import Kingfisher class AVAssetImageGeneratorViewController: UIViewController { @IBOutlet weak var imageView: UIImageView! override func viewDidLoad() { super.viewDidLoad() let provider = AVAssetImageDataProvider( assetURL: URL(string: "https://github.com/onevcat/sample-files/raw/main/video/mp4/astronaut_flying_fantasy.mp4")!, seconds: 6.0 ) KF.dataProvider(provider).set(to: imageView) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/AutoSizingTableViewController.swift ================================================ // // AutoSizingTableViewController.swift // Kingfisher // // Created by onevcat on 2021/03/15. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher // Cell with an image view (loading by Kingfisher) with fix width and dynamic height which keeps the image with aspect ratio. class AutoSizingTableViewCell: UITableViewCell { static let p = ResizingImageProcessor(referenceSize: .init(width: 200, height: CGFloat.infinity), mode: .aspectFit) @IBOutlet weak var leadingImageView: UIImageView! @IBOutlet weak var sizeLabel: UILabel! var updateLayout: (() -> Void)? func set(with url: URL) { leadingImageView.kf.setImage(with: url, options: [.processor(AutoSizingTableViewCell.p), .transition(.fade(1))]) { r in if case .success(let value) = r { self.sizeLabel.text = "\(value.image.size.width) x \(value.image.size.height)" self.updateLayout?() } else { self.sizeLabel.text = "" } } } } class AutoSizingTableViewController: UIViewController { @IBOutlet weak var tableView: UITableView! var data: [Int] = Array(1..<700) override func viewDidLoad() { super.viewDidLoad() tableView.estimatedRowHeight = 150 } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) UIView.setAnimationsEnabled(false) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) UIView.setAnimationsEnabled(true) } } extension AutoSizingTableViewController: UITableViewDataSource { private func updateLayout() { tableView.beginUpdates() tableView.endUpdates() } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return data.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "AutoSizingTableViewCell", for: indexPath) as! AutoSizingTableViewCell cell.set(with: ImageLoader.roseImage(index: data[indexPath.row])) cell.updateLayout = { [weak self] in self?.updateLayout() } return cell } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/DetailImageViewController.swift ================================================ // // DetailImageViewController.swift // Kingfisher // // Created by onevcat on 2018/11/25. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit class DetailImageViewController: UIViewController { var imageURL: URL! @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var infoLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() scrollView.delegate = self imageView.kf.setImage(with: imageURL, options: [.memoryCacheExpiration(.expired)]) { result in guard let image = try? result.get().image else { return } let scrollViewFrame = self.scrollView.frame let scaleWidth = scrollViewFrame.size.width / image.size.width let scaleHeight = scrollViewFrame.size.height / image.size.height let minScale = min(scaleWidth, scaleHeight) self.scrollView.minimumZoomScale = minScale DispatchQueue.main.async { self.scrollView.zoomScale = minScale } self.infoLabel.text = "\(image.size)" } } } extension DetailImageViewController: UIScrollViewDelegate { func viewForZooming(in scrollView: UIScrollView) -> UIView? { return imageView } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/GIFHeavyViewController.swift ================================================ // // GIFHeavyViewController.swift // Kingfisher // // Created by taras on 16/04/2021. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher class GIFHeavyViewController: UIViewController { let stackView = UIStackView() let imageView_1 = AnimatedImageView() let imageView_2 = AnimatedImageView() let imageView_3 = AnimatedImageView() let imageView_4 = AnimatedImageView() override func viewDidLoad() { super.viewDidLoad() stackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stackView) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) } else { NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), stackView.topAnchor.constraint(equalTo: view.topAnchor), stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } stackView.axis = .vertical stackView.distribution = .fillEqually stackView.addArrangedSubview(imageView_1) stackView.addArrangedSubview(imageView_2) stackView.addArrangedSubview(imageView_3) stackView.addArrangedSubview(imageView_4) imageView_1.contentMode = .scaleAspectFit imageView_2.contentMode = .scaleAspectFit imageView_3.contentMode = .scaleAspectFit imageView_4.contentMode = .scaleAspectFit let url = URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/GifHeavy.gif") imageView_1.kf.setImage(with: url) imageView_2.kf.setImage(with: url) imageView_3.kf.setImage(with: url) imageView_4.kf.setImage(with: url) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/GIFViewController.swift ================================================ // // GIFViewController.swift // Kingfisher // // Created by onevcat on 2018/11/25. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher class GIFViewController: UIViewController { @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var animatedImageView: AnimatedImageView! override func viewDidLoad() { super.viewDidLoad() let url = ImageLoader.gifImageURLs.last! // Should need to use different cache key to prevent data overwritten by each other. KF.url(url, cacheKey: "\(url)-imageview").set(to: imageView) KF.url(url, cacheKey: "\(url)-animated_imageview").set(to: animatedImageView) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/HighResolutionCollectionViewController.swift ================================================ // // HighResolutionCollectionViewController.swift // Kingfisher // // Created by onevcat on 2018/11/24. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher private let reuseIdentifier = "HighResolution" class HighResolutionCollectionViewController: UICollectionViewController { override func viewDidLoad() { super.viewDidLoad() title = "High Resolution" setupOperationNavigationBar() } override func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return ImageLoader.highResolutionImageURLs.count * 30 } override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { (cell as! ImageCollectionViewCell).cellImageView.kf.cancelDownloadTask() } override func collectionView( _ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { let imageView = (cell as! ImageCollectionViewCell).cellImageView! let url = ImageLoader.highResolutionImageURLs[indexPath.row % ImageLoader.highResolutionImageURLs.count] // Use different cache key to prevent reuse the same image. It is just for // this demo. Normally you can just use the URL to set image. // This should crash most devices due to memory pressure. // let resource = KF.ImageResource(downloadURL: url, cacheKey: "\(url.absoluteString)-\(indexPath.row)") // imageView.kf.setImage(with: resource) // This would survive on even the lowest spec devices! KF.url(url, cacheKey: "\(url.absoluteString)-\(indexPath.row)") .downsampling(size: CGSize(width: 250, height: 250)) .cacheOriginalImage() .set(to: imageView) } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) return cell } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showImage" { let vc = segue.destination as! DetailImageViewController let index = collectionView.indexPathsForSelectedItems![0].row vc.imageURL = ImageLoader.highResolutionImageURLs[index % ImageLoader.highResolutionImageURLs.count] } } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/ImageCollectionViewCell.swift ================================================ // // ImageCollectionViewCell.swift // Kingfisher-Demo // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit class ImageCollectionViewCell: UICollectionViewCell { @IBOutlet weak var cellImageView: UIImageView! #if os(tvOS) override func awakeFromNib() { super.awakeFromNib() cellImageView.adjustsImageWhenAncestorFocused = true cellImageView.clipsToBounds = false } #endif } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/ImageDataProviderCollectionViewController.swift ================================================ // // ImageDataProviderCollectionViewController.swift // Kingfisher // // Created by onevcat on 2018/12/08. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher private let reuseIdentifier = "ImageDataProviderCell" class ImageDataProviderCollectionViewController: UICollectionViewController { let model: [(String, UIColor)] = [ ("A", .red), ("B", .green), ("C", .blue), ("D", .yellow), ("赵", .purple), ("钱", .orange), ("孙", .black), ("李", .brown), ("ア", .darkGray), ("イ", .cyan), ("ウ", .magenta)] override func viewDidLoad() { super.viewDidLoad() title = "Provider" setupOperationNavigationBar() } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return model.count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell let pair = model[indexPath.row] let provider = UserNameLetterIconImageProvider(userNameFirstLetter: pair.0, backgroundColor: pair.1) KF.dataProvider(provider) .roundCorner(radius: .point(75)) .set(to: cell.cellImageView) return cell } } struct UserNameLetterIconImageProvider: ImageDataProvider { var cacheKey: String { return letter } let letter: String let color: UIColor init(userNameFirstLetter: String, backgroundColor: UIColor) { letter = userNameFirstLetter color = backgroundColor } func data(handler: @escaping (Result) -> Void) { let letter = self.letter as NSString let rect = CGRect(x: 0, y: 0, width: 250, height: 250) let format = UIGraphicsImageRendererFormat.default() format.scale = 1 let renderer = UIGraphicsImageRenderer(size: rect.size, format: format) let data = renderer.pngData { context in color.setFill() context.fill(rect) let attributes = [ NSAttributedString.Key.foregroundColor: UIColor.white, .font: UIFont.systemFont(ofSize: 200) ] let textSize = letter.size(withAttributes: attributes) let textRect = CGRect( x: (rect.width - textSize.width) / 2, y: (rect.height - textSize.height) / 2, width: textSize.width, height: textSize.height) letter.draw(in: textRect, withAttributes: attributes) } handler(.success(data)) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/IndicatorCollectionViewController.swift ================================================ // // IndicatorCollectionViewController.swift // Kingfisher // // Created by onevcat on 2018/11/26. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher private let reuseIdentifier = "IndicatorCell" let gifData: Data = { let url = Bundle.main.url(forResource: "loader", withExtension: "gif")! return try! Data(contentsOf: url) }() class IndicatorCollectionViewController: UICollectionViewController { class MyIndicator: Indicator { var timer: Timer? func startAnimatingView() { view.isHidden = false timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { _ in Task { @MainActor in UIView.animate(withDuration: 0.2, animations: { if self.view.backgroundColor == .red { self.view.backgroundColor = .orange } else { self.view.backgroundColor = .red } }) } } } func stopAnimatingView() { view.isHidden = true timer?.invalidate() } var view: IndicatorView = { let view = UIView() view.heightAnchor.constraint(equalToConstant: 30).isActive = true view.widthAnchor.constraint(equalToConstant: 30).isActive = true view.backgroundColor = .red return view }() } let indicators: [String] = [ "None", "UIActivityIndicatorView", "GIF Image", "Custom" ] var selectedIndicatorIndex: Int = 1 { didSet { collectionView.reloadData() } } var selectedIndicatorType: IndicatorType { switch selectedIndicatorIndex { case 0: return .none case 1: return .activity case 2: return .image(imageData: gifData) case 3: return .custom(indicator: MyIndicator()) default: fatalError() } } override func viewDidLoad() { super.viewDidLoad() setupOperationNavigationBar() } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return ImageLoader.sampleImageURLs.count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell cell.cellImageView.kf.indicatorType = selectedIndicatorType KF.url(ImageLoader.sampleImageURLs[indexPath.row]) .memoryCacheExpiration(.expired) .diskCacheExpiration(.expired) .set(to: cell.cellImageView) return cell } override func alertPopup(_ sender: Any) -> UIAlertController { let alert = super.alertPopup(sender) for item in indicators.enumerated() { alert.addAction(UIAlertAction.init(title: item.element, style: .default) { _ in self.selectedIndicatorIndex = item.offset }) } return alert } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/InfinityCollectionViewController.swift ================================================ // // InfinityCollectionViewController.swift // Kingfisher // // Created by Wei Wang on 2018/11/19. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher private let reuseIdentifier = "InfinityCell" class InfinityCollectionViewController: UICollectionViewController { override func viewDidLoad() { super.viewDidLoad() title = "Infinity" setupOperationNavigationBar() } override func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 10000000 } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView .dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell let urls = ImageLoader.sampleImageURLs let url = urls[indexPath.row % urls.count] // Mark each row as a new image. let resource = KF.ImageResource(downloadURL: url, cacheKey: "key-\(indexPath.row)") KF.resource(resource).set(to: cell.cellImageView) return cell } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/LivePhotoViewController.swift ================================================ // // LivePhotoViewController.swift // Kingfisher // // Created by onevcat on 2024/10/05. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import PhotosUI import Kingfisher class LivePhotoViewController: UIViewController { private var livePhotoView: PHLivePhotoView! override func viewDidLoad() { super.viewDidLoad() title = "Live Photo" setupOperationNavigationBar() livePhotoView = PHLivePhotoView() livePhotoView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(livePhotoView) NSLayoutConstraint.activate([ livePhotoView.heightAnchor.constraint(equalToConstant: 300), livePhotoView.widthAnchor.constraint(equalToConstant: 300), livePhotoView.centerXAnchor.constraint(equalTo: view.centerXAnchor), livePhotoView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -30) ]) let urls = [ "https://github.com/onevcat/Kingfisher-TestImages/raw/refs/heads/master/LivePhotos/live_photo_sample.HEIC", "https://github.com/onevcat/Kingfisher-TestImages/raw/refs/heads/master/LivePhotos/live_photo_sample.MOV" ].compactMap(URL.init) livePhotoView.kf.setImage(with: urls, completionHandler: { result in switch result { case .success(let r): print("Live Photo done. \(r.loadingInfo.cacheType)") print("Info: \(String(describing: r.info))") case .failure(let error): print("Live Photo error: \(error)") } }) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/MainViewController.swift ================================================ // // MainViewController.swift // Kingfisher // // Created by onevcat on 2018/11/18. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit class MainViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() title = "Kingfisher" setupOperationNavigationBar() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/NetworkMetricsViewController.swift ================================================ // // NetworkMetricsViewController.swift // Demo // // Created by FunnyValentine on 2025/07/25. // import UIKit import Kingfisher class NetworkMetricsViewController: UIViewController { // MARK: - UI Components private let imageView = UIImageView() private let metricsTextView = UITextView() private let fromNetworkButton = UIButton(type: .system) private let fromMemoryButton = UIButton(type: .system) private let fromDiskButton = UIButton(type: .system) private let stackView = UIStackView() private let buttonStackView = UIStackView() private let metricsContainer = UIView() // MARK: - Properties private var currentImageURL = URL(string: "https://picsum.photos/200/150?random=\(Int.random(in: 1...1000))")! private var showImage = true // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setupUI() setupConstraints() setupInitialContent() } // MARK: - UI Setup private func setupUI() { title = "Network Metrics" view.backgroundColor = .systemBackground setupImageView() setupMetricsTextView() setupButtons() setupStackViews() } private func setupImageView() { imageView.contentMode = .scaleAspectFit imageView.layer.cornerRadius = 8 imageView.clipsToBounds = true imageView.backgroundColor = .clear // Clear background imageView.translatesAutoresizingMaskIntoConstraints = false } private func setupMetricsTextView() { metricsTextView.isEditable = false metricsTextView.backgroundColor = UIColor.systemGray6 metricsTextView.layer.cornerRadius = 8 metricsTextView.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) metricsTextView.text = "Tap a button to load image..." metricsTextView.translatesAutoresizingMaskIntoConstraints = false } private func setupButtons() { setupButton(fromNetworkButton, title: "From Network", icon: "wifi", color: .systemRed, action: #selector(fromNetworkButtonTapped)) setupButton(fromMemoryButton, title: "From Memory", icon: "memorychip", color: .systemOrange, action: #selector(fromMemoryButtonTapped)) setupButton(fromDiskButton, title: "From Disk", icon: "internaldrive", color: .systemPurple, action: #selector(fromDiskButtonTapped)) } private func setupButton(_ button: UIButton, title: String, icon: String, color: UIColor, action: Selector) { button.setTitle(title, for: .normal) button.setImage(UIImage(systemName: icon), for: .normal) button.backgroundColor = color button.setTitleColor(.white, for: .normal) button.tintColor = .white button.layer.cornerRadius = 8 button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium) button.translatesAutoresizingMaskIntoConstraints = false button.addTarget(self, action: action, for: .touchUpInside) // Configure image and title positioning button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 0) button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) } private func setupStackViews() { // Button stack view buttonStackView.axis = .vertical buttonStackView.distribution = .fillEqually buttonStackView.spacing = 12 buttonStackView.translatesAutoresizingMaskIntoConstraints = false // Network button takes full width buttonStackView.addArrangedSubview(fromNetworkButton) // Memory and Disk buttons in horizontal stack let horizontalButtonStack = UIStackView() horizontalButtonStack.axis = .horizontal horizontalButtonStack.distribution = .fillEqually horizontalButtonStack.spacing = 12 horizontalButtonStack.addArrangedSubview(fromMemoryButton) horizontalButtonStack.addArrangedSubview(fromDiskButton) buttonStackView.addArrangedSubview(horizontalButtonStack) // Main stack view stackView.axis = .vertical stackView.spacing = 20 stackView.alignment = .center // Center align all items stackView.translatesAutoresizingMaskIntoConstraints = false setupMetricsSection() stackView.addArrangedSubview(imageView) stackView.addArrangedSubview(metricsContainer) stackView.addArrangedSubview(buttonStackView) view.addSubview(stackView) } private func setupMetricsSection() { metricsContainer.translatesAutoresizingMaskIntoConstraints = false let titleLabel = UILabel() titleLabel.text = "Metrics Information" titleLabel.font = UIFont.systemFont(ofSize: 18, weight: .semibold) titleLabel.translatesAutoresizingMaskIntoConstraints = false metricsContainer.addSubview(titleLabel) metricsContainer.addSubview(metricsTextView) NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: metricsContainer.topAnchor), titleLabel.leadingAnchor.constraint(equalTo: metricsContainer.leadingAnchor), titleLabel.trailingAnchor.constraint(equalTo: metricsContainer.trailingAnchor), titleLabel.heightAnchor.constraint(equalToConstant: 22), metricsTextView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), metricsTextView.leadingAnchor.constraint(equalTo: metricsContainer.leadingAnchor), metricsTextView.trailingAnchor.constraint(equalTo: metricsContainer.trailingAnchor), metricsTextView.bottomAnchor.constraint(equalTo: metricsContainer.bottomAnchor), metricsTextView.heightAnchor.constraint(equalToConstant: 400) ]) } private func setupConstraints() { NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), imageView.heightAnchor.constraint(equalToConstant: 150), imageView.widthAnchor.constraint(equalToConstant: 200), fromNetworkButton.heightAnchor.constraint(equalToConstant: 44), fromMemoryButton.heightAnchor.constraint(equalToConstant: 44), fromDiskButton.heightAnchor.constraint(equalToConstant: 44), // Make button stack view and metrics container full width buttonStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), buttonStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), metricsContainer.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), metricsContainer.trailingAnchor.constraint(equalTo: stackView.trailingAnchor) ]) } private func setupInitialContent() { // Set initial placeholder imageView.image = createPlaceholderImage(text: "Tap button to load") } // MARK: - Actions @objc private func fromNetworkButtonTapped() { // Set placeholder and hide image showImage = false imageView.image = createPlaceholderImage(text: "Reloading...") // Clear all cache to force network download KingfisherManager.shared.cache.clearCache() DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.showImage = true self.loadImage() } } @objc private func fromMemoryButtonTapped() { // Set placeholder and hide image showImage = false imageView.image = createPlaceholderImage(text: "Reloading...") // Clear disk cache only, keep memory cache KingfisherManager.shared.cache.clearDiskCache() DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.showImage = true self.loadImage() } } @objc private func fromDiskButtonTapped() { // Set placeholder and hide image showImage = false imageView.image = createPlaceholderImage(text: "Reloading...") // Clear memory cache only, keep disk cache KingfisherManager.shared.cache.clearMemoryCache() DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.showImage = true self.loadImage() } } // MARK: - Image Loading private func loadImage() { guard showImage else { return } let placeholder = createPlaceholderImage(text: "Loading...") imageView.kf.setImage( with: currentImageURL, placeholder: placeholder, options: nil, completionHandler: { [weak self] result in DispatchQueue.main.async { switch result { case .success(let retrieveImageResult): self?.displayMetrics(result: retrieveImageResult) case .failure(let error): self?.metricsTextView.text = "Failed to load image: \(error.localizedDescription)" print("Error: \(error)") } } } ) } // MARK: - Helper Methods private func createPlaceholderImage(text: String) -> UIImage { let size = CGSize(width: 200, height: 150) let renderer = UIGraphicsImageRenderer(size: size) return renderer.image { context in let rect = CGRect(origin: .zero, size: size) // Draw background with rounded corners let path = UIBezierPath(roundedRect: rect, cornerRadius: 8) UIColor.systemGray5.setFill() path.fill() // Draw text let attributes: [NSAttributedString.Key: Any] = [ .foregroundColor: UIColor.systemGray, .font: UIFont.systemFont(ofSize: 16) ] let attributedText = NSAttributedString(string: text, attributes: attributes) let textSize = attributedText.size() let textRect = CGRect( x: (size.width - textSize.width) / 2, y: (size.height - textSize.height) / 2, width: textSize.width, height: textSize.height ) attributedText.draw(in: textRect) } } private func displayMetrics(result: RetrieveImageResult) { var info = "=== Image Load Results ===\n\n" // Basic info info += "Cache Type: \(cacheTypeDescription(result.cacheType))\n\n" // Network Metrics if let metrics = result.metrics { info += "=== Network Metrics ===\n" info += "✅ Downloaded from network\n\n" // Timing breakdown info += "📊 Timing Breakdown:\n" info += "Total Request: \(String(format: "%.3f", metrics.totalRequestDuration))s\n" if let dnsTime = metrics.domainLookupDuration { info += "DNS Lookup: \(String(format: "%.3f", dnsTime))s\n" } else { info += "DNS Lookup: N/A (cached or skipped)\n" } if let connectTime = metrics.connectDuration { info += "TCP Connect: \(String(format: "%.3f", connectTime))s\n" } else { info += "TCP Connect: N/A (reused connection)\n" } if let tlsTime = metrics.secureConnectionDuration { info += "TLS Handshake: \(String(format: "%.3f", tlsTime))s\n" } else { info += "TLS Handshake: N/A (HTTP or reused)\n" } // Data transfer info += "\n📈 Data Transfer:\n" info += "Request Body: \(formatBytes(metrics.requestBodyBytesSent))\n" info += "Response Body: \(formatBytes(metrics.responseBodyBytesReceived))\n" if let speed = metrics.downloadSpeed { info += "Download Speed: \(formatBytes(Int64(speed)))/s" info += "\n" } // HTTP details info += "\n🌐 HTTP Details:\n" if let statusCode = metrics.httpStatusCode { info += "Status Code: \(statusCode) \(httpStatusDescription(statusCode))\n" } info += "Redirects: \(metrics.redirectCount)\n" } else { info += "=== Network Metrics ===\n" info += "💾 Loaded from cache\n" info += "No network request was made\n\n" info += "This image was served from:\n" switch result.cacheType { case .memory: info += "• Memory cache (fastest)\n" case .disk: info += "• Disk cache (fast)\n" case .none: info += "• Network (but no metrics available)\n" @unknown default: info += "• Unknown cache type\n" } } metricsTextView.text = info } private func cacheTypeDescription(_ cacheType: CacheType) -> String { switch cacheType { case .memory: return "Memory Cache 🚀" case .disk: return "Disk Cache 💽" case .none: return "Network Download 🌐" @unknown default: return "Unknown" } } private func httpStatusDescription(_ statusCode: Int) -> String { switch statusCode { case 200: return "OK" case 201: return "Created" case 204: return "No Content" case 301: return "Moved Permanently" case 302: return "Found" case 304: return "Not Modified" case 400: return "Bad Request" case 401: return "Unauthorized" case 403: return "Forbidden" case 404: return "Not Found" case 500: return "Internal Server Error" default: return "" } } private func formatBytes(_ bytes: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useBytes, .useKB, .useMB] formatter.countStyle = .file return formatter.string(fromByteCount: bytes) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/NormalLoadingViewController.swift ================================================ // // NormalLoadingViewController.swift // Kingfisher-Demo // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher class NormalLoadingViewController: UICollectionViewController { override func viewDidLoad() { super.viewDidLoad() title = "Loading" setupOperationNavigationBar() } } extension NormalLoadingViewController { override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return ImageLoader.sampleImageURLs.count } override func collectionView( _ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { // This will cancel all unfinished downloading task when the cell disappearing. (cell as! ImageCollectionViewCell).cellImageView.kf.cancelDownloadTask() } override func collectionView( _ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { let imageView = (cell as! ImageCollectionViewCell).cellImageView! let url = ImageLoader.sampleImageURLs[indexPath.row] KF.url(url) .fade(duration: 1) .loadDiskFileSynchronously() .onProgress { (received, total) in print("\(indexPath.row + 1): \(received)/\(total)") } .onSuccess { print($0) } .onFailure { err in print("Error: \(err)") } .set(to: imageView) } override func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell( withReuseIdentifier: "collectionViewCell", for: indexPath) as! ImageCollectionViewCell cell.cellImageView.kf.indicatorType = .activity return cell } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/OrientationImagesViewController.swift ================================================ // // OrientationImagesViewController.swift // Kingfisher-Demo // // Created by Wei Wang on 2021/05/09. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher class OrientationImagesViewController: UICollectionViewController { override func viewDidLoad() { super.viewDidLoad() title = "EXIF" setupOperationNavigationBar() } } extension OrientationImagesViewController { override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return ImageLoader.orientationImageURLs.count } override func collectionView( _ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { // This will cancel all unfinished downloading task when the cell disappearing. (cell as! ImageCollectionViewCell).cellImageView.kf.cancelDownloadTask() } override func collectionView( _ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { let imageView = (cell as! ImageCollectionViewCell).cellImageView! let url = ImageLoader.orientationImageURLs[indexPath.row] KF.url(url) .fade(duration: 1) .backgroundDecode([0, 1].randomElement() == 0) .loadDiskFileSynchronously() .onProgress { (received, total) in print("\(indexPath.row + 1): \(received)/\(total)") } .onSuccess { print($0) } .onFailure { err in print("Error: \(err)") } .set(to: imageView) } override func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell( withReuseIdentifier: "collectionViewCell", for: indexPath) as! ImageCollectionViewCell cell.cellImageView.kf.indicatorType = .activity return cell } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/PHPickerResultViewController.swift ================================================ // // PHPickerResultViewController.swift // Kingfisher // // Created by nuomi1 on 2024-04-17. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import Kingfisher import PhotosUI import UIKit class PHPickerResultViewController: UIViewController { @IBOutlet var imageView: UIImageView! @IBAction func onTapButton() { if #available(iOS 14.0, *) { presentPickerViewController() } else { presentAlertController() } } private func presentAlertController() { let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) let alertController = UIAlertController(title: "Warning!", message: "Only supports iOS 14+", preferredStyle: .alert) alertController.addAction(cancelAction) present(alertController, animated: true) } @available(iOS 14.0, *) private func presentPickerViewController() { var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.filter = .images configuration.selectionLimit = 1 let viewController = PHPickerViewController(configuration: configuration) viewController.delegate = self present(viewController, animated: true) } } @available(iOS 14, *) extension PHPickerResultViewController: PHPickerViewControllerDelegate { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) guard let result = results.first else { return } let provider = PHPickerResultImageDataProvider(pickerResult: result) imageView.kf.setImage(with: .provider(provider)) } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/ProcessorCollectionViewController.swift ================================================ // // ProcessorCollectionViewController.swift // Kingfisher // // Created by onevcat on 2018/11/19. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher private let reuseIdentifier = "ProcessorCell" class ProcessorCollectionViewController: UICollectionViewController { var currentProcessor: any ImageProcessor = DefaultImageProcessor.default { didSet { collectionView.reloadData() } } var processors: [(any ImageProcessor, String)] = [ (DefaultImageProcessor.default, "Default"), (ResizingImageProcessor(referenceSize: CGSize(width: 50, height: 50)), "Resizing"), (RoundCornerImageProcessor(radius: .point(20)), "Round Corner"), (RoundCornerImageProcessor(radius: .widthFraction(0.5), roundingCorners: [.topLeft, .bottomRight]), "Round Corner Partial"), (BorderImageProcessor(border: .init(color: .systemBlue, lineWidth: 8)), "Border"), (RoundCornerImageProcessor(radius: .widthFraction(0.2)) |> BorderImageProcessor(border: .init(color: UIColor.systemBlue.withAlphaComponent(0.7), lineWidth: 12, radius: .widthFraction(0.2))), "Round Border"), (BlendImageProcessor(blendMode: .lighten, alpha: 1.0, backgroundColor: .red), "Blend"), (BlurImageProcessor(blurRadius: 5), "Blur"), (OverlayImageProcessor(overlay: .red, fraction: 0.5), "Overlay"), (TintImageProcessor(tint: UIColor.red.withAlphaComponent(0.5)), "Tint"), (ColorControlsProcessor(brightness: 0.0, contrast: 1.1, saturation: 1.1, inputEV: 1.0), "Vibrancy"), (BlackWhiteProcessor(), "B&W"), (CroppingImageProcessor(size: CGSize(width: 100, height: 100)), "Cropping"), (DownsamplingImageProcessor(size: CGSize(width: 25, height: 25)), "Downsampling"), (BlurImageProcessor(blurRadius: 5) |> RoundCornerImageProcessor(cornerRadius: 20), "Blur + Round Corner") ] override func viewDidLoad() { super.viewDidLoad() title = "Processor" setupOperationNavigationBar() } override func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return ImageLoader.sampleImageURLs.count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell let url = ImageLoader.sampleImageURLs[indexPath.row] KF.url(url) .setProcessor(currentProcessor) .serialize(as: .PNG) .onSuccess { print($0) } .onFailure { print($0) } .set(to: cell.cellImageView) return cell } override func alertPopup(_ sender: Any) -> UIAlertController { let alert = super.alertPopup(sender) alert.addAction(UIAlertAction(title: "Processor", style: .default, handler: { _ in let alert = UIAlertController(title: "Processor", message: nil, preferredStyle: .actionSheet) for item in self.processors { alert.addAction(UIAlertAction(title: item.1, style: .default) { _ in self.currentProcessor = item.0 }) } alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) alert.popoverPresentationController?.barButtonItem = sender as? UIBarButtonItem self.present(alert, animated: true) })) return alert } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/ProgressiveJPEGViewController.swift ================================================ // // ProgressiveJPEGViewController.swift // Kingfisher // // Created by lixiang on 2019/5/12. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher class ProgressiveJPEGViewController: UIViewController { @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var progressLabel: UILabel! private var isBlur = true private var isFastestScan = true private let processor = RoundCornerImageProcessor(cornerRadius: 30) override func viewDidLoad() { super.viewDidLoad() title = "Progressive JPEG" setupOperationNavigationBar() loadImage() } private func loadImage() { progressLabel.text = "- / -" let progressive = ImageProgressive( isBlur: isBlur, isFastestScan: isFastestScan, scanInterval: 0.1 ) KF.url(ImageLoader.progressiveImageURL) .loadDiskFileSynchronously() .progressiveJPEG(progressive) .roundCorner(radius: .point(30)) .onProgress { receivedSize, totalSize in print("\(receivedSize)/\(totalSize)") self.progressLabel.text = "\(receivedSize) / \(totalSize)" } .onSuccess { result in print(result) print("Finished") } .onFailure { error in print(error) self.progressLabel.text = error.localizedDescription } .set(to: imageView) } override func alertPopup(_ sender: Any) -> UIAlertController { let alert = super.alertPopup(sender) func reloadImage() { // Cancel imageView.kf.cancelDownloadTask() // Clean cache KingfisherManager.shared.cache.removeImage( forKey: ImageLoader.progressiveImageURL.cacheKey, processorIdentifier: self.processor.identifier, callbackQueue: .mainAsync, completionHandler: { Task { @MainActor in self.loadImage() } } ) } do { let title = isBlur ? "Disable Blur" : "Enable Blur" alert.addAction(UIAlertAction(title: title, style: .default) { _ in self.isBlur.toggle() reloadImage() }) } do { let title = isFastestScan ? "Disable Fastest Scan" : "Enable Fastest Scan" alert.addAction(UIAlertAction(title: title, style: .default) { _ in self.isFastestScan.toggle() reloadImage() }) } return alert } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/SwiftUIViewController.swift ================================================ // // SwiftUIViewController.swift // Kingfisher // // Created by onevcat on 2020/12/16. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import UIKit @available(iOS 14.0, *) class SwiftUIViewController: UIHostingController { required init?(coder: NSCoder) { super.init(coder: coder, rootView: MainView()) } override func viewDidLoad() { super.viewDidLoad() } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/TextAttachmentViewController.swift ================================================ // // TextAttachmentViewController.swift // Kingfisher // // Created by onevcat on 2020/08/07. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher class TextAttachmentViewController: UIViewController { @IBOutlet weak var label: UILabel! override func viewDidLoad() { super.viewDidLoad() title = "Text Attachment" setupOperationNavigationBar() loadAttributedText() } private func loadAttributedText() { let attributedText = NSMutableAttributedString(string: "Hello World") let textAttachment = NSTextAttachment() attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) label.attributedText = attributedText let label = getLabel() KF.url(URL(string: "https://onevcat.com/assets/images/avatar.jpg")!) .resizing(referenceSize: CGSize(width: 30, height: 30)) .roundCorner(radius: .point(15)) .set(to: textAttachment, attributedView: label) } func getLabel() -> UILabel { return label } } extension TextAttachmentViewController: MainDataViewReloadable { func reload() { label.attributedText = NSAttributedString(string: "-") DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.loadAttributedText() } } } ================================================ FILE: Demo/Demo/Kingfisher-Demo/ViewControllers/TransitionViewController.swift ================================================ // // TransitionViewController.swift // Kingfisher // // Created by onevcat on 2018/11/18. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher class TransitionViewController: UIViewController { enum PickerComponent: Int, CaseIterable { case transitionType case duration } @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var transitionPickerView: UIPickerView! let durations: [TimeInterval] = [0.5, 1, 2, 4, 10] let transitions: [String] = ["none", "fade", "flip - left", "flip - right", "flip - top", "flip - bottom"] override func viewDidLoad() { super.viewDidLoad() title = "Transition" setupOperationNavigationBar() imageView.kf.indicatorType = .activity } func makeTransition(type: String, duration: TimeInterval) -> ImageTransition { switch type { case "none": return .none case "fade": return .fade(duration) case "flip - left": return .flipFromLeft(duration) case "flip - right": return .flipFromRight(duration) case "flip - top": return .flipFromTop(duration) case "flip - bottom": return .flipFromBottom(duration) default: return .none } } func reloadImageView() { let typeIndex = transitionPickerView.selectedRow(inComponent: PickerComponent.transitionType.rawValue) let transitionType = transitions[typeIndex] let durationIndex = transitionPickerView.selectedRow(inComponent: PickerComponent.duration.rawValue) let duration = durations[durationIndex] let t = makeTransition(type: transitionType, duration: duration) let url = ImageLoader.sampleImageURLs[0] KF.url(url) .forceTransition() .transition(t) .set(to: imageView) } } extension TransitionViewController: UIPickerViewDelegate { func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { switch PickerComponent(rawValue: component)! { case .transitionType: return transitions[row] case .duration: return String(durations[row]) } } func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { reloadImageView() } } extension TransitionViewController: UIPickerViewDataSource { func numberOfComponents(in pickerView: UIPickerView) -> Int { return PickerComponent.allCases.count } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { switch PickerComponent(rawValue: component) { case .transitionType: return transitions.count case .duration: return durations.count default: return 0 } } } ================================================ FILE: Demo/Demo/Kingfisher-macOS-Demo/AppDelegate.swift ================================================ // // AppDelegate.swift // Kingfisher-macOS-Demo // // Created by Wei Wang on 16/1/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Cocoa import Kingfisher @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(aNotification: NSNotification) { // Insert code here to initialize your application } func applicationWillTerminate(aNotification: NSNotification) { // Insert code here to tear down your application } } ================================================ FILE: Demo/Demo/Kingfisher-macOS-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "size" : "16x16", "scale" : "1x" }, { "idiom" : "mac", "size" : "16x16", "scale" : "2x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "1x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "2x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "1x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "2x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "1x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "2x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "1x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: Demo/Demo/Kingfisher-macOS-Demo/Cell.xib ================================================ ================================================ FILE: Demo/Demo/Kingfisher-macOS-Demo/GIFHeavyViewController.swift ================================================ // // GIFHeavyViewController.swift // Kingfisher // // Created by yeatse on 2024/1/7. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Cocoa import Kingfisher class GIFHeavyViewController: NSViewController { @IBOutlet weak var stackView: NSStackView! let imageViews = [ AnimatedImageView(), AnimatedImageView(), AnimatedImageView(), AnimatedImageView(), ] override func viewDidLoad() { super.viewDidLoad() let url = URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/GifHeavy.gif") for imageView in imageViews { stackView.addArrangedSubview(imageView) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = true imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) imageView.imageScaling = .scaleProportionallyDown } stackView.layoutSubtreeIfNeeded() for imageView in imageViews { imageView.kf.setImage(with: url) } } } ================================================ FILE: Demo/Demo/Kingfisher-macOS-Demo/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 4.6.2 CFBundleSignature ???? CFBundleVersion 1244 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2019 Wei Wang. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass NSApplication ================================================ FILE: Demo/Demo/Kingfisher-macOS-Demo/SwiftUIViewController.swift ================================================ // // SwiftUIViewController.swift // Kingfisher // // Created by yeatse on 2024/1/8. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SwiftUI import Kingfisher @available(macOS 11, *) class SwiftUIViewController: NSHostingController { required init?(coder: NSCoder) { super.init(coder: coder, rootView: MainView()) } } @available(macOS 11, *) struct MainView: View { @State private var index = 1 static let gifImageURLs: [URL] = { let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF" return (1...3).map { URL(string: "\(prefix)/\($0).gif")! } }() var url: URL { MainView.gifImageURLs[index - 1] } var body: some View { VStack { KFAnimatedImage(url) .configure { view in view.framePreloadCount = 3 } .cacheOriginalImage() .onSuccess { r in print("suc: \(r)") } .onFailure { e in print("err: \(e)") } .placeholder { p in ProgressView(p) } .fade(duration: 1) .forceTransition() .aspectRatio(contentMode: .fill) .frame(width: 300, height: 300) .cornerRadius(20) .shadow(radius: 5) .frame(width: 320, height: 320) Button(action: { self.index = (self.index % 3) + 1 }) { Text("Next Image") } } } } ================================================ FILE: Demo/Demo/Kingfisher-macOS-Demo/ViewController.swift ================================================ // // ViewController.swift // Kingfisher-macOS-Demo // // Created by Wei Wang on 16/1/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import AppKit import Kingfisher class ViewController: NSViewController { @IBOutlet weak var collectionView: NSCollectionView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. title = "Kingfisher" } @IBAction func clearCachePressed(sender: AnyObject) { KingfisherManager.shared.cache.clearMemoryCache() KingfisherManager.shared.cache.clearDiskCache() } @IBAction func reloadPressed(sender: AnyObject) { collectionView.reloadData() } } extension ViewController: NSCollectionViewDataSource { func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return 10 } func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), for: indexPath) let url = URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.item + 1).jpg")! item.imageView?.kf.indicatorType = .activity KF.url(url) .roundCorner(radius: .point(20)) .onProgress { receivedSize, totalSize in print("\(indexPath.item + 1): \(receivedSize)/\(totalSize)") } .onSuccess { print($0) } .set(to: item.imageView!) // Set imageView's `animates` to true if you are loading a GIF. // item.imageView?.animates = true return item } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/AppDelegate.swift ================================================ // // AppDelegate.swift // Kingfisher-tvOS-Demo // // Created by Wei Wang on 15/11/17. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Kingfisher @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { // 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. // 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. } func applicationDidEnterBackground(_ application: UIApplication) { // 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. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } func applicationWillEnterForeground(_ application: UIApplication) { // 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. } func applicationDidBecomeActive(_ application: UIApplication) { // 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. } func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json ================================================ { "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Middle.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json ================================================ { "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Middle.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json ================================================ { "assets" : [ { "size" : "1280x768", "idiom" : "tv", "filename" : "App Icon - Large.imagestack", "role" : "primary-app-icon" }, { "size" : "400x240", "idiom" : "tv", "filename" : "App Icon - Small.imagestack", "role" : "primary-app-icon" }, { "size" : "1920x720", "idiom" : "tv", "filename" : "Top Shelf Image.imageset", "role" : "top-shelf-image" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Assets.xcassets/LaunchImage.launchimage/Contents.json ================================================ { "images" : [ { "orientation" : "landscape", "idiom" : "tv", "extent" : "full-screen", "minimum-system-version" : "9.0", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Demo/Demo/Kingfisher-tvOS-Demo/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 4.6.2 CFBundleSignature ???? CFBundleVersion 1244 LSRequiresIPhoneOS UIMainStoryboardFile Main UIRequiredDeviceCapabilities arm64 ================================================ FILE: Demo/Demo/Kingfisher-watchOS-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "24x24", "idiom" : "watch", "scale" : "2x", "role" : "notificationCenter", "subtype" : "38mm" }, { "size" : "27.5x27.5", "idiom" : "watch", "scale" : "2x", "role" : "notificationCenter", "subtype" : "42mm" }, { "size" : "29x29", "idiom" : "watch", "role" : "companionSettings", "scale" : "2x" }, { "size" : "29x29", "idiom" : "watch", "role" : "companionSettings", "scale" : "3x" }, { "size" : "40x40", "idiom" : "watch", "scale" : "2x", "role" : "appLauncher", "subtype" : "38mm" }, { "size" : "44x44", "idiom" : "watch", "scale" : "2x", "role" : "appLauncher", "subtype" : "40mm" }, { "size" : "50x50", "idiom" : "watch", "scale" : "2x", "role" : "appLauncher", "subtype" : "44mm" }, { "size" : "86x86", "idiom" : "watch", "scale" : "2x", "role" : "quickLook", "subtype" : "38mm" }, { "size" : "98x98", "idiom" : "watch", "scale" : "2x", "role" : "quickLook", "subtype" : "42mm" }, { "size" : "108x108", "idiom" : "watch", "scale" : "2x", "role" : "quickLook", "subtype" : "44mm" }, { "idiom" : "watch-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/Demo/Kingfisher-watchOS-Demo/Base.lproj/Interface.storyboard ================================================ ================================================ FILE: Demo/Demo/Kingfisher-watchOS-Demo/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName Kingfisher-Demo CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 4.6.2 CFBundleSignature ???? CFBundleVersion 1244 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown WKCompanionAppBundleIdentifier com.onevcat.Kingfisher-Demo WKWatchKitApp ================================================ FILE: Demo/Demo/Kingfisher-watchOS-Demo Extension/Assets.xcassets/README__ignoredByTemplate__ ================================================ Did you know that git does not support storing empty directories? ================================================ FILE: Demo/Demo/Kingfisher-watchOS-Demo Extension/ExtensionDelegate.swift ================================================ // // ExtensionDelegate.swift // Kingfisher-watchOS-Demo Extension // // Created by Wei Wang on 16/1/19. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import WatchKit class ExtensionDelegate: NSObject, WKExtensionDelegate { func applicationDidFinishLaunching() { // Perform any final initialization of your application. } func applicationDidBecomeActive() { // 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. } func applicationWillResignActive() { // 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. // Use this method to pause ongoing tasks, disable timers, etc. } } ================================================ FILE: Demo/Demo/Kingfisher-watchOS-Demo Extension/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName Kingfisher-watchOS-Demo Extension CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString 4.6.2 CFBundleSignature ???? CFBundleVersion 1244 NSExtension NSExtensionAttributes WKAppBundleIdentifier com.onevcat.Kingfisher-Demo.watchkitapp NSExtensionPointIdentifier com.apple.watchkit RemoteInterfacePrincipalClass $(PRODUCT_MODULE_NAME).InterfaceController WKExtensionDelegateClassName $(PRODUCT_MODULE_NAME).ExtensionDelegate ================================================ FILE: Demo/Demo/Kingfisher-watchOS-Demo Extension/InterfaceController.swift ================================================ // // InterfaceController.swift // Kingfisher-watchOS-Demo Extension // // Created by Wei Wang on 16/1/19. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import WatchKit import Foundation import Kingfisher @MainActor var count = 0 class InterfaceController: WKInterfaceController { @IBOutlet var interfaceImage: WKInterfaceImage! var currentIndex: Int? override func awake(withContext context: Any?) { super.awake(withContext: context) currentIndex = count count += 1 } func refreshImage() { let url = URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(currentIndex! + 1).jpg")! print("Start loading... \(url)") interfaceImage.kf.setImage(with: url, completionHandler: { r in print(r) }) } override func willActivate() { // This method is called when watch view controller is about to be visible to user super.willActivate() refreshImage() } override func didDeactivate() { // This method is called when watch view controller is no longer visible super.didDeactivate() } } ================================================ FILE: Demo/Kingfisher-Demo.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.client ================================================ FILE: Demo/Kingfisher-Demo.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 072922432638639D0089E810 /* AnimatedImageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072922422638639D0089E810 /* AnimatedImageDemo.swift */; }; 0784D0992F17E41700EB2A07 /* PhotosPickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0784D0982F17E41700EB2A07 /* PhotosPickerDemo.swift */; }; 078DCB512BCFEFB40008114E /* PHPickerResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */; }; 277EAE8D2045B39C00547CD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE892045B39C00547CD3 /* Assets.xcassets */; }; 277EAE8E2045B39C00547CD3 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE8A2045B39C00547CD3 /* Interface.storyboard */; }; 277EAE9D2045B4D500547CD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE962045B4D500547CD3 /* Assets.xcassets */; }; 277EAEA12045B52800547CD3 /* InterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277EAE9E2045B52800547CD3 /* InterfaceController.swift */; }; 277EAEA32045B52800547CD3 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277EAEA02045B52800547CD3 /* ExtensionDelegate.swift */; }; 3887E1602B4AD7200062C53C /* GIFHeavyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3887E15F2B4AD7200062C53C /* GIFHeavyViewController.swift */; }; 3887E17B2B4BC04B0062C53C /* SwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3887E17A2B4BC04B0062C53C /* SwiftUIViewController.swift */; }; 4B120CA726B91BB70060B092 /* TransitionViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B120CA626B91BB70060B092 /* TransitionViewDemo.swift */; }; 4B1C7A3D21A256E300CE9D31 /* InfinityCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1C7A3C21A256E300CE9D31 /* InfinityCollectionViewController.swift */; }; 4B4307A51D87E6A700ED2DA9 /* loader.gif in Resources */ = {isa = PBXBuildFile; fileRef = 4B7742461D87E42E0077024E /* loader.gif */; }; 4B6E1B6D28DB4E8C0023B54B /* Issue1998View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6E1B6C28DB4E8C0023B54B /* Issue1998View.swift */; }; 4B7742471D87E42E0077024E /* loader.gif in Resources */ = {isa = PBXBuildFile; fileRef = 4B7742461D87E42E0077024E /* loader.gif */; }; 4B7742481D87E42E0077024E /* loader.gif in Resources */ = {isa = PBXBuildFile; fileRef = 4B7742461D87E42E0077024E /* loader.gif */; }; 4B779C8526743C2800FF9C1E /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B779C8426743C2800FF9C1E /* GeometryReaderDemo.swift */; }; 4B92FE5625FF906B00473088 /* AutoSizingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92FE5525FF906B00473088 /* AutoSizingTableViewController.swift */; }; 4B9AD88F26480D3B0086A261 /* OrientationImagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCC51AA26480CD5007004E8 /* OrientationImagesViewController.swift */; }; 4B9AD89026480D3C0086A261 /* OrientationImagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCC51AA26480CD5007004E8 /* OrientationImagesViewController.swift */; }; 4BC0ED4A29A6EE78003E9CD1 /* Issue2035View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC0ED4929A6EE78003E9CD1 /* Issue2035View.swift */; }; 4BCCF33D1D5B02F8003387C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCCF3361D5B02F8003387C2 /* AppDelegate.swift */; }; 4BCCF33E1D5B02F8003387C2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4BCCF3371D5B02F8003387C2 /* Assets.xcassets */; }; 4BCCF33F1D5B02F8003387C2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4BCCF3381D5B02F8003387C2 /* Main.storyboard */; }; 4BCCF3401D5B02F8003387C2 /* Cell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BCCF33A1D5B02F8003387C2 /* Cell.xib */; }; 4BCCF3421D5B02F8003387C2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCCF33C1D5B02F8003387C2 /* ViewController.swift */; }; 4BE855522320F9C800FE4205 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855512320F9C800FE4205 /* Kingfisher.framework */; }; 4BE855532320F9C800FE4205 /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855512320F9C800FE4205 /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4BE855552320F9CF00FE4205 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855542320F9CF00FE4205 /* Kingfisher.framework */; }; 4BE855562320F9CF00FE4205 /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855542320F9CF00FE4205 /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4BE855582320F9D300FE4205 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855572320F9D300FE4205 /* Kingfisher.framework */; }; 4BE855592320F9D300FE4205 /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855572320F9D300FE4205 /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4BE855612320F9DE00FE4205 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855602320F9DE00FE4205 /* Kingfisher.framework */; }; 4BE855622320F9DE00FE4205 /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE855602320F9DE00FE4205 /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 769F07F126298E2E00610767 /* GIFHeavyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F07F026298E2E00610767 /* GIFHeavyViewController.swift */; }; C959EEE622874DC600467A10 /* ProgressiveJPEGViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C959EEE522874DC600467A10 /* ProgressiveJPEGViewController.swift */; }; D10AC99821A300C9005F057C /* ProcessorCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10AC99721A300C9005F057C /* ProcessorCollectionViewController.swift */; }; D12E0C951C47F91800AC98AD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C8C1C47F91800AC98AD /* AppDelegate.swift */; }; D12E0C971C47F91800AC98AD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C8F1C47F91800AC98AD /* Main.storyboard */; }; D12E0C981C47F91800AC98AD /* ImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C911C47F91800AC98AD /* ImageCollectionViewCell.swift */; }; D12E0C991C47F91800AC98AD /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C921C47F91800AC98AD /* Images.xcassets */; }; D12E0C9B1C47F91800AC98AD /* NormalLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C941C47F91800AC98AD /* NormalLoadingViewController.swift */; }; D12E0CA21C47F92200AC98AD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C9D1C47F92200AC98AD /* AppDelegate.swift */; }; D12E0CA31C47F92200AC98AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C9E1C47F92200AC98AD /* Assets.xcassets */; }; D12E0CA41C47F92200AC98AD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C9F1C47F92200AC98AD /* Main.storyboard */; }; D12E0CB61C47F9C100AC98AD /* NormalLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C941C47F91800AC98AD /* NormalLoadingViewController.swift */; }; D12EB83E24DD902300329EE1 /* TextAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12EB83D24DD902300329EE1 /* TextAttachmentViewController.swift */; }; D12EB84024DDB9E100329EE1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D12EB83F24DDB9E000329EE1 /* LaunchScreen.storyboard */; }; D12F67682CB10AE000AB63AB /* LivePhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67672CB10AD900AB63AB /* LivePhotoViewController.swift */; }; D14799D92E1129A900053537 /* LoadingFailureDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14799D82E1129A800053537 /* LoadingFailureDemo.swift */; }; D1679A461C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D16AAF282D5247CF00E7F764 /* Issue2352View.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16AAF272D5247CA00E7F764 /* Issue2352View.swift */; }; D16CC3D824E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16CC3D724E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift */; }; D198F41E25EDC11500C53E0D /* LazyVStackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D198F41D25EDC11500C53E0D /* LazyVStackDemo.swift */; }; D198F42025EDC34000C53E0D /* SizingAnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D198F41F25EDC34000C53E0D /* SizingAnimationDemo.swift */; }; D198F42225EDC4B900C53E0D /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D198F42125EDC4B900C53E0D /* GridDemo.swift */; }; D1A1CCA321A1879600263AD8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CCA221A1879600263AD8 /* MainViewController.swift */; }; D1A1CCA721A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CCA621A18A3200263AD8 /* UIViewController+KingfisherOperation.swift */; }; D1A1CCA821A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CCA621A18A3200263AD8 /* UIViewController+KingfisherOperation.swift */; }; D1CE1BD021A1AFA300419000 /* TransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CE1BCF21A1AFA300419000 /* TransitionViewController.swift */; }; D1CE1BD321A1B45A00419000 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CE1BD221A1B45A00419000 /* ImageLoader.swift */; }; D1CE1BD421A1B45A00419000 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CE1BD221A1B45A00419000 /* ImageLoader.swift */; }; D1E4CF5421BACBA6004D029D /* ImageDataProviderCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E4CF5321BACBA6004D029D /* ImageDataProviderCollectionViewController.swift */; }; D1E612E22D75F9AC00DACD51 /* ProgressiveJPEGDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E612E12D75F99C00DACD51 /* ProgressiveJPEGDemo.swift */; }; D1EDF7422C9F01270017FFA5 /* Issue2295View.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EDF7412C9F01200017FFA5 /* Issue2295View.swift */; }; D1F06F3321AA4292000B1C38 /* DetailImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F06F3221AA4292000B1C38 /* DetailImageViewController.swift */; }; D1F06F3521AA5938000B1C38 /* ImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C911C47F91800AC98AD /* ImageCollectionViewCell.swift */; }; D1F06F3721AAEACF000B1C38 /* GIFViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F06F3621AAEACF000B1C38 /* GIFViewController.swift */; }; D1F06F3921AAF1EE000B1C38 /* IndicatorCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F06F3821AAF1EE000B1C38 /* IndicatorCollectionViewController.swift */; }; D1F78A5F2589F0AA00930759 /* SwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F78A5E2589F0AA00930759 /* SwiftUIViewController.swift */; }; D1F78A642589F17200930759 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F78A612589F17200930759 /* ListDemo.swift */; }; D1F78A652589F17200930759 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F78A622589F17200930759 /* MainView.swift */; }; D1F78A662589F17200930759 /* SingleViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F78A632589F17200930759 /* SingleViewDemo.swift */; }; D1FAB06F21A853E600908910 /* HighResolutionCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FAB06E21A853E600908910 /* HighResolutionCollectionViewController.swift */; }; F344AEF22E1AD74F00BFE702 /* LoadTransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F344AEF12E1AD74F00BFE702 /* LoadTransitionDemo.swift */; }; F39B68D42E33E0D600404B02 /* NetworkMetricsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39B68D32E33E0D600404B02 /* NetworkMetricsViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ D1679A471C4E78B20020FD12 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D1ED2D031AD2CFA600CFC3EB /* Project object */; proxyType = 1; remoteGlobalIDString = D1679A441C4E78B20020FD12; remoteInfo = "Kingfisher-watchOS-Demo Extension"; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ D12F2EEC1C4E7CF500B8054D /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 4BE855622320F9DE00FE4205 /* Kingfisher.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; D13F49ED1BEDA82000CE335D /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 4BE855562320F9CF00FE4205 /* Kingfisher.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; D1679A571C4E78B20020FD12 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( D1679A461C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; D19ADCFC23099ADE00D20B28 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 4BE855592320F9D300FE4205 /* Kingfisher.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; D19ADCFF23099B4300D20B28 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; D1ED2D511AD2D09F00CFC3EB /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 4BE855532320F9C800FE4205 /* Kingfisher.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 072922422638639D0089E810 /* AnimatedImageDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageDemo.swift; sourceTree = ""; }; 0784D0982F17E41700EB2A07 /* PhotosPickerDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosPickerDemo.swift; sourceTree = ""; }; 078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultViewController.swift; sourceTree = ""; }; 277EAE892045B39C00547CD3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 277EAE8B2045B39C00547CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; 277EAE8C2045B39C00547CD3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 277EAE962045B4D500547CD3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 277EAE9E2045B52800547CD3 /* InterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterfaceController.swift; sourceTree = ""; }; 277EAE9F2045B52800547CD3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 277EAEA02045B52800547CD3 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; }; 3887E15F2B4AD7200062C53C /* GIFHeavyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFHeavyViewController.swift; sourceTree = ""; }; 3887E17A2B4BC04B0062C53C /* SwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewController.swift; sourceTree = ""; }; 4B120CA626B91BB70060B092 /* TransitionViewDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionViewDemo.swift; sourceTree = ""; }; 4B1C7A3C21A256E300CE9D31 /* InfinityCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfinityCollectionViewController.swift; sourceTree = ""; }; 4B2944551C3D03880088C3E7 /* Kingfisher-macOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-macOS-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4B6E1B6C28DB4E8C0023B54B /* Issue1998View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue1998View.swift; sourceTree = ""; }; 4B7742461D87E42E0077024E /* loader.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = loader.gif; sourceTree = ""; }; 4B779C8426743C2800FF9C1E /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = ""; }; 4B92FE5525FF906B00473088 /* AutoSizingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoSizingTableViewController.swift; sourceTree = ""; }; 4BC0ED4929A6EE78003E9CD1 /* Issue2035View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue2035View.swift; sourceTree = ""; }; 4BCC51AA26480CD5007004E8 /* OrientationImagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationImagesViewController.swift; sourceTree = ""; }; 4BCCF3361D5B02F8003387C2 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 4BCCF3371D5B02F8003387C2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 4BCCF3391D5B02F8003387C2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 4BCCF33A1D5B02F8003387C2 /* Cell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = Cell.xib; sourceTree = ""; }; 4BCCF33B1D5B02F8003387C2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4BCCF33C1D5B02F8003387C2 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 4BE855512320F9C800FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4BE855542320F9CF00FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4BE855572320F9D300FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4BE8555A2320F9D800FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4BE8555B2320F9D800FE4205 /* KingfisherSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KingfisherSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4BE855602320F9DE00FE4205 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 769F07F026298E2E00610767 /* GIFHeavyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFHeavyViewController.swift; sourceTree = ""; }; C959EEE522874DC600467A10 /* ProgressiveJPEGViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressiveJPEGViewController.swift; sourceTree = ""; }; D10AC99721A300C9005F057C /* ProcessorCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessorCollectionViewController.swift; sourceTree = ""; }; D12E0C8C1C47F91800AC98AD /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D12E0C901C47F91800AC98AD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; D12E0C911C47F91800AC98AD /* ImageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCollectionViewCell.swift; sourceTree = ""; }; D12E0C921C47F91800AC98AD /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; D12E0C931C47F91800AC98AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D12E0C941C47F91800AC98AD /* NormalLoadingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalLoadingViewController.swift; sourceTree = ""; }; D12E0C9D1C47F92200AC98AD /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D12E0C9E1C47F92200AC98AD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D12E0CA01C47F92200AC98AD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; D12E0CA11C47F92200AC98AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D12EB83D24DD902300329EE1 /* TextAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAttachmentViewController.swift; sourceTree = ""; }; D12EB83F24DDB9E000329EE1 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; D12F67672CB10AD900AB63AB /* LivePhotoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoViewController.swift; sourceTree = ""; }; D13F49C21BEDA53F00CE335D /* Kingfisher-tvOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-tvOS-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D14799D82E1129A800053537 /* LoadingFailureDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingFailureDemo.swift; sourceTree = ""; }; D16218A4238EAA67004A1C6C /* Kingfisher-Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Kingfisher-Demo.entitlements"; sourceTree = ""; }; D1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-watchOS-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Kingfisher-watchOS-Demo Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; D16AAF272D5247CA00E7F764 /* Issue2352View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue2352View.swift; sourceTree = ""; }; D16CC3D724E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVAssetImageGeneratorViewController.swift; sourceTree = ""; }; D198F41D25EDC11500C53E0D /* LazyVStackDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyVStackDemo.swift; sourceTree = ""; }; D198F41F25EDC34000C53E0D /* SizingAnimationDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizingAnimationDemo.swift; sourceTree = ""; }; D198F42125EDC4B900C53E0D /* GridDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = ""; }; D1A1CCA221A1879600263AD8 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; D1A1CCA621A18A3200263AD8 /* UIViewController+KingfisherOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+KingfisherOperation.swift"; sourceTree = ""; }; D1CE1BCF21A1AFA300419000 /* TransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionViewController.swift; sourceTree = ""; }; D1CE1BD221A1B45A00419000 /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; D1E4CF5321BACBA6004D029D /* ImageDataProviderCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataProviderCollectionViewController.swift; sourceTree = ""; }; D1E612E12D75F99C00DACD51 /* ProgressiveJPEGDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressiveJPEGDemo.swift; sourceTree = ""; }; D1ED2D0B1AD2CFA600CFC3EB /* Kingfisher-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D1EDF7412C9F01200017FFA5 /* Issue2295View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue2295View.swift; sourceTree = ""; }; D1F06F3221AA4292000B1C38 /* DetailImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailImageViewController.swift; sourceTree = ""; }; D1F06F3621AAEACF000B1C38 /* GIFViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFViewController.swift; sourceTree = ""; }; D1F06F3821AAF1EE000B1C38 /* IndicatorCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndicatorCollectionViewController.swift; sourceTree = ""; }; D1F78A5E2589F0AA00930759 /* SwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewController.swift; sourceTree = ""; }; D1F78A612589F17200930759 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = ""; }; D1F78A622589F17200930759 /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; D1F78A632589F17200930759 /* SingleViewDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleViewDemo.swift; sourceTree = ""; }; D1FAB06E21A853E600908910 /* HighResolutionCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighResolutionCollectionViewController.swift; sourceTree = ""; }; F344AEF12E1AD74F00BFE702 /* LoadTransitionDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTransitionDemo.swift; sourceTree = ""; }; F39B68D32E33E0D600404B02 /* NetworkMetricsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMetricsViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 4B2944521C3D03880088C3E7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4BE855582320F9D300FE4205 /* Kingfisher.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 60796AE9A717329E55ACCCC7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; D13F49BF1BEDA53F00CE335D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4BE855552320F9CF00FE4205 /* Kingfisher.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; D1679A421C4E78B20020FD12 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4BE855612320F9DE00FE4205 /* Kingfisher.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; D1ED2D081AD2CFA600CFC3EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4BE855522320F9C800FE4205 /* Kingfisher.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 277EAE762045ADE700547CD3 /* Frameworks */ = { isa = PBXGroup; children = ( 4BE855602320F9DE00FE4205 /* Kingfisher.framework */, 4BE8555A2320F9D800FE4205 /* Kingfisher.framework */, 4BE8555B2320F9D800FE4205 /* KingfisherSwiftUI.framework */, 4BE855572320F9D300FE4205 /* Kingfisher.framework */, 4BE855542320F9CF00FE4205 /* Kingfisher.framework */, 4BE855512320F9C800FE4205 /* Kingfisher.framework */, ); name = Frameworks; sourceTree = ""; }; 277EAE882045B39C00547CD3 /* Kingfisher-watchOS-Demo */ = { isa = PBXGroup; children = ( 277EAE892045B39C00547CD3 /* Assets.xcassets */, 277EAE8A2045B39C00547CD3 /* Interface.storyboard */, 277EAE8C2045B39C00547CD3 /* Info.plist */, ); name = "Kingfisher-watchOS-Demo"; path = "Demo/Kingfisher-watchOS-Demo"; sourceTree = ""; }; 277EAE952045B4D500547CD3 /* Kingfisher-watchOS-Demo Extension */ = { isa = PBXGroup; children = ( 277EAE962045B4D500547CD3 /* Assets.xcassets */, 277EAEA02045B52800547CD3 /* ExtensionDelegate.swift */, 277EAE9F2045B52800547CD3 /* Info.plist */, 277EAE9E2045B52800547CD3 /* InterfaceController.swift */, ); name = "Kingfisher-watchOS-Demo Extension"; path = "Demo/Kingfisher-watchOS-Demo Extension"; sourceTree = ""; }; 4BC0ED4829A6EE4F003E9CD1 /* Regression */ = { isa = PBXGroup; children = ( D1EDF7412C9F01200017FFA5 /* Issue2295View.swift */, 4B6E1B6C28DB4E8C0023B54B /* Issue1998View.swift */, 4BC0ED4929A6EE78003E9CD1 /* Issue2035View.swift */, D16AAF272D5247CA00E7F764 /* Issue2352View.swift */, ); path = Regression; sourceTree = ""; }; 4BCCF3351D5B02F8003387C2 /* Kingfisher-macOS-Demo */ = { isa = PBXGroup; children = ( 4BCCF3361D5B02F8003387C2 /* AppDelegate.swift */, 4BCCF3371D5B02F8003387C2 /* Assets.xcassets */, 4BCCF3381D5B02F8003387C2 /* Main.storyboard */, 4BCCF33A1D5B02F8003387C2 /* Cell.xib */, 4BCCF33B1D5B02F8003387C2 /* Info.plist */, 4BCCF33C1D5B02F8003387C2 /* ViewController.swift */, 3887E15F2B4AD7200062C53C /* GIFHeavyViewController.swift */, 3887E17A2B4BC04B0062C53C /* SwiftUIViewController.swift */, ); name = "Kingfisher-macOS-Demo"; path = "Demo/Kingfisher-macOS-Demo"; sourceTree = ""; }; D10EC22B1C3D62DE00A4211C /* Demo */ = { isa = PBXGroup; children = ( D12E0C8B1C47F91800AC98AD /* Kingfisher-Demo */, 4BCCF3351D5B02F8003387C2 /* Kingfisher-macOS-Demo */, D12E0C9C1C47F92200AC98AD /* Kingfisher-tvOS-Demo */, 277EAE952045B4D500547CD3 /* Kingfisher-watchOS-Demo Extension */, 277EAE882045B39C00547CD3 /* Kingfisher-watchOS-Demo */, ); name = Demo; sourceTree = ""; }; D12E0C8B1C47F91800AC98AD /* Kingfisher-Demo */ = { isa = PBXGroup; children = ( D12E0C931C47F91800AC98AD /* Info.plist */, D12E0C8C1C47F91800AC98AD /* AppDelegate.swift */, D12EB83F24DDB9E000329EE1 /* LaunchScreen.storyboard */, D12E0C8F1C47F91800AC98AD /* Main.storyboard */, D1A1CCA921A1936300263AD8 /* ViewControllers */, D1F78A602589F14C00930759 /* SwiftUIViews */, D1A1CCA521A1895000263AD8 /* Extensions */, D1A1CCAB21A1939100263AD8 /* Resources */, D12E0C921C47F91800AC98AD /* Images.xcassets */, ); name = "Kingfisher-Demo"; path = "Demo/Kingfisher-Demo"; sourceTree = ""; }; D12E0C9C1C47F92200AC98AD /* Kingfisher-tvOS-Demo */ = { isa = PBXGroup; children = ( D12E0C9D1C47F92200AC98AD /* AppDelegate.swift */, D12E0C9E1C47F92200AC98AD /* Assets.xcassets */, D12E0C9F1C47F92200AC98AD /* Main.storyboard */, D12E0CA11C47F92200AC98AD /* Info.plist */, ); name = "Kingfisher-tvOS-Demo"; path = "Demo/Kingfisher-tvOS-Demo"; sourceTree = ""; }; D1A1CCA521A1895000263AD8 /* Extensions */ = { isa = PBXGroup; children = ( D1A1CCA621A18A3200263AD8 /* UIViewController+KingfisherOperation.swift */, ); path = Extensions; sourceTree = ""; }; D1A1CCA921A1936300263AD8 /* ViewControllers */ = { isa = PBXGroup; children = ( D12F67672CB10AD900AB63AB /* LivePhotoViewController.swift */, D10AC99721A300C9005F057C /* ProcessorCollectionViewController.swift */, 4B1C7A3C21A256E300CE9D31 /* InfinityCollectionViewController.swift */, D1CE1BCF21A1AFA300419000 /* TransitionViewController.swift */, D12E0C941C47F91800AC98AD /* NormalLoadingViewController.swift */, 4BCC51AA26480CD5007004E8 /* OrientationImagesViewController.swift */, 4B92FE5525FF906B00473088 /* AutoSizingTableViewController.swift */, D12E0C911C47F91800AC98AD /* ImageCollectionViewCell.swift */, D1A1CCA221A1879600263AD8 /* MainViewController.swift */, D1FAB06E21A853E600908910 /* HighResolutionCollectionViewController.swift */, D1F06F3221AA4292000B1C38 /* DetailImageViewController.swift */, D1F06F3621AAEACF000B1C38 /* GIFViewController.swift */, 769F07F026298E2E00610767 /* GIFHeavyViewController.swift */, D1F06F3821AAF1EE000B1C38 /* IndicatorCollectionViewController.swift */, D1E4CF5321BACBA6004D029D /* ImageDataProviderCollectionViewController.swift */, C959EEE522874DC600467A10 /* ProgressiveJPEGViewController.swift */, D12EB83D24DD902300329EE1 /* TextAttachmentViewController.swift */, D16CC3D724E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift */, 078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */, D1F78A5E2589F0AA00930759 /* SwiftUIViewController.swift */, F39B68D32E33E0D600404B02 /* NetworkMetricsViewController.swift */, ); path = ViewControllers; sourceTree = ""; }; D1A1CCAB21A1939100263AD8 /* Resources */ = { isa = PBXGroup; children = ( 4B7742461D87E42E0077024E /* loader.gif */, D1CE1BD221A1B45A00419000 /* ImageLoader.swift */, ); path = Resources; sourceTree = ""; }; D1ED2D021AD2CFA600CFC3EB = { isa = PBXGroup; children = ( D16218A4238EAA67004A1C6C /* Kingfisher-Demo.entitlements */, D10EC22B1C3D62DE00A4211C /* Demo */, D1ED2D0C1AD2CFA600CFC3EB /* Products */, 277EAE762045ADE700547CD3 /* Frameworks */, ); sourceTree = ""; }; D1ED2D0C1AD2CFA600CFC3EB /* Products */ = { isa = PBXGroup; children = ( D1ED2D0B1AD2CFA600CFC3EB /* Kingfisher-Demo.app */, D13F49C21BEDA53F00CE335D /* Kingfisher-tvOS-Demo.app */, 4B2944551C3D03880088C3E7 /* Kingfisher-macOS-Demo.app */, D1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */, D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */, ); name = Products; sourceTree = ""; }; D1F78A602589F14C00930759 /* SwiftUIViews */ = { isa = PBXGroup; children = ( 4BC0ED4829A6EE4F003E9CD1 /* Regression */, D1F78A622589F17200930759 /* MainView.swift */, D14799D82E1129A800053537 /* LoadingFailureDemo.swift */, D1F78A612589F17200930759 /* ListDemo.swift */, D198F42125EDC4B900C53E0D /* GridDemo.swift */, 4B779C8426743C2800FF9C1E /* GeometryReaderDemo.swift */, D1F78A632589F17200930759 /* SingleViewDemo.swift */, D1E612E12D75F99C00DACD51 /* ProgressiveJPEGDemo.swift */, 4B120CA626B91BB70060B092 /* TransitionViewDemo.swift */, D198F41D25EDC11500C53E0D /* LazyVStackDemo.swift */, D198F41F25EDC34000C53E0D /* SizingAnimationDemo.swift */, 072922422638639D0089E810 /* AnimatedImageDemo.swift */, F344AEF12E1AD74F00BFE702 /* LoadTransitionDemo.swift */, 0784D0982F17E41700EB2A07 /* PhotosPickerDemo.swift */, ); path = SwiftUIViews; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 4B2944541C3D03880088C3E7 /* Kingfisher-macOS-Demo */ = { isa = PBXNativeTarget; buildConfigurationList = 4B2944611C3D03880088C3E7 /* Build configuration list for PBXNativeTarget "Kingfisher-macOS-Demo" */; buildPhases = ( 4B2944511C3D03880088C3E7 /* Sources */, 4B2944521C3D03880088C3E7 /* Frameworks */, 4B2944531C3D03880088C3E7 /* Resources */, D19ADCFC23099ADE00D20B28 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Kingfisher-macOS-Demo"; productName = "Kingfisher-OSX-Demo"; productReference = 4B2944551C3D03880088C3E7 /* Kingfisher-macOS-Demo.app */; productType = "com.apple.product-type.application"; }; D13F49C11BEDA53F00CE335D /* Kingfisher-tvOS-Demo */ = { isa = PBXNativeTarget; buildConfigurationList = D13F49D01BEDA53F00CE335D /* Build configuration list for PBXNativeTarget "Kingfisher-tvOS-Demo" */; buildPhases = ( D13F49BE1BEDA53F00CE335D /* Sources */, D13F49BF1BEDA53F00CE335D /* Frameworks */, D13F49C01BEDA53F00CE335D /* Resources */, D13F49ED1BEDA82000CE335D /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Kingfisher-tvOS-Demo"; productName = "Kingfisher-tvOS-Demo"; productReference = D13F49C21BEDA53F00CE335D /* Kingfisher-tvOS-Demo.app */; productType = "com.apple.product-type.application"; }; D1679A381C4E78B20020FD12 /* Kingfisher-watchOS-Demo */ = { isa = PBXNativeTarget; buildConfigurationList = D1679A581C4E78B20020FD12 /* Build configuration list for PBXNativeTarget "Kingfisher-watchOS-Demo" */; buildPhases = ( D1679A371C4E78B20020FD12 /* Resources */, D1679A571C4E78B20020FD12 /* Embed Foundation Extensions */, 60796AE9A717329E55ACCCC7 /* Frameworks */, D19ADCFF23099B4300D20B28 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( D1679A481C4E78B20020FD12 /* PBXTargetDependency */, ); name = "Kingfisher-watchOS-Demo"; productName = "Kingfisher-watchOS-Demo"; productReference = D1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */; productType = "com.apple.product-type.application.watchapp2"; }; D1679A441C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension */ = { isa = PBXNativeTarget; buildConfigurationList = D1679A541C4E78B20020FD12 /* Build configuration list for PBXNativeTarget "Kingfisher-watchOS-Demo Extension" */; buildPhases = ( D1679A411C4E78B20020FD12 /* Sources */, D1679A421C4E78B20020FD12 /* Frameworks */, D1679A431C4E78B20020FD12 /* Resources */, D12F2EEC1C4E7CF500B8054D /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Kingfisher-watchOS-Demo Extension"; productName = "Kingfisher-watchOS-Demo Extension"; productReference = D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */; productType = "com.apple.product-type.watchkit2-extension"; }; D1ED2D0A1AD2CFA600CFC3EB /* Kingfisher-Demo */ = { isa = PBXNativeTarget; buildConfigurationList = D1ED2D2A1AD2CFA600CFC3EB /* Build configuration list for PBXNativeTarget "Kingfisher-Demo" */; buildPhases = ( D1ED2D071AD2CFA600CFC3EB /* Sources */, D1ED2D081AD2CFA600CFC3EB /* Frameworks */, D1ED2D091AD2CFA600CFC3EB /* Resources */, D1ED2D511AD2D09F00CFC3EB /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Kingfisher-Demo"; productName = "Kingfisher-Demo"; productReference = D1ED2D0B1AD2CFA600CFC3EB /* Kingfisher-Demo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ D1ED2D031AD2CFA600CFC3EB /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1100; LastUpgradeCheck = 1640; ORGANIZATIONNAME = "Wei Wang"; TargetAttributes = { 4B2944541C3D03880088C3E7 = { CreatedOnToolsVersion = 7.2; DevelopmentTeam = A4YJ9MRZ66; LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; D13F49C11BEDA53F00CE335D = { CreatedOnToolsVersion = 7.1; LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; D1679A381C4E78B20020FD12 = { CreatedOnToolsVersion = 7.2; LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; D1679A441C4E78B20020FD12 = { CreatedOnToolsVersion = 7.2; LastSwiftMigration = 0920; }; D1ED2D0A1AD2CFA600CFC3EB = { CreatedOnToolsVersion = 6.2; LastSwiftMigration = 1200; }; }; }; buildConfigurationList = D1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject "Kingfisher-Demo" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = D1ED2D021AD2CFA600CFC3EB; productRefGroup = D1ED2D0C1AD2CFA600CFC3EB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( D1ED2D0A1AD2CFA600CFC3EB /* Kingfisher-Demo */, D13F49C11BEDA53F00CE335D /* Kingfisher-tvOS-Demo */, 4B2944541C3D03880088C3E7 /* Kingfisher-macOS-Demo */, D1679A381C4E78B20020FD12 /* Kingfisher-watchOS-Demo */, D1679A441C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4B2944531C3D03880088C3E7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4BCCF33E1D5B02F8003387C2 /* Assets.xcassets in Resources */, 4BCCF3401D5B02F8003387C2 /* Cell.xib in Resources */, 4B4307A51D87E6A700ED2DA9 /* loader.gif in Resources */, 4BCCF33F1D5B02F8003387C2 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; D13F49C01BEDA53F00CE335D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4B7742481D87E42E0077024E /* loader.gif in Resources */, D12E0CA31C47F92200AC98AD /* Assets.xcassets in Resources */, D12E0CA41C47F92200AC98AD /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; D1679A371C4E78B20020FD12 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 277EAE8E2045B39C00547CD3 /* Interface.storyboard in Resources */, 277EAE8D2045B39C00547CD3 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; D1679A431C4E78B20020FD12 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 277EAE9D2045B4D500547CD3 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; D1ED2D091AD2CFA600CFC3EB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( D12E0C991C47F91800AC98AD /* Images.xcassets in Resources */, 4B7742471D87E42E0077024E /* loader.gif in Resources */, D12EB84024DDB9E100329EE1 /* LaunchScreen.storyboard in Resources */, D12E0C971C47F91800AC98AD /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 4B2944511C3D03880088C3E7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 3887E17B2B4BC04B0062C53C /* SwiftUIViewController.swift in Sources */, 4BCCF3421D5B02F8003387C2 /* ViewController.swift in Sources */, 3887E1602B4AD7200062C53C /* GIFHeavyViewController.swift in Sources */, 4BCCF33D1D5B02F8003387C2 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; D13F49BE1BEDA53F00CE335D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D1CE1BD421A1B45A00419000 /* ImageLoader.swift in Sources */, D12E0CB61C47F9C100AC98AD /* NormalLoadingViewController.swift in Sources */, D1F06F3521AA5938000B1C38 /* ImageCollectionViewCell.swift in Sources */, 4B9AD89026480D3C0086A261 /* OrientationImagesViewController.swift in Sources */, D1A1CCA821A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */, D12E0CA21C47F92200AC98AD /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; D1679A411C4E78B20020FD12 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 277EAEA32045B52800547CD3 /* ExtensionDelegate.swift in Sources */, 277EAEA12045B52800547CD3 /* InterfaceController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; D1ED2D071AD2CFA600CFC3EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D16CC3D824E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift in Sources */, C959EEE622874DC600467A10 /* ProgressiveJPEGViewController.swift in Sources */, D1CE1BD321A1B45A00419000 /* ImageLoader.swift in Sources */, D12EB83E24DD902300329EE1 /* TextAttachmentViewController.swift in Sources */, D12E0C9B1C47F91800AC98AD /* NormalLoadingViewController.swift in Sources */, D1CE1BD021A1AFA300419000 /* TransitionViewController.swift in Sources */, F344AEF22E1AD74F00BFE702 /* LoadTransitionDemo.swift in Sources */, D10AC99821A300C9005F057C /* ProcessorCollectionViewController.swift in Sources */, D1F06F3921AAF1EE000B1C38 /* IndicatorCollectionViewController.swift in Sources */, D1F78A662589F17200930759 /* SingleViewDemo.swift in Sources */, F39B68D42E33E0D600404B02 /* NetworkMetricsViewController.swift in Sources */, D12E0C981C47F91800AC98AD /* ImageCollectionViewCell.swift in Sources */, D198F42025EDC34000C53E0D /* SizingAnimationDemo.swift in Sources */, 072922432638639D0089E810 /* AnimatedImageDemo.swift in Sources */, 4B6E1B6D28DB4E8C0023B54B /* Issue1998View.swift in Sources */, D1A1CCA721A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */, D14799D92E1129A900053537 /* LoadingFailureDemo.swift in Sources */, D1E612E22D75F9AC00DACD51 /* ProgressiveJPEGDemo.swift in Sources */, 4B92FE5625FF906B00473088 /* AutoSizingTableViewController.swift in Sources */, D1F78A642589F17200930759 /* ListDemo.swift in Sources */, D198F41E25EDC11500C53E0D /* LazyVStackDemo.swift in Sources */, D1E4CF5421BACBA6004D029D /* ImageDataProviderCollectionViewController.swift in Sources */, 4B9AD88F26480D3B0086A261 /* OrientationImagesViewController.swift in Sources */, D1FAB06F21A853E600908910 /* HighResolutionCollectionViewController.swift in Sources */, D1F78A652589F17200930759 /* MainView.swift in Sources */, 4B779C8526743C2800FF9C1E /* GeometryReaderDemo.swift in Sources */, D1F78A5F2589F0AA00930759 /* SwiftUIViewController.swift in Sources */, 0784D0992F17E41700EB2A07 /* PhotosPickerDemo.swift in Sources */, D12E0C951C47F91800AC98AD /* AppDelegate.swift in Sources */, D1F06F3321AA4292000B1C38 /* DetailImageViewController.swift in Sources */, D198F42225EDC4B900C53E0D /* GridDemo.swift in Sources */, 4B1C7A3D21A256E300CE9D31 /* InfinityCollectionViewController.swift in Sources */, 078DCB512BCFEFB40008114E /* PHPickerResultViewController.swift in Sources */, D1EDF7422C9F01270017FFA5 /* Issue2295View.swift in Sources */, D1A1CCA321A1879600263AD8 /* MainViewController.swift in Sources */, D16AAF282D5247CF00E7F764 /* Issue2352View.swift in Sources */, D12F67682CB10AE000AB63AB /* LivePhotoViewController.swift in Sources */, 4BC0ED4A29A6EE78003E9CD1 /* Issue2035View.swift in Sources */, D1F06F3721AAEACF000B1C38 /* GIFViewController.swift in Sources */, 4B120CA726B91BB70060B092 /* TransitionViewDemo.swift in Sources */, 769F07F126298E2E00610767 /* GIFHeavyViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ D1679A481C4E78B20020FD12 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D1679A441C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension */; targetProxy = D1679A471C4E78B20020FD12 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 277EAE8A2045B39C00547CD3 /* Interface.storyboard */ = { isa = PBXVariantGroup; children = ( 277EAE8B2045B39C00547CD3 /* Base */, ); name = Interface.storyboard; sourceTree = ""; }; 4BCCF3381D5B02F8003387C2 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 4BCCF3391D5B02F8003387C2 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; D12E0C8F1C47F91800AC98AD /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( D12E0C901C47F91800AC98AD /* Base */, ); name = Main.storyboard; sourceTree = ""; }; D12E0C9F1C47F92200AC98AD /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( D12E0CA01C47F92200AC98AD /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 4B2944621C3D03880088C3E7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = A4YJ9MRZ66; ENABLE_HARDENED_RUNTIME = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "Demo/Kingfisher-macOS-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.Kingfisher-OSX-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; }; name = Debug; }; 4B2944631C3D03880088C3E7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = A4YJ9MRZ66; ENABLE_HARDENED_RUNTIME = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "Demo/Kingfisher-macOS-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.Kingfisher-OSX-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; D13F49CE1BEDA53F00CE335D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "Demo/Kingfisher-tvOS-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.Kingfisher-tvOS-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; TARGETED_DEVICE_FAMILY = 3; }; name = Debug; }; D13F49CF1BEDA53F00CE335D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "Demo/Kingfisher-tvOS-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.Kingfisher-tvOS-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 3; }; name = Release; }; D1679A551C4E78B20020FD12 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "Demo/Kingfisher-watchOS-Demo Extension/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.Kingfisher-Demo.watchkitapp.watchkitextension"; PRODUCT_NAME = "${TARGET_NAME}"; SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 4; }; name = Debug; }; D1679A561C4E78B20020FD12 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "Demo/Kingfisher-watchOS-Demo Extension/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.Kingfisher-Demo.watchkitapp.watchkitextension"; PRODUCT_NAME = "${TARGET_NAME}"; SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 4; }; name = Release; }; D1679A591C4E78B20020FD12 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; GCC_NO_COMMON_BLOCKS = YES; IBSC_MODULE = Kingfisher_watchOS_Demo_Extension; INFOPLIST_FILE = "Demo/Kingfisher-watchOS-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.Kingfisher-Demo.watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = watchos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 4; }; name = Debug; }; D1679A5A1C4E78B20020FD12 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; GCC_NO_COMMON_BLOCKS = YES; IBSC_MODULE = Kingfisher_watchOS_Demo_Extension; INFOPLIST_FILE = "Demo/Kingfisher-watchOS-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.Kingfisher-Demo.watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = watchos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 4; }; name = Release; }; D1ED2D281AD2CFA600CFC3EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 13.0; WATCHOS_DEPLOYMENT_TARGET = 6.0; XROS_DEPLOYMENT_TARGET = 1.0; }; name = Debug; }; D1ED2D291AD2CFA600CFC3EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 6.0; XROS_DEPLOYMENT_TARGET = 1.0; }; name = Release; }; D1ED2D2B1AD2CFA600CFC3EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Kingfisher-Demo.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = A4YJ9MRZ66; INFOPLIST_FILE = "Demo/Kingfisher-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; }; D1ED2D2C1AD2CFA600CFC3EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Kingfisher-Demo.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = A4YJ9MRZ66; INFOPLIST_FILE = "Demo/Kingfisher-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 4B2944611C3D03880088C3E7 /* Build configuration list for PBXNativeTarget "Kingfisher-macOS-Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( 4B2944621C3D03880088C3E7 /* Debug */, 4B2944631C3D03880088C3E7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D13F49D01BEDA53F00CE335D /* Build configuration list for PBXNativeTarget "Kingfisher-tvOS-Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( D13F49CE1BEDA53F00CE335D /* Debug */, D13F49CF1BEDA53F00CE335D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D1679A541C4E78B20020FD12 /* Build configuration list for PBXNativeTarget "Kingfisher-watchOS-Demo Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( D1679A551C4E78B20020FD12 /* Debug */, D1679A561C4E78B20020FD12 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D1679A581C4E78B20020FD12 /* Build configuration list for PBXNativeTarget "Kingfisher-watchOS-Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( D1679A591C4E78B20020FD12 /* Debug */, D1679A5A1C4E78B20020FD12 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject "Kingfisher-Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( D1ED2D281AD2CFA600CFC3EB /* Debug */, D1ED2D291AD2CFA600CFC3EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D1ED2D2A1AD2CFA600CFC3EB /* Build configuration list for PBXNativeTarget "Kingfisher-Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( D1ED2D2B1AD2CFA600CFC3EB /* Debug */, D1ED2D2C1AD2CFA600CFC3EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = D1ED2D031AD2CFA600CFC3EB /* Project object */; } ================================================ FILE: Demo/Kingfisher-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Demo/Kingfisher-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Demo/Kingfisher-Demo.xcodeproj/xcshareddata/xcschemes/Kingfisher-Demo.xcscheme ================================================ ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" gem "base64", "~> 0.2.0" gem "fastlane" gem "cocoapods" ================================================ FILE: Kingfisher.json ================================================ { "8.3.2": "https://github.com/onevcat/Kingfisher/releases/download/8.3.2/Kingfisher-8.3.2.xcframework.zip", "8.3.1": "https://github.com/onevcat/Kingfisher/releases/download/8.3.1/Kingfisher-8.3.1.xcframework.zip", "8.3.0": "https://github.com/onevcat/Kingfisher/releases/download/8.3.0/Kingfisher-8.3.0.xcframework.zip" } ================================================ FILE: Kingfisher.podspec ================================================ Pod::Spec.new do |s| s.name = "Kingfisher" s.version = "8.8.0" s.summary = "A lightweight and pure Swift implemented library for downloading and cacheing image from the web." s.description = <<-DESC 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. * Everything in Kingfisher goes asynchronously, not only downloading, but also caching. That means you can never worry about blocking your UI thread. * 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. * 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. * 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. * Cancellable processing task. You can cancel the downloading or retriving image process if it is not needed anymore. * Independent components. You can use the downloader or caching system separately. Or even create your own cache based on Kingfisher's code. * Options to decompress the image in background before render it, which could improve the UI performance. * A category over `UIImageView` for setting image from an url directly. DESC s.homepage = "https://github.com/onevcat/Kingfisher" s.screenshots = "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png" s.license = { :type => "MIT", :file => "LICENSE" } s.authors = { "onevcat" => "onevcat@gmail.com" } s.social_media_url = "https://github.com/onevcat" s.swift_versions = ['5.0'] s.ios.deployment_target = "13.0" s.tvos.deployment_target = "13.0" s.osx.deployment_target = "10.15" s.watchos.deployment_target = "6.0" s.visionos.deployment_target = "1.0" s.source = { :git => "https://github.com/onevcat/Kingfisher.git", :tag => s.version } s.source_files = ["Sources/**/*.swift"] s.resource_bundles = {"Kingfisher" => ["Sources/PrivacyInfo.xcprivacy"]} s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } s.requires_arc = true s.frameworks = "CFNetwork", "Accelerate" s.weak_frameworks = "SwiftUI", "Combine" end ================================================ FILE: Kingfisher.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 00A8E26E2E81B89600ABB84F /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A8E26D2E81B89600ABB84F /* NetworkMonitor.swift */; }; 07292245263B02F00089E810 /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07292244263B02F00089E810 /* KFAnimatedImage.swift */; }; 0784D09B2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0784D09A2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift */; }; 078DCB4F2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */; }; 22FDCE0E2700078B0044D11E /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */; }; 388F37382B4D9CDB0089705C /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388F37372B4D9CDB0089705C /* DisplayLink.swift */; }; 3ADE9AF92A73CD69009A86CA /* String+SHA256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */; }; 4B10480D216F157000300C61 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B10480C216F157000300C61 /* ImageDataProcessor.swift */; }; 4B46CC5F217449C600D90C4A /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC5E217449C600D90C4A /* MemoryStorage.swift */; }; 4B46CC64217449E000D90C4A /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC63217449E000D90C4A /* Storage.swift */; }; 4B46CC6921744AC500D90C4A /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC6821744AC500D90C4A /* DiskStorage.swift */; }; 4B8351C8217066580081EED8 /* StubHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8351C7217066580081EED8 /* StubHelpers.swift */; }; 4B8351CC217084660081EED8 /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8351CB217084660081EED8 /* Runtime.swift */; }; 4B88CEB02646C056009EBB41 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */; }; 4B88CEB22646C653009EBB41 /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */; }; 4B88CEB42646D0BF009EBB41 /* ImageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88CEB32646D0BF009EBB41 /* ImageContext.swift */; }; 4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */; }; 4B8E291C216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */; }; 4BA3BF1E228BCDD100909201 /* DataReceivingSideEffectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */; }; 4BCFF7A621990DB70055AAC4 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */; }; 4BCFF7AA219932390055AAC4 /* DiskStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCFF7A9219932390055AAC4 /* DiskStorageTests.swift */; }; 4BD821622189FC0C0084CC21 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */; }; 4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821662189FD330084CC21 /* SessionDataTask.swift */; }; 4BE688F722FD513100B11168 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */; }; 76FB4FD2262D773E006D15F8 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FB4FD1262D773E006D15F8 /* GraphicsContext.swift */; }; C9286407228584EB00257182 /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9286406228584EB00257182 /* ImageProgressive.swift */; }; D1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1132C9625919F69003E528D /* KFOptionsSetter.swift */; }; D11D9B72245FA6F700C5A0AE /* RetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11D9B71245FA6F700C5A0AE /* RetryStrategy.swift */; }; D12AB6C0215D2BB50013BA68 /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB69D215D2BB50013BA68 /* RequestModifier.swift */; }; D12AB6C4215D2BB50013BA68 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB69E215D2BB50013BA68 /* Resource.swift */; }; D12AB6C8215D2BB50013BA68 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB69F215D2BB50013BA68 /* ImageDownloader.swift */; }; D12AB6CC215D2BB50013BA68 /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A0215D2BB50013BA68 /* ImageModifier.swift */; }; D12AB6D0215D2BB50013BA68 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A1215D2BB50013BA68 /* ImagePrefetcher.swift */; }; D12AB6D4215D2BB50013BA68 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A3215D2BB50013BA68 /* Image.swift */; }; D12AB6D8215D2BB50013BA68 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A4215D2BB50013BA68 /* ImageTransition.swift */; }; D12AB6DC215D2BB50013BA68 /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A5215D2BB50013BA68 /* ImageProcessor.swift */; }; D12AB6E0215D2BB50013BA68 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A6215D2BB50013BA68 /* Filter.swift */; }; D12AB6E4215D2BB50013BA68 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A7215D2BB50013BA68 /* Placeholder.swift */; }; D12AB6E8215D2BB50013BA68 /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6A8215D2BB50013BA68 /* GIFAnimatedImage.swift */; }; D12AB6F4215D2BB50013BA68 /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */; }; D12AB6FC215D2BB50013BA68 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */; }; D12AB704215D2BB50013BA68 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B1215D2BB50013BA68 /* Kingfisher.swift */; }; D12AB708215D2BB50013BA68 /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B2215D2BB50013BA68 /* KingfisherError.swift */; }; D12AB70C215D2BB50013BA68 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B3215D2BB50013BA68 /* KingfisherManager.swift */; }; D12AB710215D2BB50013BA68 /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B4215D2BB50013BA68 /* KingfisherOptionsInfo.swift */; }; D12AB714215D2BB50013BA68 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B6215D2BB50013BA68 /* ImageCache.swift */; }; D12AB718215D2BB50013BA68 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B7215D2BB50013BA68 /* CacheSerializer.swift */; }; D12AB71C215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B8215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift */; }; D12AB724215D2BB50013BA68 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BB215D2BB50013BA68 /* Box.swift */; }; D12AB72C215D2BB50013BA68 /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BE215D2BB50013BA68 /* Indicator.swift */; }; D12AB730215D2BB50013BA68 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BF215D2BB50013BA68 /* AnimatedImageView.swift */; }; D12E0C4F1C47F23500AC98AD /* dancing-banana.gif in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C441C47F23500AC98AD /* dancing-banana.gif */; }; D12E0C501C47F23500AC98AD /* ImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C451C47F23500AC98AD /* ImageCacheTests.swift */; }; D12E0C511C47F23500AC98AD /* ImageDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C461C47F23500AC98AD /* ImageDownloaderTests.swift */; }; D12E0C521C47F23500AC98AD /* ImageExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C471C47F23500AC98AD /* ImageExtensionTests.swift */; }; D12E0C531C47F23500AC98AD /* ImageViewExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C481C47F23500AC98AD /* ImageViewExtensionTests.swift */; }; D12E0C551C47F23500AC98AD /* KingfisherManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4A1C47F23500AC98AD /* KingfisherManagerTests.swift */; }; D12E0C561C47F23500AC98AD /* KingfisherOptionsInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4B1C47F23500AC98AD /* KingfisherOptionsInfoTests.swift */; }; D12E0C571C47F23500AC98AD /* KingfisherTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4C1C47F23500AC98AD /* KingfisherTestHelper.swift */; }; D12E0C581C47F23500AC98AD /* UIButtonExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */; }; D12EB83C24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */; }; D12F67602CAC2DBF00AB63AB /* ImageDownloader+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F675F2CAC2DB700AB63AB /* ImageDownloader+LivePhoto.swift */; }; D12F67622CAC32BF00AB63AB /* KingfisherManager+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */; }; D12F67642CAC330A00AB63AB /* LivePhotoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67632CAC330600AB63AB /* LivePhotoSource.swift */; }; D12F67662CB022FC00AB63AB /* PHLivePhotoView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67652CB022EB00AB63AB /* PHLivePhotoView+Kingfisher.swift */; }; D13646742165A1A100A33652 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = D13646732165A1A100A33652 /* Result.swift */; }; D16CC3D624E02E9500F1A515 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */; }; D16FEA3A23078C63006E67D5 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = D16FE9F623078C63006E67D5 /* LICENSE */; }; D16FEA3B23078C63006E67D5 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = D16FE9F723078C63006E67D5 /* README.md */; }; D16FEA3C23078C63006E67D5 /* LSStubRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FE9FA23078C63006E67D5 /* LSStubRequest.m */; }; D16FEA3D23078C63006E67D5 /* LSStubResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FE9FB23078C63006E67D5 /* LSStubResponse.m */; }; D16FEA3E23078C63006E67D5 /* LSNocilla.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FE9FE23078C63006E67D5 /* LSNocilla.m */; }; D16FEA3F23078C63006E67D5 /* LSHTTPRequestDiff.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0023078C63006E67D5 /* LSHTTPRequestDiff.m */; }; D16FEA4023078C63006E67D5 /* LSHTTPClientHook.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0423078C63006E67D5 /* LSHTTPClientHook.m */; }; D16FEA4123078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0723078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m */; }; D16FEA4223078C63006E67D5 /* LSNSURLHook.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0C23078C63006E67D5 /* LSNSURLHook.m */; }; D16FEA4323078C63006E67D5 /* NSURLRequest+DSL.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0D23078C63006E67D5 /* NSURLRequest+DSL.m */; }; D16FEA4423078C63006E67D5 /* LSHTTPStubURLProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA0E23078C63006E67D5 /* LSHTTPStubURLProtocol.m */; }; D16FEA4523078C63006E67D5 /* ASIHTTPRequestStub.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA1123078C63006E67D5 /* ASIHTTPRequestStub.m */; }; D16FEA4623078C63006E67D5 /* LSASIHTTPRequestHook.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA1323078C63006E67D5 /* LSASIHTTPRequestHook.m */; }; D16FEA4723078C63006E67D5 /* LSASIHTTPRequestAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA1423078C63006E67D5 /* LSASIHTTPRequestAdapter.m */; }; D16FEA4823078C63006E67D5 /* LSNSURLSessionHook.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA1823078C63006E67D5 /* LSNSURLSessionHook.m */; }; D16FEA4923078C63006E67D5 /* NSRegularExpression+Matcheable.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2223078C63006E67D5 /* NSRegularExpression+Matcheable.m */; }; D16FEA4A23078C63006E67D5 /* NSString+Matcheable.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2423078C63006E67D5 /* NSString+Matcheable.m */; }; D16FEA4B23078C63006E67D5 /* NSData+Matcheable.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2523078C63006E67D5 /* NSData+Matcheable.m */; }; D16FEA4C23078C63006E67D5 /* LSDataMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2623078C63006E67D5 /* LSDataMatcher.m */; }; D16FEA4D23078C63006E67D5 /* LSMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2723078C63006E67D5 /* LSMatcher.m */; }; D16FEA4E23078C63006E67D5 /* LSRegexMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2823078C63006E67D5 /* LSRegexMatcher.m */; }; D16FEA4F23078C63006E67D5 /* LSStringMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA2A23078C63006E67D5 /* LSStringMatcher.m */; }; D16FEA5023078C63006E67D5 /* NSString+Nocilla.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3023078C63006E67D5 /* NSString+Nocilla.m */; }; D16FEA5123078C63006E67D5 /* NSData+Nocilla.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3123078C63006E67D5 /* NSData+Nocilla.m */; }; D16FEA5223078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3423078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m */; }; D16FEA5323078C63006E67D5 /* LSStubResponseDSL.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3623078C63006E67D5 /* LSStubResponseDSL.m */; }; D16FEA5423078C63006E67D5 /* LSStubRequestDSL.m in Sources */ = {isa = PBXBuildFile; fileRef = D16FEA3723078C63006E67D5 /* LSStubRequestDSL.m */; }; D16FEA5523079707006E67D5 /* NSButtonExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */; }; D1839845216E333E003927D3 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1839844216E333E003927D3 /* Delegate.swift */; }; D186696D21834261002B502E /* ImageDrawingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D186696C21834261002B502E /* ImageDrawingTests.swift */; }; D1889534258F7649003B73BE /* KFImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1889533258F7648003B73BE /* KFImageOptions.swift */; }; D18B3222251852E100662F63 /* KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18B3221251852E100662F63 /* KF.swift */; }; D1A1CC9A219FAB4B00263AD8 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CC99219FAB4B00263AD8 /* Source.swift */; }; D1A1CC9F21A0F98600263AD8 /* ImageDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1CC9E21A0F98600263AD8 /* ImageDataProviderTests.swift */; }; D1A37BDE215D34E8009B39B7 /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A37BDD215D34E8009B39B7 /* ImageDrawing.swift */; }; D1A37BE3215D359F009B39B7 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A37BE2215D359F009B39B7 /* ImageFormat.swift */; }; D1A37BE8215D365A009B39B7 /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A37BE7215D365A009B39B7 /* ExtensionHelpers.swift */; }; D1A37BF2215D3850009B39B7 /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A37BF1215D3850009B39B7 /* SizeExtensions.swift */; }; D1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F7607523097532000C5269 /* ImageBinder.swift */; }; D1AEB09725890DEA008556DF /* KFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F7607623097532000C5269 /* KFImage.swift */; }; D1BA781D2174D07800C69D7B /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BA781C2174D07800C69D7B /* CallbackQueue.swift */; }; D1BFED95222ACC6B009330C8 /* ImageProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BFED94222ACC6B009330C8 /* ImageProcessorTests.swift */; }; D1D2C32A1C70A3230018F2F9 /* single-frame.gif in Resources */ = {isa = PBXBuildFile; fileRef = D1D2C3291C70A3230018F2F9 /* single-frame.gif */; }; D1D550D52AEB9E8700AAD79D /* PrivacyInfo.xcprivacy in CopyFiles */ = {isa = PBXBuildFile; fileRef = D1C04A3E2A45D20500B3775F /* PrivacyInfo.xcprivacy */; }; D1DC4B411D60996D00DFDFAA /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DC4B401D60996D00DFDFAA /* StringExtensionTests.swift */; }; D1E564412199C21E0057AAE3 /* StorageExpirationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E564402199C21E0057AAE3 /* StorageExpirationTests.swift */; }; D1E56445219B16330057AAE3 /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E56444219B16330057AAE3 /* ImageDataProvider.swift */; }; D1ED2D401AD2D09F00CFC3EB /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */; }; D1F1F6FF24625EC600910725 /* RetryStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F1F6FE24625EC600910725 /* RetryStrategyTests.swift */; }; D1F66CC12CB2CF2E004959F3 /* LivePhotoSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F66CC02CB2CF2D004959F3 /* LivePhotoSourceTests.swift */; }; D8FCF6A821C5A0E500F9ABC0 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */; }; D9638BA61C7DC71F0046523D /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */; }; E9E3ED8B2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */; }; F39B68C82E33AC2A00404B02 /* NetworkMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39B68C72E33AC2A00404B02 /* NetworkMetrics.swift */; }; F72CE9CE1FCF17ED00CC522A /* ImageModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */; }; 38D5D3A32C5C757E00BF1D01 /* PixelFormatDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5D3A42C5C757E00BF1D01 /* PixelFormatDecodingTests.swift */; }; 38D5D3C12C5C7A1800BF1D01 /* gradient-8b-srgb-opaque.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3AC2C5C784700BF1D01 /* gradient-8b-srgb-opaque.png */; }; 38D5D3C22C5C7A1800BF1D01 /* gradient-8b-srgb-alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3AD2C5C784700BF1D01 /* gradient-8b-srgb-alpha.png */; }; 38D5D3C32C5C7A1800BF1D01 /* gradient-8b-displayp3-alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3AE2C5C784700BF1D01 /* gradient-8b-displayp3-alpha.png */; }; 38D5D3C42C5C7A1800BF1D01 /* gradient-8b-gray.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3AF2C5C784700BF1D01 /* gradient-8b-gray.png */; }; 38D5D3C52C5C7A1800BF1D01 /* gradient-10b-srgb-opaque.heic in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B02C5C784700BF1D01 /* gradient-10b-srgb-opaque.heic */; }; 38D5D3C62C5C7A1800BF1D01 /* gradient-10b-srgb-alpha.heic in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B12C5C784700BF1D01 /* gradient-10b-srgb-alpha.heic */; }; 38D5D3C72C5C7A1800BF1D01 /* gradient-10b-displayp3-alpha.heic in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B22C5C784700BF1D01 /* gradient-10b-displayp3-alpha.heic */; }; 38D5D3C82C5C7A1800BF1D01 /* gradient-16b-srgb-alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B32C5C784700BF1D01 /* gradient-16b-srgb-alpha.png */; }; 38D5D3C92C5C7A1800BF1D01 /* gradient-16b-gray.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D5D3B42C5C784700BF1D01 /* gradient-16b-gray.png */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ D1ED2D411AD2D09F00CFC3EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D1ED2D031AD2CFA600CFC3EB /* Project object */; proxyType = 1; remoteGlobalIDString = D1ED2D341AD2D09F00CFC3EB; remoteInfo = Kingfisher; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ D1D550D42AEB9E7300AAD79D /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( D1D550D52AEB9E8700AAD79D /* PrivacyInfo.xcprivacy in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 00A8E26D2E81B89600ABB84F /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; 07292244263B02F00089E810 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFAnimatedImage.swift; sourceTree = ""; }; 0784D09A2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosPickerItemImageDataProvider.swift; sourceTree = ""; }; 078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultImageDataProvider.swift; sourceTree = ""; }; 185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSButtonExtensionTests.swift; sourceTree = ""; }; 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CPListItem+Kingfisher.swift"; sourceTree = ""; }; 388F37372B4D9CDB0089705C /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = ""; }; 3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SHA256.swift"; sourceTree = ""; }; 4B10480C216F157000300C61 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataProcessor.swift; sourceTree = ""; }; 4B164ACE1B8D554200768EC6 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; 4B3E714D1B01FEB200F5AAED /* WatchKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WatchKit.framework; path = System/Library/Frameworks/WatchKit.framework; sourceTree = SDKROOT; }; 4B46CC5E217449C600D90C4A /* MemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorage.swift; sourceTree = ""; }; 4B46CC63217449E000D90C4A /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 4B46CC6821744AC500D90C4A /* DiskStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskStorage.swift; sourceTree = ""; }; 4B8351C7217066580081EED8 /* StubHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubHelpers.swift; sourceTree = ""; }; 4B8351CB217084660081EED8 /* Runtime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Runtime.swift; sourceTree = ""; }; 4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageProtocol.swift; sourceTree = ""; }; 4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageRenderer.swift; sourceTree = ""; }; 4B88CEB32646D0BF009EBB41 /* ImageContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContext.swift; sourceTree = ""; }; 4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloaderDelegate.swift; sourceTree = ""; }; 4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationChallengeResponsable.swift; sourceTree = ""; }; 4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataReceivingSideEffectTests.swift; sourceTree = ""; }; 38D5D3A42C5C757E00BF1D01 /* PixelFormatDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFormatDecodingTests.swift; sourceTree = ""; }; 4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorageTests.swift; sourceTree = ""; }; 4BCFF7A9219932390055AAC4 /* DiskStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskStorageTests.swift; sourceTree = ""; }; 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDelegate.swift; sourceTree = ""; }; 4BD821662189FD330084CC21 /* SessionDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDataTask.swift; sourceTree = ""; }; 38D5D3AC2C5C784700BF1D01 /* gradient-8b-srgb-opaque.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-8b-srgb-opaque.png; sourceTree = ""; }; 38D5D3AD2C5C784700BF1D01 /* gradient-8b-srgb-alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-8b-srgb-alpha.png; sourceTree = ""; }; 38D5D3AE2C5C784700BF1D01 /* gradient-8b-displayp3-alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-8b-displayp3-alpha.png; sourceTree = ""; }; 38D5D3AF2C5C784700BF1D01 /* gradient-8b-gray.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-8b-gray.png; sourceTree = ""; }; 38D5D3B02C5C784700BF1D01 /* gradient-10b-srgb-opaque.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = gradient-10b-srgb-opaque.heic; sourceTree = ""; }; 38D5D3B12C5C784700BF1D01 /* gradient-10b-srgb-alpha.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = gradient-10b-srgb-alpha.heic; sourceTree = ""; }; 38D5D3B22C5C784700BF1D01 /* gradient-10b-displayp3-alpha.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = gradient-10b-displayp3-alpha.heic; sourceTree = ""; }; 38D5D3B32C5C784700BF1D01 /* gradient-16b-srgb-alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-16b-srgb-alpha.png; sourceTree = ""; }; 38D5D3B42C5C784700BF1D01 /* gradient-16b-gray.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gradient-16b-gray.png; sourceTree = ""; }; 76FB4FD1262D773E006D15F8 /* GraphicsContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicsContext.swift; sourceTree = ""; }; C9286406228584EB00257182 /* ImageProgressive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProgressive.swift; sourceTree = ""; }; C959EEE7228940FE00467A10 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; D1132C9625919F69003E528D /* KFOptionsSetter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFOptionsSetter.swift; sourceTree = ""; }; D11D9B71245FA6F700C5A0AE /* RetryStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryStrategy.swift; sourceTree = ""; }; D12AB69D215D2BB50013BA68 /* RequestModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestModifier.swift; sourceTree = ""; }; D12AB69E215D2BB50013BA68 /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = ""; }; D12AB69F215D2BB50013BA68 /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; D12AB6A0215D2BB50013BA68 /* ImageModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageModifier.swift; sourceTree = ""; }; D12AB6A1215D2BB50013BA68 /* ImagePrefetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcher.swift; sourceTree = ""; }; D12AB6A3215D2BB50013BA68 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; D12AB6A4215D2BB50013BA68 /* ImageTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageTransition.swift; sourceTree = ""; }; D12AB6A5215D2BB50013BA68 /* ImageProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = ""; }; D12AB6A6215D2BB50013BA68 /* Filter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; D12AB6A7215D2BB50013BA68 /* Placeholder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Placeholder.swift; sourceTree = ""; }; D12AB6A8215D2BB50013BA68 /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GIFAnimatedImage.swift; sourceTree = ""; }; D12AB6A9215D2BB50013BA68 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ImageView+Kingfisher.swift"; sourceTree = ""; }; D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSButton+Kingfisher.swift"; sourceTree = ""; }; D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Kingfisher.swift"; sourceTree = ""; }; D12AB6B1215D2BB50013BA68 /* Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Kingfisher.swift; sourceTree = ""; }; D12AB6B2215D2BB50013BA68 /* KingfisherError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherError.swift; sourceTree = ""; }; D12AB6B3215D2BB50013BA68 /* KingfisherManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherManager.swift; sourceTree = ""; }; D12AB6B4215D2BB50013BA68 /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherOptionsInfo.swift; sourceTree = ""; }; D12AB6B6215D2BB50013BA68 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; D12AB6B7215D2BB50013BA68 /* CacheSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheSerializer.swift; sourceTree = ""; }; D12AB6B8215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormatIndicatedCacheSerializer.swift; sourceTree = ""; }; D12AB6BB215D2BB50013BA68 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; D12AB6BE215D2BB50013BA68 /* Indicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Indicator.swift; sourceTree = ""; }; D12AB6BF215D2BB50013BA68 /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = ""; }; D12E0C441C47F23500AC98AD /* dancing-banana.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "dancing-banana.gif"; sourceTree = ""; }; D12E0C451C47F23500AC98AD /* ImageCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheTests.swift; sourceTree = ""; }; D12E0C461C47F23500AC98AD /* ImageDownloaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloaderTests.swift; sourceTree = ""; }; D12E0C471C47F23500AC98AD /* ImageExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageExtensionTests.swift; sourceTree = ""; }; D12E0C481C47F23500AC98AD /* ImageViewExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewExtensionTests.swift; sourceTree = ""; }; D12E0C491C47F23500AC98AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D12E0C4A1C47F23500AC98AD /* KingfisherManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherManagerTests.swift; sourceTree = ""; }; D12E0C4B1C47F23500AC98AD /* KingfisherOptionsInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherOptionsInfoTests.swift; sourceTree = ""; }; D12E0C4C1C47F23500AC98AD /* KingfisherTestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherTestHelper.swift; sourceTree = ""; }; D12E0C4D1C47F23500AC98AD /* KingfisherTests-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "KingfisherTests-Bridging-Header.h"; sourceTree = ""; }; D12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensionTests.swift; sourceTree = ""; }; D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Kingfisher.swift"; sourceTree = ""; }; D12F675F2CAC2DB700AB63AB /* ImageDownloader+LivePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageDownloader+LivePhoto.swift"; sourceTree = ""; }; D12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KingfisherManager+LivePhoto.swift"; sourceTree = ""; }; D12F67632CAC330600AB63AB /* LivePhotoSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoSource.swift; sourceTree = ""; }; D12F67652CB022EB00AB63AB /* PHLivePhotoView+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHLivePhotoView+Kingfisher.swift"; sourceTree = ""; }; D1356CEA2B273AEC009554C8 /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = ""; }; D13646732165A1A100A33652 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; D16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVAssetImageDataProvider.swift; sourceTree = ""; }; D16FE9F623078C63006E67D5 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; D16FE9F723078C63006E67D5 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; D16FE9FA23078C63006E67D5 /* LSStubRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStubRequest.m; sourceTree = ""; }; D16FE9FB23078C63006E67D5 /* LSStubResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStubResponse.m; sourceTree = ""; }; D16FE9FC23078C63006E67D5 /* LSStubResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStubResponse.h; sourceTree = ""; }; D16FE9FD23078C63006E67D5 /* LSStubRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStubRequest.h; sourceTree = ""; }; D16FE9FE23078C63006E67D5 /* LSNocilla.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSNocilla.m; sourceTree = ""; }; D16FEA0023078C63006E67D5 /* LSHTTPRequestDiff.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSHTTPRequestDiff.m; sourceTree = ""; }; D16FEA0123078C63006E67D5 /* LSHTTPRequestDiff.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPRequestDiff.h; sourceTree = ""; }; D16FEA0223078C63006E67D5 /* Nocilla.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Nocilla.h; sourceTree = ""; }; D16FEA0423078C63006E67D5 /* LSHTTPClientHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSHTTPClientHook.m; sourceTree = ""; }; D16FEA0523078C63006E67D5 /* LSHTTPClientHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPClientHook.h; sourceTree = ""; }; D16FEA0723078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLRequest+LSHTTPRequest.m"; sourceTree = ""; }; D16FEA0823078C63006E67D5 /* NSURLRequest+DSL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+DSL.h"; sourceTree = ""; }; D16FEA0923078C63006E67D5 /* LSNSURLHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSNSURLHook.h; sourceTree = ""; }; D16FEA0A23078C63006E67D5 /* LSHTTPStubURLProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPStubURLProtocol.h; sourceTree = ""; }; D16FEA0B23078C63006E67D5 /* NSURLRequest+LSHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+LSHTTPRequest.h"; sourceTree = ""; }; D16FEA0C23078C63006E67D5 /* LSNSURLHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSNSURLHook.m; sourceTree = ""; }; D16FEA0D23078C63006E67D5 /* NSURLRequest+DSL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLRequest+DSL.m"; sourceTree = ""; }; D16FEA0E23078C63006E67D5 /* LSHTTPStubURLProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSHTTPStubURLProtocol.m; sourceTree = ""; }; D16FEA1023078C63006E67D5 /* LSASIHTTPRequestHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSASIHTTPRequestHook.h; sourceTree = ""; }; D16FEA1123078C63006E67D5 /* ASIHTTPRequestStub.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIHTTPRequestStub.m; sourceTree = ""; }; D16FEA1223078C63006E67D5 /* LSASIHTTPRequestAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSASIHTTPRequestAdapter.h; sourceTree = ""; }; D16FEA1323078C63006E67D5 /* LSASIHTTPRequestHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSASIHTTPRequestHook.m; sourceTree = ""; }; D16FEA1423078C63006E67D5 /* LSASIHTTPRequestAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSASIHTTPRequestAdapter.m; sourceTree = ""; }; D16FEA1523078C63006E67D5 /* ASIHTTPRequestStub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIHTTPRequestStub.h; sourceTree = ""; }; D16FEA1723078C63006E67D5 /* LSNSURLSessionHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSNSURLSessionHook.h; sourceTree = ""; }; D16FEA1823078C63006E67D5 /* LSNSURLSessionHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSNSURLSessionHook.m; sourceTree = ""; }; D16FEA1A23078C63006E67D5 /* LSHTTPBody.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPBody.h; sourceTree = ""; }; D16FEA1B23078C63006E67D5 /* LSHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPRequest.h; sourceTree = ""; }; D16FEA1C23078C63006E67D5 /* LSHTTPResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPResponse.h; sourceTree = ""; }; D16FEA1D23078C63006E67D5 /* LSNocilla.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSNocilla.h; sourceTree = ""; }; D16FEA1F23078C63006E67D5 /* LSMatcheable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSMatcheable.h; sourceTree = ""; }; D16FEA2023078C63006E67D5 /* LSMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSMatcher.h; sourceTree = ""; }; D16FEA2123078C63006E67D5 /* LSStringMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStringMatcher.h; sourceTree = ""; }; D16FEA2223078C63006E67D5 /* NSRegularExpression+Matcheable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSRegularExpression+Matcheable.m"; sourceTree = ""; }; D16FEA2323078C63006E67D5 /* LSRegexMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSRegexMatcher.h; sourceTree = ""; }; D16FEA2423078C63006E67D5 /* NSString+Matcheable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Matcheable.m"; sourceTree = ""; }; D16FEA2523078C63006E67D5 /* NSData+Matcheable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Matcheable.m"; sourceTree = ""; }; D16FEA2623078C63006E67D5 /* LSDataMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSDataMatcher.m; sourceTree = ""; }; D16FEA2723078C63006E67D5 /* LSMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSMatcher.m; sourceTree = ""; }; D16FEA2823078C63006E67D5 /* LSRegexMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSRegexMatcher.m; sourceTree = ""; }; D16FEA2923078C63006E67D5 /* NSRegularExpression+Matcheable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSRegularExpression+Matcheable.h"; sourceTree = ""; }; D16FEA2A23078C63006E67D5 /* LSStringMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStringMatcher.m; sourceTree = ""; }; D16FEA2B23078C63006E67D5 /* NSString+Matcheable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Matcheable.h"; sourceTree = ""; }; D16FEA2C23078C63006E67D5 /* LSDataMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSDataMatcher.h; sourceTree = ""; }; D16FEA2D23078C63006E67D5 /* NSData+Matcheable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Matcheable.h"; sourceTree = ""; }; D16FEA2F23078C63006E67D5 /* NSData+Nocilla.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Nocilla.h"; sourceTree = ""; }; D16FEA3023078C63006E67D5 /* NSString+Nocilla.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Nocilla.m"; sourceTree = ""; }; D16FEA3123078C63006E67D5 /* NSData+Nocilla.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Nocilla.m"; sourceTree = ""; }; D16FEA3223078C63006E67D5 /* NSString+Nocilla.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Nocilla.h"; sourceTree = ""; }; D16FEA3423078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSHTTPRequestDSLRepresentation.m; sourceTree = ""; }; D16FEA3523078C63006E67D5 /* LSStubRequestDSL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStubRequestDSL.h; sourceTree = ""; }; D16FEA3623078C63006E67D5 /* LSStubResponseDSL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStubResponseDSL.m; sourceTree = ""; }; D16FEA3723078C63006E67D5 /* LSStubRequestDSL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSStubRequestDSL.m; sourceTree = ""; }; D16FEA3823078C63006E67D5 /* LSHTTPRequestDSLRepresentation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSHTTPRequestDSLRepresentation.h; sourceTree = ""; }; D16FEA3923078C63006E67D5 /* LSStubResponseDSL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSStubResponseDSL.h; sourceTree = ""; }; D1839844216E333E003927D3 /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; D186696C21834261002B502E /* ImageDrawingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDrawingTests.swift; sourceTree = ""; }; D1889533258F7648003B73BE /* KFImageOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageOptions.swift; sourceTree = ""; }; D18B3221251852E100662F63 /* KF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KF.swift; sourceTree = ""; }; D1A1CC99219FAB4B00263AD8 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = ""; }; D1A1CC9E21A0F98600263AD8 /* ImageDataProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataProviderTests.swift; sourceTree = ""; }; D1A37BDD215D34E8009B39B7 /* ImageDrawing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDrawing.swift; sourceTree = ""; }; D1A37BE2215D359F009B39B7 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; D1A37BE7215D365A009B39B7 /* ExtensionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionHelpers.swift; sourceTree = ""; }; D1A37BF1215D3850009B39B7 /* SizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeExtensions.swift; sourceTree = ""; }; D1BA781C2174D07800C69D7B /* CallbackQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallbackQueue.swift; sourceTree = ""; }; D1BFED94222ACC6B009330C8 /* ImageProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessorTests.swift; sourceTree = ""; }; D1C04A3E2A45D20500B3775F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; D1D2C3291C70A3230018F2F9 /* single-frame.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "single-frame.gif"; sourceTree = ""; }; D1DC4B401D60996D00DFDFAA /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = ""; }; D1E564402199C21E0057AAE3 /* StorageExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageExpirationTests.swift; sourceTree = ""; }; D1E56444219B16330057AAE3 /* ImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = SOURCE_ROOT; }; D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D1ED2D3F1AD2D09F00CFC3EB /* KingfisherTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KingfisherTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D1F1F6FE24625EC600910725 /* RetryStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryStrategyTests.swift; sourceTree = ""; }; D1F66CC02CB2CF2D004959F3 /* LivePhotoSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoSourceTests.swift; sourceTree = ""; }; D1F7607523097532000C5269 /* ImageBinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageBinder.swift; sourceTree = ""; }; D1F7607623097532000C5269 /* KFImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KFImage.swift; sourceTree = ""; }; D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedirectHandler.swift; sourceTree = ""; }; D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcherTests.swift; sourceTree = ""; }; E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HasImageComponent+Kingfisher.swift"; sourceTree = ""; }; F39B68C72E33AC2A00404B02 /* NetworkMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMetrics.swift; sourceTree = ""; }; F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageModifierTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ D1ED2D311AD2D09F00CFC3EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; D1ED2D3C1AD2D09F00CFC3EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D1ED2D401AD2D09F00CFC3EB /* Kingfisher.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4B8351C6217066400081EED8 /* Utils */ = { isa = PBXGroup; children = ( 4B8351C7217066580081EED8 /* StubHelpers.swift */, ); path = Utils; sourceTree = ""; }; D10EC22C1C3D62E800A4211C /* Tests */ = { isa = PBXGroup; children = ( D16FE9F423078C63006E67D5 /* Dependency */, D12E0C431C47F23500AC98AD /* KingfisherTests */, ); name = Tests; sourceTree = ""; }; D12AB688215D2A280013BA68 /* Sources */ = { isa = PBXGroup; children = ( D1356CEA2B273AEC009554C8 /* Documentation.docc */, D12AB6A9215D2BB50013BA68 /* Info.plist */, D1C04A3E2A45D20500B3775F /* PrivacyInfo.xcprivacy */, D12AB6B0215D2BB50013BA68 /* General */, D12AB6A2215D2BB50013BA68 /* Image */, D12AB69C215D2BB50013BA68 /* Networking */, D12AB6B5215D2BB50013BA68 /* Cache */, D12AB6BD215D2BB50013BA68 /* Views */, D12AB6AB215D2BB50013BA68 /* Extensions */, D12AB6B9215D2BB50013BA68 /* Utility */, D1F7607423097532000C5269 /* SwiftUI */, ); path = Sources; sourceTree = ""; }; D12AB69C215D2BB50013BA68 /* Networking */ = { isa = PBXGroup; children = ( 00A8E26D2E81B89600ABB84F /* NetworkMonitor.swift */, D12AB69D215D2BB50013BA68 /* RequestModifier.swift */, D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */, D12AB69F215D2BB50013BA68 /* ImageDownloader.swift */, D12F675F2CAC2DB700AB63AB /* ImageDownloader+LivePhoto.swift */, 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */, 4BD821662189FD330084CC21 /* SessionDataTask.swift */, 4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */, F39B68C72E33AC2A00404B02 /* NetworkMetrics.swift */, 4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */, 4B10480C216F157000300C61 /* ImageDataProcessor.swift */, D12AB6A0215D2BB50013BA68 /* ImageModifier.swift */, D12AB6A1215D2BB50013BA68 /* ImagePrefetcher.swift */, D11D9B71245FA6F700C5A0AE /* RetryStrategy.swift */, ); path = Networking; sourceTree = ""; }; D12AB6A2215D2BB50013BA68 /* Image */ = { isa = PBXGroup; children = ( D12AB6A3215D2BB50013BA68 /* Image.swift */, D1A37BDD215D34E8009B39B7 /* ImageDrawing.swift */, D1A37BE2215D359F009B39B7 /* ImageFormat.swift */, D12AB6A4215D2BB50013BA68 /* ImageTransition.swift */, D12AB6A5215D2BB50013BA68 /* ImageProcessor.swift */, C9286406228584EB00257182 /* ImageProgressive.swift */, D12AB6A6215D2BB50013BA68 /* Filter.swift */, D12AB6A7215D2BB50013BA68 /* Placeholder.swift */, D12AB6A8215D2BB50013BA68 /* GIFAnimatedImage.swift */, 76FB4FD1262D773E006D15F8 /* GraphicsContext.swift */, ); path = Image; sourceTree = ""; }; D12AB6AB215D2BB50013BA68 /* Extensions */ = { isa = PBXGroup; children = ( D12F67652CB022EB00AB63AB /* PHLivePhotoView+Kingfisher.swift */, D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */, D12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */, D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */, E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */, D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */, 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */, ); path = Extensions; sourceTree = ""; }; D12AB6B0215D2BB50013BA68 /* General */ = { isa = PBXGroup; children = ( D1A1CC98219FAB3500263AD8 /* ImageSource */, D12AB6B1215D2BB50013BA68 /* Kingfisher.swift */, D18B3221251852E100662F63 /* KF.swift */, D1132C9625919F69003E528D /* KFOptionsSetter.swift */, D12AB6B2215D2BB50013BA68 /* KingfisherError.swift */, D12AB6B3215D2BB50013BA68 /* KingfisherManager.swift */, D12F67612CAC32B800AB63AB /* KingfisherManager+LivePhoto.swift */, D12AB6B4215D2BB50013BA68 /* KingfisherOptionsInfo.swift */, ); path = General; sourceTree = ""; }; D12AB6B5215D2BB50013BA68 /* Cache */ = { isa = PBXGroup; children = ( D12AB6B6215D2BB50013BA68 /* ImageCache.swift */, D12AB6B7215D2BB50013BA68 /* CacheSerializer.swift */, D12AB6B8215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift */, 4B46CC63217449E000D90C4A /* Storage.swift */, 4B46CC5E217449C600D90C4A /* MemoryStorage.swift */, 4B46CC6821744AC500D90C4A /* DiskStorage.swift */, ); path = Cache; sourceTree = ""; }; D12AB6B9215D2BB50013BA68 /* Utility */ = { isa = PBXGroup; children = ( D13646732165A1A100A33652 /* Result.swift */, D12AB6BB215D2BB50013BA68 /* Box.swift */, D1A37BE7215D365A009B39B7 /* ExtensionHelpers.swift */, D1A37BF1215D3850009B39B7 /* SizeExtensions.swift */, D1839844216E333E003927D3 /* Delegate.swift */, 4B8351CB217084660081EED8 /* Runtime.swift */, D1BA781C2174D07800C69D7B /* CallbackQueue.swift */, 388F37372B4D9CDB0089705C /* DisplayLink.swift */, 3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */, ); path = Utility; sourceTree = ""; }; D12AB6BD215D2BB50013BA68 /* Views */ = { isa = PBXGroup; children = ( D12AB6BE215D2BB50013BA68 /* Indicator.swift */, D12AB6BF215D2BB50013BA68 /* AnimatedImageView.swift */, ); path = Views; sourceTree = ""; }; D12E0C431C47F23500AC98AD /* KingfisherTests */ = { isa = PBXGroup; children = ( 38D5D3AB2C5C77F200BF1D01 /* PixelFormats */, 4B8351C6217066400081EED8 /* Utils */, D12E0C491C47F23500AC98AD /* Info.plist */, D12E0C441C47F23500AC98AD /* dancing-banana.gif */, D1D2C3291C70A3230018F2F9 /* single-frame.gif */, D12E0C451C47F23500AC98AD /* ImageCacheTests.swift */, D1DC4B401D60996D00DFDFAA /* StringExtensionTests.swift */, D12E0C461C47F23500AC98AD /* ImageDownloaderTests.swift */, D12E0C471C47F23500AC98AD /* ImageExtensionTests.swift */, 38D5D3A42C5C757E00BF1D01 /* PixelFormatDecodingTests.swift */, D186696C21834261002B502E /* ImageDrawingTests.swift */, D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */, D12E0C481C47F23500AC98AD /* ImageViewExtensionTests.swift */, D12E0C4A1C47F23500AC98AD /* KingfisherManagerTests.swift */, D12E0C4B1C47F23500AC98AD /* KingfisherOptionsInfoTests.swift */, D12E0C4C1C47F23500AC98AD /* KingfisherTestHelper.swift */, F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */, D12E0C4D1C47F23500AC98AD /* KingfisherTests-Bridging-Header.h */, D12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */, 185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */, 4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */, 4BCFF7A9219932390055AAC4 /* DiskStorageTests.swift */, D1E564402199C21E0057AAE3 /* StorageExpirationTests.swift */, D1A1CC9E21A0F98600263AD8 /* ImageDataProviderTests.swift */, D1F66CC02CB2CF2D004959F3 /* LivePhotoSourceTests.swift */, D1BFED94222ACC6B009330C8 /* ImageProcessorTests.swift */, 4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */, D1F1F6FE24625EC600910725 /* RetryStrategyTests.swift */, ); name = KingfisherTests; path = Tests/KingfisherTests; sourceTree = ""; }; 38D5D3AB2C5C77F200BF1D01 /* PixelFormats */ = { isa = PBXGroup; children = ( 38D5D3AC2C5C784700BF1D01 /* gradient-8b-srgb-opaque.png */, 38D5D3AD2C5C784700BF1D01 /* gradient-8b-srgb-alpha.png */, 38D5D3AE2C5C784700BF1D01 /* gradient-8b-displayp3-alpha.png */, 38D5D3AF2C5C784700BF1D01 /* gradient-8b-gray.png */, 38D5D3B02C5C784700BF1D01 /* gradient-10b-srgb-opaque.heic */, 38D5D3B12C5C784700BF1D01 /* gradient-10b-srgb-alpha.heic */, 38D5D3B22C5C784700BF1D01 /* gradient-10b-displayp3-alpha.heic */, 38D5D3B32C5C784700BF1D01 /* gradient-16b-srgb-alpha.png */, 38D5D3B42C5C784700BF1D01 /* gradient-16b-gray.png */, ); path = PixelFormats; sourceTree = ""; }; D16FE9F423078C63006E67D5 /* Dependency */ = { isa = PBXGroup; children = ( D16FE9F523078C63006E67D5 /* Nocilla */, ); name = Dependency; path = Tests/Dependency; sourceTree = ""; }; D16FE9F523078C63006E67D5 /* Nocilla */ = { isa = PBXGroup; children = ( D16FE9F623078C63006E67D5 /* LICENSE */, D16FE9F723078C63006E67D5 /* README.md */, D16FE9F823078C63006E67D5 /* Nocilla */, ); path = Nocilla; sourceTree = ""; }; D16FE9F823078C63006E67D5 /* Nocilla */ = { isa = PBXGroup; children = ( D16FE9F923078C63006E67D5 /* Stubs */, D16FE9FE23078C63006E67D5 /* LSNocilla.m */, D16FE9FF23078C63006E67D5 /* Diff */, D16FEA0223078C63006E67D5 /* Nocilla.h */, D16FEA0323078C63006E67D5 /* Hooks */, D16FEA1923078C63006E67D5 /* Model */, D16FEA1D23078C63006E67D5 /* LSNocilla.h */, D16FEA1E23078C63006E67D5 /* Matchers */, D16FEA2E23078C63006E67D5 /* Categories */, D16FEA3323078C63006E67D5 /* DSL */, ); path = Nocilla; sourceTree = ""; }; D16FE9F923078C63006E67D5 /* Stubs */ = { isa = PBXGroup; children = ( D16FE9FA23078C63006E67D5 /* LSStubRequest.m */, D16FE9FB23078C63006E67D5 /* LSStubResponse.m */, D16FE9FC23078C63006E67D5 /* LSStubResponse.h */, D16FE9FD23078C63006E67D5 /* LSStubRequest.h */, ); path = Stubs; sourceTree = ""; }; D16FE9FF23078C63006E67D5 /* Diff */ = { isa = PBXGroup; children = ( D16FEA0023078C63006E67D5 /* LSHTTPRequestDiff.m */, D16FEA0123078C63006E67D5 /* LSHTTPRequestDiff.h */, ); path = Diff; sourceTree = ""; }; D16FEA0323078C63006E67D5 /* Hooks */ = { isa = PBXGroup; children = ( D16FEA0423078C63006E67D5 /* LSHTTPClientHook.m */, D16FEA0523078C63006E67D5 /* LSHTTPClientHook.h */, D16FEA0623078C63006E67D5 /* NSURLRequest */, D16FEA0F23078C63006E67D5 /* ASIHTTPRequest */, D16FEA1623078C63006E67D5 /* NSURLSession */, ); path = Hooks; sourceTree = ""; }; D16FEA0623078C63006E67D5 /* NSURLRequest */ = { isa = PBXGroup; children = ( D16FEA0723078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m */, D16FEA0823078C63006E67D5 /* NSURLRequest+DSL.h */, D16FEA0923078C63006E67D5 /* LSNSURLHook.h */, D16FEA0A23078C63006E67D5 /* LSHTTPStubURLProtocol.h */, D16FEA0B23078C63006E67D5 /* NSURLRequest+LSHTTPRequest.h */, D16FEA0C23078C63006E67D5 /* LSNSURLHook.m */, D16FEA0D23078C63006E67D5 /* NSURLRequest+DSL.m */, D16FEA0E23078C63006E67D5 /* LSHTTPStubURLProtocol.m */, ); path = NSURLRequest; sourceTree = ""; }; D16FEA0F23078C63006E67D5 /* ASIHTTPRequest */ = { isa = PBXGroup; children = ( D16FEA1023078C63006E67D5 /* LSASIHTTPRequestHook.h */, D16FEA1123078C63006E67D5 /* ASIHTTPRequestStub.m */, D16FEA1223078C63006E67D5 /* LSASIHTTPRequestAdapter.h */, D16FEA1323078C63006E67D5 /* LSASIHTTPRequestHook.m */, D16FEA1423078C63006E67D5 /* LSASIHTTPRequestAdapter.m */, D16FEA1523078C63006E67D5 /* ASIHTTPRequestStub.h */, ); path = ASIHTTPRequest; sourceTree = ""; }; D16FEA1623078C63006E67D5 /* NSURLSession */ = { isa = PBXGroup; children = ( D16FEA1723078C63006E67D5 /* LSNSURLSessionHook.h */, D16FEA1823078C63006E67D5 /* LSNSURLSessionHook.m */, ); path = NSURLSession; sourceTree = ""; }; D16FEA1923078C63006E67D5 /* Model */ = { isa = PBXGroup; children = ( D16FEA1A23078C63006E67D5 /* LSHTTPBody.h */, D16FEA1B23078C63006E67D5 /* LSHTTPRequest.h */, D16FEA1C23078C63006E67D5 /* LSHTTPResponse.h */, ); path = Model; sourceTree = ""; }; D16FEA1E23078C63006E67D5 /* Matchers */ = { isa = PBXGroup; children = ( D16FEA1F23078C63006E67D5 /* LSMatcheable.h */, D16FEA2023078C63006E67D5 /* LSMatcher.h */, D16FEA2123078C63006E67D5 /* LSStringMatcher.h */, D16FEA2223078C63006E67D5 /* NSRegularExpression+Matcheable.m */, D16FEA2323078C63006E67D5 /* LSRegexMatcher.h */, D16FEA2423078C63006E67D5 /* NSString+Matcheable.m */, D16FEA2523078C63006E67D5 /* NSData+Matcheable.m */, D16FEA2623078C63006E67D5 /* LSDataMatcher.m */, D16FEA2723078C63006E67D5 /* LSMatcher.m */, D16FEA2823078C63006E67D5 /* LSRegexMatcher.m */, D16FEA2923078C63006E67D5 /* NSRegularExpression+Matcheable.h */, D16FEA2A23078C63006E67D5 /* LSStringMatcher.m */, D16FEA2B23078C63006E67D5 /* NSString+Matcheable.h */, D16FEA2C23078C63006E67D5 /* LSDataMatcher.h */, D16FEA2D23078C63006E67D5 /* NSData+Matcheable.h */, ); path = Matchers; sourceTree = ""; }; D16FEA2E23078C63006E67D5 /* Categories */ = { isa = PBXGroup; children = ( D16FEA2F23078C63006E67D5 /* NSData+Nocilla.h */, D16FEA3023078C63006E67D5 /* NSString+Nocilla.m */, D16FEA3123078C63006E67D5 /* NSData+Nocilla.m */, D16FEA3223078C63006E67D5 /* NSString+Nocilla.h */, ); path = Categories; sourceTree = ""; }; D16FEA3323078C63006E67D5 /* DSL */ = { isa = PBXGroup; children = ( D16FEA3423078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m */, D16FEA3523078C63006E67D5 /* LSStubRequestDSL.h */, D16FEA3623078C63006E67D5 /* LSStubResponseDSL.m */, D16FEA3723078C63006E67D5 /* LSStubRequestDSL.m */, D16FEA3823078C63006E67D5 /* LSHTTPRequestDSLRepresentation.h */, D16FEA3923078C63006E67D5 /* LSStubResponseDSL.h */, ); path = DSL; sourceTree = ""; }; D1A1CC98219FAB3500263AD8 /* ImageSource */ = { isa = PBXGroup; children = ( D1A1CC99219FAB4B00263AD8 /* Source.swift */, D12F67632CAC330600AB63AB /* LivePhotoSource.swift */, D12AB69E215D2BB50013BA68 /* Resource.swift */, D1E56444219B16330057AAE3 /* ImageDataProvider.swift */, D16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */, 078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */, 0784D09A2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift */, ); path = ImageSource; sourceTree = ""; }; D1ED2D021AD2CFA600CFC3EB = { isa = PBXGroup; children = ( D12AB688215D2A280013BA68 /* Sources */, D10EC22C1C3D62E800A4211C /* Tests */, D1ED2D0C1AD2CFA600CFC3EB /* Products */, EA99D30544BD22799F7A5367 /* Frameworks */, ); sourceTree = ""; }; D1ED2D0C1AD2CFA600CFC3EB /* Products */ = { isa = PBXGroup; children = ( D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */, D1ED2D3F1AD2D09F00CFC3EB /* KingfisherTests.xctest */, ); name = Products; sourceTree = ""; }; D1F7607423097532000C5269 /* SwiftUI */ = { isa = PBXGroup; children = ( D1F7607523097532000C5269 /* ImageBinder.swift */, 4B88CEB32646D0BF009EBB41 /* ImageContext.swift */, D1F7607623097532000C5269 /* KFImage.swift */, 07292244263B02F00089E810 /* KFAnimatedImage.swift */, 4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */, D1889533258F7648003B73BE /* KFImageOptions.swift */, 4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */, ); path = SwiftUI; sourceTree = ""; }; EA99D30544BD22799F7A5367 /* Frameworks */ = { isa = PBXGroup; children = ( C959EEE7228940FE00467A10 /* QuartzCore.framework */, 4B164ACE1B8D554200768EC6 /* CFNetwork.framework */, 4B3E714D1B01FEB200F5AAED /* WatchKit.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ D1ED2D321AD2D09F00CFC3EB /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ D1ED2D341AD2D09F00CFC3EB /* Kingfisher */ = { isa = PBXNativeTarget; buildConfigurationList = D1ED2D4E1AD2D09F00CFC3EB /* Build configuration list for PBXNativeTarget "Kingfisher" */; buildPhases = ( D1ED2D301AD2D09F00CFC3EB /* Sources */, D1D550D42AEB9E7300AAD79D /* CopyFiles */, D1ED2D311AD2D09F00CFC3EB /* Frameworks */, D1ED2D321AD2D09F00CFC3EB /* Headers */, ); buildRules = ( ); dependencies = ( ); name = Kingfisher; productName = Kingfisher; productReference = D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */; productType = "com.apple.product-type.framework"; }; D1ED2D3E1AD2D09F00CFC3EB /* KingfisherTests */ = { isa = PBXNativeTarget; buildConfigurationList = D1ED2D521AD2D09F00CFC3EB /* Build configuration list for PBXNativeTarget "KingfisherTests" */; buildPhases = ( D1ED2D3B1AD2D09F00CFC3EB /* Sources */, D1ED2D3C1AD2D09F00CFC3EB /* Frameworks */, D1ED2D3D1AD2D09F00CFC3EB /* Resources */, ); buildRules = ( ); dependencies = ( D1ED2D421AD2D09F00CFC3EB /* PBXTargetDependency */, ); name = KingfisherTests; productName = KingfisherTests; productReference = D1ED2D3F1AD2D09F00CFC3EB /* KingfisherTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ D1ED2D031AD2CFA600CFC3EB /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0720; LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Wei Wang"; TargetAttributes = { D1ED2D341AD2D09F00CFC3EB = { CreatedOnToolsVersion = 6.2; LastSwiftMigration = 1020; ProvisioningStyle = Manual; }; D1ED2D3E1AD2D09F00CFC3EB = { CreatedOnToolsVersion = 6.2; LastSwiftMigration = 1020; }; }; }; buildConfigurationList = D1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject "Kingfisher" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = D1ED2D021AD2CFA600CFC3EB; productRefGroup = D1ED2D0C1AD2CFA600CFC3EB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( D1ED2D341AD2D09F00CFC3EB /* Kingfisher */, D1ED2D3E1AD2D09F00CFC3EB /* KingfisherTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ D1ED2D3D1AD2D09F00CFC3EB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( D16FEA3A23078C63006E67D5 /* LICENSE in Resources */, D1D2C32A1C70A3230018F2F9 /* single-frame.gif in Resources */, D16FEA3B23078C63006E67D5 /* README.md in Resources */, D12E0C4F1C47F23500AC98AD /* dancing-banana.gif in Resources */, 38D5D3C12C5C7A1800BF1D01 /* gradient-8b-srgb-opaque.png in Resources */, 38D5D3C22C5C7A1800BF1D01 /* gradient-8b-srgb-alpha.png in Resources */, 38D5D3C32C5C7A1800BF1D01 /* gradient-8b-displayp3-alpha.png in Resources */, 38D5D3C42C5C7A1800BF1D01 /* gradient-8b-gray.png in Resources */, 38D5D3C52C5C7A1800BF1D01 /* gradient-10b-srgb-opaque.heic in Resources */, 38D5D3C62C5C7A1800BF1D01 /* gradient-10b-srgb-alpha.heic in Resources */, 38D5D3C72C5C7A1800BF1D01 /* gradient-10b-displayp3-alpha.heic in Resources */, 38D5D3C82C5C7A1800BF1D01 /* gradient-16b-srgb-alpha.png in Resources */, 38D5D3C92C5C7A1800BF1D01 /* gradient-16b-gray.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ D1ED2D301AD2D09F00CFC3EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D12AB6CC215D2BB50013BA68 /* ImageModifier.swift in Sources */, D12AB718215D2BB50013BA68 /* CacheSerializer.swift in Sources */, D1E56445219B16330057AAE3 /* ImageDataProvider.swift in Sources */, 4B88CEB22646C653009EBB41 /* KFImageRenderer.swift in Sources */, D12AB730215D2BB50013BA68 /* AnimatedImageView.swift in Sources */, 4B46CC64217449E000D90C4A /* Storage.swift in Sources */, D12AB6E4215D2BB50013BA68 /* Placeholder.swift in Sources */, 4B46CC6921744AC500D90C4A /* DiskStorage.swift in Sources */, 4B46CC5F217449C600D90C4A /* MemoryStorage.swift in Sources */, 4B88CEB42646D0BF009EBB41 /* ImageContext.swift in Sources */, D16CC3D624E02E9500F1A515 /* AVAssetImageDataProvider.swift in Sources */, D1839845216E333E003927D3 /* Delegate.swift in Sources */, D12AB6D8215D2BB50013BA68 /* ImageTransition.swift in Sources */, D12F67642CAC330A00AB63AB /* LivePhotoSource.swift in Sources */, D1A37BE8215D365A009B39B7 /* ExtensionHelpers.swift in Sources */, C9286407228584EB00257182 /* ImageProgressive.swift in Sources */, 0784D09B2F17E4D100EB2A07 /* PhotosPickerItemImageDataProvider.swift in Sources */, D12AB6DC215D2BB50013BA68 /* ImageProcessor.swift in Sources */, D12AB6D4215D2BB50013BA68 /* Image.swift in Sources */, D1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */, D12F67622CAC32BF00AB63AB /* KingfisherManager+LivePhoto.swift in Sources */, 4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */, E9E3ED8B2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift in Sources */, D1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */, D18B3222251852E100662F63 /* KF.swift in Sources */, D12AB704215D2BB50013BA68 /* Kingfisher.swift in Sources */, 00A8E26E2E81B89600ABB84F /* NetworkMonitor.swift in Sources */, D1AEB09725890DEA008556DF /* KFImage.swift in Sources */, F39B68C82E33AC2A00404B02 /* NetworkMetrics.swift in Sources */, D1BA781D2174D07800C69D7B /* CallbackQueue.swift in Sources */, D12AB71C215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift in Sources */, D1A37BF2215D3850009B39B7 /* SizeExtensions.swift in Sources */, D12AB70C215D2BB50013BA68 /* KingfisherManager.swift in Sources */, 4B8351CC217084660081EED8 /* Runtime.swift in Sources */, D12AB6C0215D2BB50013BA68 /* RequestModifier.swift in Sources */, 4B10480D216F157000300C61 /* ImageDataProcessor.swift in Sources */, D12AB72C215D2BB50013BA68 /* Indicator.swift in Sources */, D12AB6C8215D2BB50013BA68 /* ImageDownloader.swift in Sources */, D11D9B72245FA6F700C5A0AE /* RetryStrategy.swift in Sources */, D1A37BE3215D359F009B39B7 /* ImageFormat.swift in Sources */, D12EB83C24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift in Sources */, D1889534258F7649003B73BE /* KFImageOptions.swift in Sources */, D12AB714215D2BB50013BA68 /* ImageCache.swift in Sources */, 4B88CEB02646C056009EBB41 /* KFImageProtocol.swift in Sources */, D12AB6D0215D2BB50013BA68 /* ImagePrefetcher.swift in Sources */, 078DCB4F2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift in Sources */, 388F37382B4D9CDB0089705C /* DisplayLink.swift in Sources */, D12AB6F4215D2BB50013BA68 /* ImageView+Kingfisher.swift in Sources */, D12AB6FC215D2BB50013BA68 /* UIButton+Kingfisher.swift in Sources */, D12F67602CAC2DBF00AB63AB /* ImageDownloader+LivePhoto.swift in Sources */, D12AB6E8215D2BB50013BA68 /* GIFAnimatedImage.swift in Sources */, 22FDCE0E2700078B0044D11E /* CPListItem+Kingfisher.swift in Sources */, D13646742165A1A100A33652 /* Result.swift in Sources */, D1A1CC9A219FAB4B00263AD8 /* Source.swift in Sources */, 4BD821622189FC0C0084CC21 /* SessionDelegate.swift in Sources */, D12AB6E0215D2BB50013BA68 /* Filter.swift in Sources */, 4BE688F722FD513100B11168 /* NSButton+Kingfisher.swift in Sources */, D12AB6C4215D2BB50013BA68 /* Resource.swift in Sources */, 76FB4FD2262D773E006D15F8 /* GraphicsContext.swift in Sources */, D8FCF6A821C5A0E500F9ABC0 /* RedirectHandler.swift in Sources */, 07292245263B02F00089E810 /* KFAnimatedImage.swift in Sources */, D1A37BDE215D34E8009B39B7 /* ImageDrawing.swift in Sources */, 4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */, D12AB708215D2BB50013BA68 /* KingfisherError.swift in Sources */, D12AB724215D2BB50013BA68 /* Box.swift in Sources */, 4B8E291C216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */, 3ADE9AF92A73CD69009A86CA /* String+SHA256.swift in Sources */, D12F67662CB022FC00AB63AB /* PHLivePhotoView+Kingfisher.swift in Sources */, D12AB710215D2BB50013BA68 /* KingfisherOptionsInfo.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; D1ED2D3B1AD2D09F00CFC3EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D12E0C571C47F23500AC98AD /* KingfisherTestHelper.swift in Sources */, D16FEA4923078C63006E67D5 /* NSRegularExpression+Matcheable.m in Sources */, D12E0C581C47F23500AC98AD /* UIButtonExtensionTests.swift in Sources */, D1E564412199C21E0057AAE3 /* StorageExpirationTests.swift in Sources */, D16FEA4123078C63006E67D5 /* NSURLRequest+LSHTTPRequest.m in Sources */, 4BCFF7A621990DB70055AAC4 /* MemoryStorageTests.swift in Sources */, D16FEA4623078C63006E67D5 /* LSASIHTTPRequestHook.m in Sources */, D12E0C561C47F23500AC98AD /* KingfisherOptionsInfoTests.swift in Sources */, D9638BA61C7DC71F0046523D /* ImagePrefetcherTests.swift in Sources */, D12E0C551C47F23500AC98AD /* KingfisherManagerTests.swift in Sources */, D16FEA4523078C63006E67D5 /* ASIHTTPRequestStub.m in Sources */, D16FEA4423078C63006E67D5 /* LSHTTPStubURLProtocol.m in Sources */, D12E0C511C47F23500AC98AD /* ImageDownloaderTests.swift in Sources */, D16FEA4A23078C63006E67D5 /* NSString+Matcheable.m in Sources */, D16FEA4723078C63006E67D5 /* LSASIHTTPRequestAdapter.m in Sources */, D16FEA4F23078C63006E67D5 /* LSStringMatcher.m in Sources */, D16FEA3C23078C63006E67D5 /* LSStubRequest.m in Sources */, D12E0C521C47F23500AC98AD /* ImageExtensionTests.swift in Sources */, D16FEA5123078C63006E67D5 /* NSData+Nocilla.m in Sources */, D16FEA5523079707006E67D5 /* NSButtonExtensionTests.swift in Sources */, D16FEA5023078C63006E67D5 /* NSString+Nocilla.m in Sources */, D16FEA4E23078C63006E67D5 /* LSRegexMatcher.m in Sources */, F72CE9CE1FCF17ED00CC522A /* ImageModifierTests.swift in Sources */, D1F66CC12CB2CF2E004959F3 /* LivePhotoSourceTests.swift in Sources */, D12E0C531C47F23500AC98AD /* ImageViewExtensionTests.swift in Sources */, D16FEA4023078C63006E67D5 /* LSHTTPClientHook.m in Sources */, D16FEA3F23078C63006E67D5 /* LSHTTPRequestDiff.m in Sources */, D186696D21834261002B502E /* ImageDrawingTests.swift in Sources */, 38D5D3A32C5C757E00BF1D01 /* PixelFormatDecodingTests.swift in Sources */, D16FEA4323078C63006E67D5 /* NSURLRequest+DSL.m in Sources */, D16FEA5323078C63006E67D5 /* LSStubResponseDSL.m in Sources */, D16FEA4C23078C63006E67D5 /* LSDataMatcher.m in Sources */, 4BCFF7AA219932390055AAC4 /* DiskStorageTests.swift in Sources */, D16FEA5423078C63006E67D5 /* LSStubRequestDSL.m in Sources */, D1A1CC9F21A0F98600263AD8 /* ImageDataProviderTests.swift in Sources */, D16FEA4823078C63006E67D5 /* LSNSURLSessionHook.m in Sources */, D16FEA3E23078C63006E67D5 /* LSNocilla.m in Sources */, D16FEA4D23078C63006E67D5 /* LSMatcher.m in Sources */, 4B8351C8217066580081EED8 /* StubHelpers.swift in Sources */, D1F1F6FF24625EC600910725 /* RetryStrategyTests.swift in Sources */, D1DC4B411D60996D00DFDFAA /* StringExtensionTests.swift in Sources */, D1BFED95222ACC6B009330C8 /* ImageProcessorTests.swift in Sources */, D16FEA5223078C63006E67D5 /* LSHTTPRequestDSLRepresentation.m in Sources */, D16FEA4223078C63006E67D5 /* LSNSURLHook.m in Sources */, D16FEA4B23078C63006E67D5 /* NSData+Matcheable.m in Sources */, D16FEA3D23078C63006E67D5 /* LSStubResponse.m in Sources */, D12E0C501C47F23500AC98AD /* ImageCacheTests.swift in Sources */, 4BA3BF1E228BCDD100909201 /* DataReceivingSideEffectTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ D1ED2D421AD2D09F00CFC3EB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D1ED2D341AD2D09F00CFC3EB /* Kingfisher */; targetProxy = D1ED2D411AD2D09F00CFC3EB /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ D1ED2D281AD2CFA600CFC3EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_DYLIB_INSTALL_NAME = "@rpath"; MACOSX_DEPLOYMENT_TARGET = 10.15; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_PACKAGE_TYPE = BNDL; SDKROOT = ""; SUPPORTED_PLATFORMS = "watchsimulator iphonesimulator appletvsimulator watchos appletvos iphoneos macosx"; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 13.0; WATCHOS_DEPLOYMENT_TARGET = 6.0; XROS_DEPLOYMENT_TARGET = 1.0; _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES; }; name = Debug; }; D1ED2D291AD2CFA600CFC3EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_DYLIB_INSTALL_NAME = "@rpath"; MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_PACKAGE_TYPE = BNDL; SDKROOT = ""; SUPPORTED_PLATFORMS = "watchsimulator iphonesimulator appletvsimulator watchos appletvos iphoneos macosx"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 13.0; WATCHOS_DEPLOYMENT_TARGET = 6.0; XROS_DEPLOYMENT_TARGET = 1.0; _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES; }; name = Release; }; D1ED2D4F1AD2D09F00CFC3EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_ASSIGN_ENUM = YES; CLANG_WARN_CXX0X_EXTENSIONS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE; CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; CURRENT_PROJECT_VERSION = 3260; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 3260; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; GCC_WARN_SIGN_COMPARE = YES; GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; INFOPLIST_FILE = Sources/Info.plist; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=150"; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_PACKAGE_TYPE = FMWK; PRODUCT_NAME = Kingfisher; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; TARGETED_DEVICE_FAMILY = "1,2,3,4,7"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; D1ED2D501AD2D09F00CFC3EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_ASSIGN_ENUM = YES; CLANG_WARN_CXX0X_EXTENSIONS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE; CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; CURRENT_PROJECT_VERSION = 3260; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 3260; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; GCC_WARN_SIGN_COMPARE = YES; GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; INFOPLIST_FILE = Sources/Info.plist; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_PACKAGE_TYPE = FMWK; PRODUCT_NAME = Kingfisher; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_STRICT_CONCURRENCY = complete; TARGETED_DEVICE_FAMILY = "1,2,3,4,7"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; D1ED2D531AD2D09F00CFC3EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEAD_CODE_STRIPPING = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = Tests/KingfisherTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Tests/KingfisherTests/KingfisherTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2,3,4,7"; }; name = Debug; }; D1ED2D541AD2D09F00CFC3EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = Tests/KingfisherTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "Tests/KingfisherTests/KingfisherTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2,3,4,7"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ D1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject "Kingfisher" */ = { isa = XCConfigurationList; buildConfigurations = ( D1ED2D281AD2CFA600CFC3EB /* Debug */, D1ED2D291AD2CFA600CFC3EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D1ED2D4E1AD2D09F00CFC3EB /* Build configuration list for PBXNativeTarget "Kingfisher" */ = { isa = XCConfigurationList; buildConfigurations = ( D1ED2D4F1AD2D09F00CFC3EB /* Debug */, D1ED2D501AD2D09F00CFC3EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D1ED2D521AD2D09F00CFC3EB /* Build configuration list for PBXNativeTarget "KingfisherTests" */ = { isa = XCConfigurationList; buildConfigurations = ( D1ED2D531AD2D09F00CFC3EB /* Debug */, D1ED2D541AD2D09F00CFC3EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = D1ED2D031AD2CFA600CFC3EB /* Project object */; } ================================================ FILE: Kingfisher.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Kingfisher.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Kingfisher.xcodeproj/xcshareddata/xcbaselines/D1ED2D3E1AD2D09F00CFC3EB.xcbaseline/74237B0B-7981-4A24-B6C4-95F4A5E7727F.plist ================================================ classNames ImageCacheTests testRetrivingImagePerformance() com.apple.XCTPerformanceMetric_WallClockTime baselineAverage 0.27 baselineIntegrationDisplayName Local Baseline ================================================ FILE: Kingfisher.xcodeproj/xcshareddata/xcbaselines/D1ED2D3E1AD2D09F00CFC3EB.xcbaseline/Info.plist ================================================ runDestinationsByUUID 74237B0B-7981-4A24-B6C4-95F4A5E7727F localComputer busSpeedInMHz 100 cpuCount 1 cpuKind Intel Core i7 cpuSpeedInMHz 3500 logicalCPUCoresPerPackage 8 modelCode iMac14,2 physicalCPUCoresPerPackage 4 platformIdentifier com.apple.platform.macosx targetArchitecture x86_64 targetDevice modelCode iPhone7,2 platformIdentifier com.apple.platform.iphonesimulator ================================================ FILE: Kingfisher.xcodeproj/xcshareddata/xcschemes/Kingfisher.xcscheme ================================================ ================================================ FILE: Kingfisher.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Kingfisher.xcworkspace/xcshareddata/IDETemplateMacros.plist ================================================ FILEHEADER // ___FILENAME___ // Kingfisher // // Created by ___USERNAME___ on ___DATE___. // // Copyright (c) ___YEAR___ Wei Wang <onevcat@gmail.com> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. ================================================ FILE: Kingfisher.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Kingfisher.xcworkspace/xcshareddata/Kingfisher.xcscmblueprint ================================================ { "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "17FB51EC7B87DC25DD21E28FDFD877B479CFFAC1", "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { }, "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { "EDD4D5CA2765BF1400F1A6D6680AADA3F565AD7A" : 9223372036854775807, "17FB51EC7B87DC25DD21E28FDFD877B479CFFAC1" : 9223372036854775807 }, "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "A97B351B-F1BD-4510-8138-460C0D267EF0", "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { "EDD4D5CA2765BF1400F1A6D6680AADA3F565AD7A" : "Kingfisher\/Kingfisher-TestImages\/", "17FB51EC7B87DC25DD21E28FDFD877B479CFFAC1" : "Kingfisher\/" }, "DVTSourceControlWorkspaceBlueprintNameKey" : "Kingfisher", "DVTSourceControlWorkspaceBlueprintVersion" : 204, "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Kingfisher.xcworkspace", "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ { "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:onevcat\/Kingfisher.git", "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "17FB51EC7B87DC25DD21E28FDFD877B479CFFAC1" }, { "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:onevcat\/Kingfisher-TestImages.git", "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "EDD4D5CA2765BF1400F1A6D6680AADA3F565AD7A" } ] } ================================================ FILE: Kingfisher.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 Wei Wang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.1 import PackageDescription let package = Package( name: "Kingfisher", platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6)], products: [ .library(name: "Kingfisher", targets: ["Kingfisher"]) ], targets: [ .target( name: "Kingfisher", path: "Sources" ) ] ) ================================================ FILE: Package@swift-5.9.swift ================================================ // swift-tools-version:5.9 import PackageDescription let package = Package( name: "Kingfisher", platforms: [ .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1) ], products: [ .library(name: "Kingfisher", targets: ["Kingfisher"]) ], targets: [ .target( name: "Kingfisher", path: "Sources", resources: [.process("PrivacyInfo.xcprivacy")] ) ] ) ================================================ FILE: README-LLM.md ================================================ # Kingfisher Kingfisher 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. ## Quick Start **Core API Entry Points:** - `Sources/General/KingfisherManager.swift` - Central coordinator - `Sources/General/KF.swift` - Builder pattern API (`KF.url()...`) - `Sources/Extensions/ImageView+Kingfisher.swift` - UIKit/AppKit extensions - `Sources/SwiftUI/KFImage.swift` - SwiftUI components **Essential Build Commands:** ```bash # Install dependencies and run all tests bundle install && bundle exec fastlane tests # Build for specific platform swift build # Full release workflow bundle exec fastlane release version:X.X.X ``` ## Documentation **For LLMs and Developers:** - **[Project Overview](docs/project-overview.md)** - What Kingfisher does, core purpose, technology stack, and platform support - **[Architecture](docs/architecture.md)** - System organization, component map, key files, and data flow with specific file references - **[Build System](docs/build-system.md)** - Swift Package Manager and Fastlane workflows, platform setup, and troubleshooting - **[Testing](docs/testing.md)** - Test categories, running tests, and test infrastructure with file locations - **[Development](docs/development.md)** - Code style, implementation patterns, workflows, and common solutions - **[Deployment](docs/deployment.md)** - Package types, platform deployment, release management, and CI/CD - **[File Catalog](docs/files.md)** - Comprehensive file organization with specific file purposes and relationships **Configuration Files:** - `Package.swift` - Swift Package Manager manifest - `Kingfisher.podspec` - CocoaPods specification - `fastlane/Fastfile` - Build automation - `Sources/Documentation.docc/` - DocC documentation **Key Patterns:** - Namespace wrapper (`.kf` property) in `Sources/General/Kingfisher.swift` - Builder pattern API in `Sources/General/KF.swift` - Options system in `Sources/General/KingfisherOptionsInfo.swift` - Protocol-oriented design throughout `Sources/Image/ImageProcessor.swift` ## Requirements - **Swift 5.9+** (Swift 6 strict concurrency ready) - **iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+** - **SwiftUI support**: iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+ ================================================ FILE: README.md ================================================

Kingfisher

Kingfisher 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. ## Features - [x] Asynchronous image downloading and caching. - [x] Loading image from either `URLSession`-based networking or local provided data. - [x] Useful image processors and filters provided. - [x] Multiple-layer hybrid cache for both memory and disk. - [x] Fine control on cache behavior. Customizable expiration date and size limit. - [x] Cancelable downloading and auto-reusing previous downloaded content to improve performance. - [x] Independent components. Use the downloader, caching system, and image processors separately as you need. - [x] Prefetching images and showing them from the cache to boost your app. - [x] Extensions for `UIImageView`, `NSImageView`, `NSButton`, `UIButton`, `NSTextAttachment`, `WKInterfaceImage`, `TVMonogramView` and `CPListItem` to directly set an image from a URL. - [x] Built-in transition animation when setting images. - [x] Customizable placeholder and indicator while loading images. - [x] Extensible image processing and image format easily. - [x] Low Data Mode support. - [x] SwiftUI support. - [x] Swift 6 & Swift Concurrency (strict mode) prepared. - [x] Load & cache for Live Photo. ### Kingfisher 101 The simplest use-case is setting an image to an image view with the `UIImageView` extension: ```swift import Kingfisher let url = URL(string: "https://example.com/image.png") imageView.kf.setImage(with: url) ``` Kingfisher will download the image from `url`, send it to both memory cache and disk cache, and display it in `imageView`. When you set it with the same URL later, the image will be retrieved from the cache and shown immediately. It also works if you use SwiftUI: ```swift var body: some View { KFImage(URL(string: "https://example.com/image.png")!) } ``` ### A More Advanced Example With the powerful options, you can do hard tasks with Kingfisher in a simple way. For example, the code below: 1. Downloads a high-resolution image. 2. Downsamples it to match the image view size. 3. Makes it round cornered with a given radius. 4. Shows a system indicator and a placeholder image while downloading. 5. When prepared, it animates the small thumbnail image with a "fade in" effect. 6. The original large image is also cached to disk for later use, to get rid of downloading it again in a detail view. 7. A console log is printed when the task finishes, either for success or failure. ```swift let url = URL(string: "https://example.com/high_resolution_image.png") let processor = DownsamplingImageProcessor(size: imageView.bounds.size) |> RoundCornerImageProcessor(cornerRadius: 20) imageView.kf.indicatorType = .activity imageView.kf.setImage( with: url, placeholder: UIImage(named: "placeholderImage"), options: [ .processor(processor), .scaleFactor(UIScreen.main.scale), .transition(.fade(1)), .cacheOriginalImage ]) { result in switch result { case .success(let value): print("Task done for: \(value.source.url?.absoluteString ?? "")") case .failure(let error): print("Job failed: \(error.localizedDescription)") } } ``` It is a common situation I can meet in my daily work. Think about how many lines you need to write without Kingfisher! ### Method Chaining If you are not a fan of the `kf` extension, you can also prefer to use the `KF` builder and chained the method invocations. The code below is doing the same thing: ```swift // Use `kf` extension imageView.kf.setImage( with: url, placeholder: placeholderImage, options: [ .processor(processor), .loadDiskFileSynchronously, .cacheOriginalImage, .transition(.fade(0.25)), .lowDataMode(.network(lowResolutionURL)) ], progressBlock: { receivedSize, totalSize in // Progress updated }, completionHandler: { result in // Done } ) // Use `KF` builder KF.url(url) .placeholder(placeholderImage) .setProcessor(processor) .loadDiskFileSynchronously() .cacheMemoryOnly() .fade(duration: 0.25) .lowDataModeSource(.network(lowResolutionURL)) .onProgress { receivedSize, totalSize in } .onSuccess { result in } .onFailure { error in } .set(to: imageView) ``` And even better, if later you want to switch to SwiftUI, just change the `KF` above to `KFImage`, and you've done: ```swift struct ContentView: View { var body: some View { KFImage.url(url) .placeholder(placeholderImage) .setProcessor(processor) .loadDiskFileSynchronously() .cacheMemoryOnly() .fade(duration: 0.25) .lowDataModeSource(.network(lowResolutionURL)) .onProgress { receivedSize, totalSize in } .onSuccess { result in } .onFailure { error in } } } ``` ## Requirements ### Kingfisher 8.0 - (UIKit/AppKit) iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+ - (SwiftUI) iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+ - Swift 5.9+ ### Kingfisher 7.0 - (UIKit/AppKit) iOS 12.0+ / macOS 10.14+ / tvOS 12.0+ / watchOS 5.0+ / visionOS 1.0+ - (SwiftUI) iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+ - Swift 5.0+ ### Installation Refer to one of the following tutorials to install and use the framework: - [UIKit Tutorial](https://swiftpackageindex.com/onevcat/kingfisher/master/tutorials/kingfisher/gettingstarteduikit) - [SwiftUI Tutorial](https://swiftpackageindex.com/onevcat/kingfisher/master/tutorials/kingfisher/gettingstartedswiftui) Alternatively, you can follow either of the methods below. #### Swift Package Manager - File > Swift Packages > Add Package Dependency - Add `https://github.com/onevcat/Kingfisher.git` - Select "Up to Next Major" with "8.0.0" #### CocoaPods ```ruby source 'https://github.com/CocoaPods/Specs.git' platform :ios, '13.0' use_frameworks! target 'MyApp' do pod 'Kingfisher', '~> 8.0' end ``` #### Pre-built Framework 1. Open the release page, download the latest version of Kingfisher from the assets section. 2. Drag the `Kingfisher.xcframework` into your project and add it to the target (usually the app target). 3. Select your target, in the "General" Tab, find the "Frameworks, Libraries, and Embedded Content" section, set the `Embed Without Signing` to Kingfisher. ## Documentation Check the documentation and tutorials: - [Documentation Home](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher) - [Getting Started](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/gettingstarted) - [UIKit Tutorial](https://swiftpackageindex.com/onevcat/kingfisher/master/tutorials/kingfisher/gettingstarteduikit) - [SwiftUI Tutorial](https://swiftpackageindex.com/onevcat/kingfisher/master/tutorials/kingfisher/gettingstartedswiftui) - [Common Tasks - General](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/commontasks) - [Common Tasks - Cache](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/commontasks_cache) - [Common Tasks - Downloader](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/commontasks_downloader) - [Common tasks - Processor](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/commontasks_processor) ### Migrating - [Kingfisher 8.0 Migration](https://swiftpackageindex.com/onevcat/kingfisher/master/documentation/kingfisher/migration-to-8) - [Kingfisher 7.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-7.0-Migration-Guide) If you are using an even earlier version, see the guides below to know the steps for migrating. ## Other ### Future of Kingfisher I 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. ### Developments and Tests Any 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. :) ### About the logo The 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? ### Contact Follow 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. ## Backers & Sponsors Open-source projects cannot live long without your help. If you find Kingfisher to be useful, please consider supporting this project 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. Become a sponsor through [GitHub Sponsors](https://github.com/sponsors/onevcat). :heart: Special thanks to: [![imgly](https://user-images.githubusercontent.com/1812216/106253726-271ed000-6218-11eb-98e0-c9c681925770.png)](https://img.ly/) [![emergetools](https://github-production-user-asset-6210df.s3.amazonaws.com/1019875/254794187-d44f6f50-993f-42e3-b79c-960f69c4adc1.png)](https://www.emergetools.com) ### License Kingfisher is released under the MIT license. See LICENSE for details. ================================================ FILE: Sources/Cache/CacheSerializer.swift ================================================ // // CacheSerializer.swift // Kingfisher // // Created by Wei Wang on 2016/09/02. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A `CacheSerializer` is used to convert some data to an image object after retrieving it from disk storage, /// and vice versa, to convert an image to a data object for storing it to the disk storage. public protocol CacheSerializer: Sendable { /// Retrieves the serialized data from a provided image and optional original data for caching to disk. /// /// - Parameters: /// - image: The image to be serialized. /// - original: The original data that was just downloaded. /// If the image is retrieved from the cache instead of being downloaded, it will be `nil`. /// - Returns: The data object for storing to disk, or `nil` when no valid data can be serialized. func data(with image: KFCrossPlatformImage, original: Data?) -> Data? /// Retrieves an image from the provided serialized data. /// /// - Parameters: /// - data: The data from which an image should be deserialized. /// - options: The parsed options for deserialization. /// - Returns: A deserialized image, or `nil` when no valid image can be deserialized. func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? /// Indicates whether this serializer prefers to cache the original data in its implementation. /// /// If `true`, during storing phase, the original data is preferred to be stored to the disk if exists. When /// retrieving image from the disk cache, after creating the image from the loaded data, Kingfisher will continue /// to apply the processor to get the final image. /// /// By default, it is `false`, and the actual processed image is assumed to be serialized to and later deserialized /// from the disk. That means the processed version of the image is stored and loaded. var originalDataUsed: Bool { get } } public extension CacheSerializer { var originalDataUsed: Bool { false } } /// Represents a basic and default `CacheSerializer` used in the Kingfisher disk cache system. /// /// It can serialize and deserialize images in PNG, JPEG, and GIF formats. For images other than these formats, a /// normalized ``KingfisherWrapper/pngRepresentation()`` will be used. /// /// When converting an `image` to the data, it will only be converted to the corresponding data type when `original` /// contains valid PNG, JPEG, and GIF format data. If the `original` is provided but not valid, or if `original` is /// `nil`, the input `image` will be encoded as PNG data. /// /// If `original` is `nil` but the input `image` contains embedded GIF data (for example, a cached animated image /// created from GIF data), the serializer will prefer the embedded GIF data and store it as GIF instead of falling /// back to PNG. /// /// > Tip: If you create a new image instance from an animated image in a custom processor, use /// > ``KingfisherWrapper/copyKingfisherState(to:)`` to propagate the embedded animated data to the new image. public struct DefaultCacheSerializer: CacheSerializer { /// The default general cache serializer utilized throughout Kingfisher's caching mechanism. public static let `default` = DefaultCacheSerializer() /// The compression quality used when converting an image to lossy format data (such as JPEG). /// /// Default is 1.0. public var compressionQuality: CGFloat = 1.0 /// Determines whether the original data should be prioritized during image serialization. /// /// If set to `true`, the original input data will be initially inspected and used, unless the data is `nil`. /// In the event of a `nil` data, the serialization process will revert to generating data from the image. /// /// > This value is used as ``CacheSerializer/originalDataUsed-d2v9``. public var preferCacheOriginalData: Bool = false public var originalDataUsed: Bool { preferCacheOriginalData } /// Creates a cache serializer that serializes and deserializes images in PNG, JPEG, and GIF formats. /// /// > Prefer to use the ``DefaultCacheSerializer/default`` value unless you need to specify your own properties. public init() { } public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { let format: ImageFormat = { if let original = original { return original.kf.imageFormat } if let animatedData = image.kf.gifRepresentation(), animatedData.kf.imageFormat == .GIF { return .GIF } return .unknown }() if preferCacheOriginalData { if let original = original { return original } if format == .GIF { return image.kf.gifRepresentation() } } return image.kf.data(format: format, compressionQuality: compressionQuality) } public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) } } ================================================ FILE: Sources/Cache/DiskStorage.swift ================================================ // // DiskStorage.swift // Kingfisher // // Created by Wei Wang on 2018/10/15. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents the concepts related to storage that stores a specific type of value in disk. /// /// This serves as a namespace for memory storage types. A ``DiskStorage/Backend`` with a particular /// ``DiskStorage/Config`` is used to define the storage. /// /// Refer to these composite types for further details. public enum DiskStorage { /// Represents a storage backend for the ``DiskStorage``. /// /// The value is serialized to binary data and stored as a file in the file system under a specified location. /// /// You can configure a ``DiskStorage/Backend`` in its ``DiskStorage/Backend/init(config:)`` by passing a /// ``DiskStorage/Config`` value or by modifying the ``DiskStorage/Backend/config`` property after it has been /// created. The ``DiskStorage/Backend`` will use the file's attributes to keep track of a file for its expiration /// or size limitation. public final class Backend: @unchecked Sendable where T: Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.kingfisher.DiskStorage.Backend.propertyQueue") private var _config: Config /// The configuration used for this disk storage. /// /// It is a value you can set and use to configure the storage as needed. public var config: Config { get { propertyQueue.sync { _config } } set { propertyQueue.sync { _config = newValue } } } /// The final storage URL on disk of the disk storage ``DiskStorage/Backend``, considering the /// ``DiskStorage/Config/name`` and the ``DiskStorage/Config/cachePathBlock``. public let directoryURL: URL let metaChangingQueue: DispatchQueue // A shortcut (which contains false-positive) to improve matching performance. var maybeCached : Set? let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue") // `false` if the storage initialized with an error. // This prevents unexpected forcibly crash when creating storage in the default cache. private var storageReady: Bool = true /// Creates a disk storage with the given ``DiskStorage/Config``. /// /// - Parameter config: The configuration used for this disk storage. /// - Throws: An error if the folder for storage cannot be obtained or created. public convenience init(config: Config) throws { self.init(noThrowConfig: config, creatingDirectory: false) try prepareDirectory() } // If `creatingDirectory` is `false`, the directory preparation will be skipped. // We need to call `prepareDirectory` manually after this returns. init(noThrowConfig config: Config, creatingDirectory: Bool) { var config = config let creation = Creation(config) self.directoryURL = creation.directoryURL // Break any possible retain cycle set by outside. config.cachePathBlock = nil _config = config metaChangingQueue = DispatchQueue(label: creation.cacheName) setupCacheChecking() if creatingDirectory { try? prepareDirectory() } } private func setupCacheChecking() { DispatchQueue.global(qos: .default).async { do { let allFiles = try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path) let maybeCached = Set(allFiles) self.maybeCachedCheckingQueue.async { self.maybeCached = maybeCached } } catch { self.maybeCachedCheckingQueue.async { // Just disable the functionality if we fail to initialize it properly. This will just revert to // the behavior which is to check file existence on disk directly. self.maybeCached = nil } } } } // Creates the storage folder. private func prepareDirectory() throws { let fileManager = config.fileManager let path = directoryURL.path guard !fileManager.fileExists(atPath: path) else { return } do { try fileManager.createDirectory( atPath: path, withIntermediateDirectories: true, attributes: nil) } catch { self.storageReady = false throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error)) } } /// Stores a value in the storage under the specified key and expiration policy. /// /// - Parameters: /// - value: The value to be stored. /// - key: The key to which the `value` will be stored. If there is already a value under the key, the old /// value will be overwritten by the new `value`. /// - expiration: The expiration policy used by this storage action. /// - writeOptions: Data writing options used for the new files. /// - forcedExtension: The file extension, if exists. /// - Throws: An error during converting the value to a data format or during writing it to disk. public func store( value: T, forKey key: String, expiration: StorageExpiration? = nil, writeOptions: Data.WritingOptions = [], forcedExtension: String? = nil ) throws { guard storageReady else { throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) } let expiration = expiration ?? config.expiration // The expiration indicates that already expired, no need to store. guard !expiration.isExpired else { return } let data: Data do { data = try value.toData() } catch { throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error)) } let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension) do { try data.write(to: fileURL, options: writeOptions) } catch { if error.isFolderMissing { // The whole cache folder is deleted. Try to recreate it and write file again. do { try prepareDirectory() try data.write(to: fileURL, options: writeOptions) } catch { throw KingfisherError.cacheError( reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error) ) } } else { throw KingfisherError.cacheError( reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error) ) } } let now = Date() let attributes: [FileAttributeKey : Any] = [ // The last access date. .creationDate: now.fileAttributeDate, // The estimated expiration date. .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate ] do { try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path) } catch { try? config.fileManager.removeItem(at: fileURL) throw KingfisherError.cacheError( reason: .cannotSetCacheFileAttribute( filePath: fileURL.path, attributes: attributes, error: error ) ) } maybeCachedCheckingQueue.async { self.maybeCached?.insert(fileURL.lastPathComponent) } } /// Retrieves a value from the storage. /// - Parameters: /// - key: The cache key of the value. /// - forcedExtension: The file extension, if exists. /// - extendingExpiration: The expiration policy used by this retrieval action. /// - Throws: An error during converting the data to a value or during the operation of disk files. /// - Returns: The value under `key` if it is valid and found in the storage; otherwise, `nil`. public func value( forKey key: String, forcedExtension: String? = nil, extendingExpiration: ExpirationExtending = .cacheTime ) throws -> T? { try value( forKey: key, referenceDate: Date(), actuallyLoad: true, extendingExpiration: extendingExpiration, forcedExtension: forcedExtension ) } func value( forKey key: String, referenceDate: Date, actuallyLoad: Bool, extendingExpiration: ExpirationExtending, forcedExtension: String? ) throws -> T? { guard storageReady else { throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) } let fileManager = config.fileManager let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension) let filePath = fileURL.path let fileMaybeCached = maybeCachedCheckingQueue.sync { return maybeCached?.contains(fileURL.lastPathComponent) ?? true } guard fileMaybeCached else { return nil } guard fileManager.fileExists(atPath: filePath) else { return nil } let meta: FileMeta do { let resourceKeys: Set = [.contentModificationDateKey, .creationDateKey] meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys) } catch { throw KingfisherError.cacheError( reason: .invalidURLResource(error: error, key: key, url: fileURL)) } if meta.expired(referenceDate: referenceDate) { return nil } if !actuallyLoad { return T.empty } do { let data = try Data(contentsOf: fileURL) let obj = try T.fromData(data) metaChangingQueue.async { meta.extendExpiration(with: self.config.fileManager, extendingExpiration: extendingExpiration) } return obj } catch { throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error)) } } /// Determines whether there is valid cached data under a given key. /// /// - Parameters: /// - key: The cache key of the value. /// - forcedExtension: The file extension, if exists. /// - Returns: `true` if there is valid data under the key and file extension; otherwise, `false`. /// /// > This method does not actually load the data from disk, so it is faster than directly loading the cached /// value by checking the nullability of the /// ``DiskStorage/Backend/value(forKey:forcedExtension:extendingExpiration:)`` method. public func isCached(forKey key: String, forcedExtension: String? = nil) -> Bool { return isCached(forKey: key, referenceDate: Date(), forcedExtension: forcedExtension) } /// Determines whether there is valid cached data under a given key and a reference date. /// /// - Parameters: /// - key: The cache key of the value. /// - referenceDate: A reference date to check whether the cache is still valid. /// - forcedExtension: The file extension, if exists. /// /// - Returns: `true` if there is valid data under the key; otherwise, `false`. /// /// If you pass `Date()` as the `referenceDate`, this method is identical to /// ``DiskStorage/Backend/isCached(forKey:forcedExtension:)``. Use the `referenceDate` to determine whether the /// cache is still valid for a future date. public func isCached(forKey key: String, referenceDate: Date, forcedExtension: String? = nil) -> Bool { do { let result = try value( forKey: key, referenceDate: referenceDate, actuallyLoad: false, extendingExpiration: .none, forcedExtension: forcedExtension ) return result != nil } catch { return false } } /// Removes a value from a specified key. /// - Parameters: /// - key: The cache key of the value. /// - forcedExtension: The file extension, if exists. /// - Throws: An error during the removal of the value. public func remove(forKey key: String, forcedExtension: String? = nil) throws { let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension) try removeFile(at: fileURL) } func removeFile(at url: URL) throws { try config.fileManager.removeItem(at: url) } /// Removes all values in this storage. /// - Throws: An error during the removal of the values. public func removeAll() throws { try removeAll(skipCreatingDirectory: false) } func removeAll(skipCreatingDirectory: Bool) throws { try config.fileManager.removeItem(at: directoryURL) if !skipCreatingDirectory { try prepareDirectory() } } /// The URL of the cached file with a given computed `key`. /// - Parameters: /// - key: The final computed key used when caching the image. Please note that usually this is not /// the ``Source/cacheKey`` of an image ``Source``. It is the computed key with the processor identifier /// considered. /// - forcedExtension: The file extension, if exists. /// - Returns: The expected file URL on the disk based on the `key` and the `forcedExtension`. /// /// This method does not guarantee that an image is already cached at the returned URL. It just provides the URL /// where the image should be if it exists in the disk storage, with the given key and file extension. /// public func cacheFileURL(forKey key: String, forcedExtension: String? = nil) -> URL { let fileName = cacheFileName(forKey: key, forcedExtension: forcedExtension) return directoryURL.appendingPathComponent(fileName, isDirectory: false) } func cacheFileName(forKey key: String, forcedExtension: String? = nil) -> String { let baseName = config.usesHashedFileName ? key.kf.sha256 : key if let ext = fileExtension(key: key, forcedExtension: forcedExtension) { return "\(baseName).\(ext)" } return baseName } func fileExtension(key: String, forcedExtension: String?) -> String? { if let ext = forcedExtension ?? config.pathExtension { return ext } if config.usesHashedFileName && config.autoExtAfterHashedFileName { return key.kf.ext } return nil } func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] { let fileManager = config.fileManager guard let directoryEnumerator = fileManager.enumerator( at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else { throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL)) } guard let urls = directoryEnumerator.allObjects as? [URL] else { throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL)) } return urls } /// Removes all expired values from this storage. /// - Throws: A file manager error during the removal of the file. /// - Returns: The URLs for the removed files. public func removeExpiredValues() throws -> [URL] { return try removeExpiredValues(referenceDate: Date()) } func removeExpiredValues(referenceDate: Date) throws -> [URL] { let propertyKeys: [URLResourceKey] = [ .isDirectoryKey, .contentModificationDateKey ] let urls = try allFileURLs(for: propertyKeys) let keys = Set(propertyKeys) let expiredFiles = urls.filter { fileURL in do { let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) if meta.isDirectory { return false } return meta.expired(referenceDate: referenceDate) } catch { return true } } try expiredFiles.forEach { url in try removeFile(at: url) } return expiredFiles } /// Removes all size-exceeded values from this storage. /// - Throws: A file manager error during the removal of the file. /// - Returns: The URLs for the removed files. /// /// This method checks ``DiskStorage/Config/sizeLimit`` and removes cached files in an LRU /// (Least Recently Used) way. public func removeSizeExceededValues() throws -> [URL] { if config.sizeLimit == 0 { return [] } // Back compatible. 0 means no limit. var size = try totalSize() if size < config.sizeLimit { return [] } let propertyKeys: [URLResourceKey] = [ .isDirectoryKey, .creationDateKey, .fileSizeKey ] let keys = Set(propertyKeys) let urls = try allFileURLs(for: propertyKeys) var pendings: [FileMeta] = urls.compactMap { fileURL in guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else { return nil } return meta } // Sort by last access date. Most recent file first. pendings.sort(by: FileMeta.lastAccessDate) var removed: [URL] = [] let target = config.sizeLimit / 2 while size > target, let meta = pendings.popLast() { size -= UInt(meta.fileSize) try removeFile(at: meta.url) removed.append(meta.url) } return removed } /// Gets the total file size of the cache folder in bytes. public func totalSize() throws -> UInt { let propertyKeys: [URLResourceKey] = [.fileSizeKey] let urls = try allFileURLs(for: propertyKeys) let keys = Set(propertyKeys) let totalSize: UInt = urls.reduce(0) { size, fileURL in do { let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) return size + UInt(meta.fileSize) } catch { return size } } return totalSize } } } extension DiskStorage { /// Represents the configuration used in a ``DiskStorage/Backend``. public struct Config: @unchecked Sendable { /// The file size limit on disk of the storage in bytes. /// /// `0` means no limit. public var sizeLimit: UInt /// The `StorageExpiration` used in this disk storage. /// /// The default is `.days(7)`, which means that the disk cache will expire in one week if not accessed anymore. public var expiration: StorageExpiration = .days(7) /// The preferred extension of the cache item. It will be appended to the file name as its extension. /// /// The default is `nil`, which means that the cache file does not contain a file extension. public var pathExtension: String? = nil /// Whether the cache file name will be hashed before storing. /// /// The default is `true`, which means that file name is hashed to protect user information (for example, the /// original download URL which is used as the cache key). public var usesHashedFileName = true /// Whether the image extension will be extracted from the original file name and appended to the hashed file /// name, which will be used as the cache key on disk. /// /// The default is `false`. public var autoExtAfterHashedFileName = false /// A closure that takes in the initial directory path and generates the final disk cache path. /// /// You can use it to fully customize your cache path. public var cachePathBlock: (@Sendable (_ directory: URL, _ cacheName: String) -> URL)! = { (directory, cacheName) in return directory.appendingPathComponent(cacheName, isDirectory: true) } /// The desired name of the disk cache. /// /// This name will be used as a part of the cache folder name by default. public let name: String let fileManager: FileManager let directory: URL? /// Creates a config value based on the given parameters. /// /// - Parameters: /// - name: The name of the cache. It is used as part of the storage folder and to identify the disk storage. /// Two storages with the same `name` would share the same folder on the disk, and this should be prevented. /// - sizeLimit: The size limit in bytes for all existing files in the disk storage. /// - fileManager: The `FileManager` used to manipulate files on the disk. The default is `FileManager.default`. /// - directory: The URL where the disk storage should reside. The storage will use this as the root folder, /// and append a path that is constructed by the input `name`. The default is `nil`, indicating that /// the cache directory under the user domain mask will be used. public init( name: String, sizeLimit: UInt, fileManager: FileManager = .default, directory: URL? = nil) { self.name = name self.fileManager = fileManager self.directory = directory self.sizeLimit = sizeLimit } } } extension DiskStorage { struct FileMeta { let url: URL let lastAccessDate: Date? let estimatedExpirationDate: Date? let isDirectory: Bool let fileSize: Int static func lastAccessDate(lhs: FileMeta, rhs: FileMeta) -> Bool { return lhs.lastAccessDate ?? .distantPast > rhs.lastAccessDate ?? .distantPast } init(fileURL: URL, resourceKeys: Set) throws { let meta = try fileURL.resourceValues(forKeys: resourceKeys) self.init( fileURL: fileURL, lastAccessDate: meta.creationDate, estimatedExpirationDate: meta.contentModificationDate, isDirectory: meta.isDirectory ?? false, fileSize: meta.fileSize ?? 0) } init( fileURL: URL, lastAccessDate: Date?, estimatedExpirationDate: Date?, isDirectory: Bool, fileSize: Int) { self.url = fileURL self.lastAccessDate = lastAccessDate self.estimatedExpirationDate = estimatedExpirationDate self.isDirectory = isDirectory self.fileSize = fileSize } func expired(referenceDate: Date) -> Bool { return estimatedExpirationDate?.isPast(referenceDate: referenceDate) ?? true } func extendExpiration(with fileManager: FileManager, extendingExpiration: ExpirationExtending) { guard let lastAccessDate = lastAccessDate, let lastEstimatedExpiration = estimatedExpirationDate else { return } let attributes: [FileAttributeKey : Any] switch extendingExpiration { case .none: // not extending expiration time here return case .cacheTime: let originalExpiration: StorageExpiration = .seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate)) attributes = [ .creationDate: Date().fileAttributeDate, .modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate ] case .expirationTime(let expirationTime): attributes = [ .creationDate: Date().fileAttributeDate, .modificationDate: expirationTime.estimatedExpirationSinceNow.fileAttributeDate ] } try? fileManager.setAttributes(attributes, ofItemAtPath: url.path) } } } extension DiskStorage { struct Creation { let directoryURL: URL let cacheName: String init(_ config: Config) { let url: URL if let directory = config.directory { url = directory } else { url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] } cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)" directoryURL = config.cachePathBlock(url, cacheName) } } } fileprivate extension Error { var isFolderMissing: Bool { let nsError = self as NSError guard nsError.domain == NSCocoaErrorDomain, nsError.code == 4 else { return false } guard let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError else { return false } guard underlyingError.domain == NSPOSIXErrorDomain, underlyingError.code == 2 else { return false } return true } } ================================================ FILE: Sources/Cache/FormatIndicatedCacheSerializer.swift ================================================ // // RequestModifier.swift // Kingfisher // // Created by Junyu Kuang on 5/28/17. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// The ``FormatIndicatedCacheSerializer`` enables you to specify an image format for serialized caches. /// /// It can serialize and deserialize PNG, JPEG, and GIF images. For images other than these formats, a normalized /// ``KingfisherWrapper/pngRepresentation()`` will be used. /// /// **Example:** /// /// ```swift /// let profileImageSize = CGSize(width: 44, height: 44) /// /// // A round corner image. /// let imageProcessor = RoundCornerImageProcessor( /// cornerRadius: profileImageSize.width / 2, targetSize: profileImageSize) /// /// let optionsInfo: KingfisherOptionsInfo = [ /// .cacheSerializer(FormatIndicatedCacheSerializer.png), /// .processor(imageProcessor) /// ] /// /// // A URL pointing to a JPEG image. /// let url = URL(string: "https://example.com/image.jpg")! /// /// // The image will always be cached as PNG format to preserve the alpha channel for the round rectangle. /// // When you load it from the cache later, it will still be round cornered. /// // Otherwise, the corner part would be filled by a white color (since JPEG does not contain an alpha channel). /// imageView.kf.setImage(with: url, options: optionsInfo) /// ``` public struct FormatIndicatedCacheSerializer: CacheSerializer { /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the PNG format. /// /// If the image cannot be represented in the PNG format, it will fallback to its actual format determined by the /// `original` data in ``CacheSerializer/data(with:original:)``. public static let png = FormatIndicatedCacheSerializer(imageFormat: .PNG, jpegCompressionQuality: nil) /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format. If the image cannot be /// represented by JPEG format, it will fallback to its real format which is determined by `original` data. /// The compression quality is 1.0 when using this serializer. If you need to set a customized compression quality, /// use `jpeg(compressionQuality:)`. /// /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the JPEG format. /// /// If the image cannot be represented in the JPEG format, it will fallback to its actual format determined by the /// `original` data in ``CacheSerializer/data(with:original:)``. /// /// > The compression quality is 1.0 when using this serializer. To set a customized compression quality, /// use ``FormatIndicatedCacheSerializer/jpeg(compressionQuality:)``. public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: 1.0) /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the JPEG format. /// /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. /// /// If the image cannot be represented in the JPEG format, it will fallback to its actual format determined by the /// `original` data in ``CacheSerializer/data(with:original:)``. public static func jpeg(compressionQuality: CGFloat) -> FormatIndicatedCacheSerializer { return FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: compressionQuality) } /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the GIF format. /// /// If the image cannot be represented in the GIF format, it will fallback to its actual format determined by the /// `original` data in ``CacheSerializer/data(with:original:)``. public static let gif = FormatIndicatedCacheSerializer(imageFormat: .GIF, jpegCompressionQuality: nil) // The specified image format. private let imageFormat: ImageFormat // The compression quality used for lossy image formats (like JPEG). private let jpegCompressionQuality: CGFloat? public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { func imageData(withFormat imageFormat: ImageFormat) -> Data? { return autoreleasepool { () -> Data? in switch imageFormat { case .PNG: return image.kf.pngRepresentation() case .JPEG: return image.kf.jpegRepresentation(compressionQuality: jpegCompressionQuality ?? 1.0) case .GIF: return image.kf.gifRepresentation() case .unknown: return nil } } } // generate data with indicated image format if let data = imageData(withFormat: imageFormat) { return data } let originalFormat = original?.kf.imageFormat ?? .unknown // generate data with original image's format if originalFormat != imageFormat, let data = imageData(withFormat: originalFormat) { return data } return original ?? image.kf.normalized.kf.pngRepresentation() } public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) } } ================================================ FILE: Sources/Cache/ImageCache.swift ================================================ // // ImageCache.swift // Kingfisher // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(macOS) import AppKit #else import UIKit #endif extension Notification.Name { /// This notification is sent when the disk cache is cleared, either due to expired cached files or the total size /// exceeding the maximum allowed size. /// /// The `object` of this notification is the ``ImageCache`` object that sends the notification. You can retrieve a /// list of removed hashes (files) by accessing the array under the ``KingfisherDiskCacheCleanedHashKey`` key in /// the `userInfo` of the received notification object. By checking the array, you can determine the hash codes /// of the removed files. /// /// > Invoking the `clearDiskCache` method manually will not trigger this notification. public static let KingfisherDidCleanDiskCache = Notification.Name("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache") } /// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCache` notification. public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash" /// The type of cache for a cached image. public enum CacheType: Sendable { /// The image is not yet cached when retrieving it. /// /// This indicates that the image was recently downloaded or generated rather than being retrieved from either /// memory or disk cache. case none /// The image is cached in memory and retrieved from there. case memory /// The image is cached in disk and retrieved from there. case disk /// Indicates whether the cache type represents the image is already cached or not. public var cached: Bool { switch self { case .memory, .disk: return true case .none: return false } } } /// Represents the result of the caching operation. public struct CacheStoreResult: Sendable { /// The caching result for memory cache. /// /// Caching an image to memory will never fail. public let memoryCacheResult: Result<(), Never> /// The caching result for disk cache. /// /// If an error occurs during the caching operation, you can retrieve it from the `.failure` case of this value. /// Usually, the error contains a ``KingfisherError/CacheErrorReason``. public let diskCacheResult: Result<(), KingfisherError> } extension KFCrossPlatformImage: CacheCostCalculable { /// The cost of an image. /// /// It is an estimated size represented as a bitmap, measured in bytes of all pixels. A larger cost indicates that /// when cached in memory, it occupies more memory space. This cost contributes to the /// ``MemoryStorage/Config/countLimit``. public var cacheCost: Int { return kf.cost } } extension Data: DataTransformable { public func toData() throws -> Data { self } public static func fromData(_ data: Data) throws -> Data { data } public static let empty = Data() } /// Represents the result of the operation to retrieve an image from the cache. public enum ImageCacheResult: Sendable { /// The image can be retrieved from the disk cache. case disk(KFCrossPlatformImage) /// The image can be retrieved from the memory cache. case memory(KFCrossPlatformImage) /// The image does not exist in the cache. case none /// Extracts the image from cache result. /// /// It returns the associated `Image` value for ``ImageCacheResult/disk(_:)`` and ``ImageCacheResult/memory(_:)`` /// case. For ``ImageCacheResult/none`` case, returns `nil`. public var image: KFCrossPlatformImage? { switch self { case .disk(let image): return image case .memory(let image): return image case .none: return nil } } /// Returns the corresponding ``CacheType`` value based on the result type of `self`. public var cacheType: CacheType { switch self { case .disk: return .disk case .memory: return .memory case .none: return .none } } } /// Represents a hybrid caching system composed of a ``MemoryStorage`` and a ``DiskStorage``. /// /// ``ImageCache`` serves as a high-level abstraction for storing an image and its data in memory and on disk, as well /// as retrieving them. You can define configurations for the memory cache backend and disk cache backend, and the the /// unified methods to store images to the cache or retrieve images from either the memory cache or the disk cache. /// /// > While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create /// your own cache object and configure its storages according to your needs. This class also provides an interface for /// configuring the memory and disk storage. open class ImageCache: @unchecked Sendable { // MARK: Singleton /// The default ``ImageCache`` object. /// /// Kingfisher uses this value for its related methods if no other cache is specified. /// /// > Warning: The `name` of this default cache is reserved as "default", and you should not use this name for any /// of your custom caches. Otherwise, different caches might become mixed up and corrupted. public static let `default` = ImageCache(name: "default") // MARK: Public Properties /// The ``MemoryStorage/Backend`` object for the memory cache used in this cache. /// /// This storage stores loaded images in memory with a reasonable expire duration and a maximum memory usage. /// /// > To modify the configuration of a storage, just set the storage ``MemoryStorage/Config`` and its properties. public let memoryStorage: MemoryStorage.Backend /// The ``DiskStorage/Backend`` object for the disk cache used in this cache. /// /// This storage stores loaded images on disk with a reasonable expire duration and a maximum disk usage. /// /// > To modify the configuration of a storage, just set the storage ``DiskStorage/Config`` and its properties. public let diskStorage: DiskStorage.Backend private let ioQueue: DispatchQueue /// A closure that specifies the disk cache path based on a given path and the cache name. public typealias DiskCachePathClosure = @Sendable (URL, String) -> URL // MARK: Initializers /// Creates an ``ImageCache`` with a customized ``MemoryStorage`` and ``DiskStorage``. /// /// - Parameters: /// - memoryStorage: The ``MemoryStorage/Backend`` object to be used in the image memory cache. /// - diskStorage: The ``DiskStorage/Backend`` object to be used in the image disk cache. public init( memoryStorage: MemoryStorage.Backend, diskStorage: DiskStorage.Backend) { self.memoryStorage = memoryStorage self.diskStorage = diskStorage let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(UUID().uuidString)" ioQueue = DispatchQueue(label: ioQueueName) Task { @MainActor in let notifications: [(Notification.Name, Selector)] #if !os(macOS) && !os(watchOS) notifications = [ (UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)), (UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)), (UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache)) ] #elseif os(macOS) notifications = [ (NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)), ] #else notifications = [] #endif notifications.forEach { NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil) } } } /// Creates an ``ImageCache`` with a given `name`. /// /// Both the ``MemoryStorage`` and the ``DiskStorage`` will be created with a default configuration based on the `name`. /// /// - Parameter name: The name of the cache object. It is used to set up disk cache directories and IO queues. /// You should not use the same `name` for different caches; otherwise, the disk storages would conflict with each /// other. The `name` should not be an empty string. /// /// > Warning: The `name` "default" is reserved to be used as the name of ``ImageCache/default`` in Kingfisher, /// and you should not use this name for any of your custom caches. Otherwise, different caches might become mixed /// up and corrupted. public convenience init(name: String) { self.init(noThrowName: name, cacheDirectoryURL: nil, diskCachePathClosure: nil) } /// Creates an ``ImageCache`` with a given `name`, the cache directory `path`, and a closure to modify the cache /// directory. /// /// - Parameters: /// - name: The name of the cache object. It is used to set up disk cache directories and IO queues. /// You should not use the same `name` for different caches; otherwise, the disk storages would conflict with each /// other. The `name` should not be an empty string. /// - cacheDirectoryURL: The location of the cache directory URL on disk. It will be passed internally to the /// initializer of the ``DiskStorage`` as the disk cache directory. If `nil`, the cache directory under the user /// domain mask will be used. /// - diskCachePathClosure: A closure that takes in an optional initial path string and generates the final disk /// cache path. You can use it to fully customize your cache path. /// - Throws: An error that occurs during the creation of the image cache, such as being unable to create a /// directory at the given path. /// /// > Warning: The `name` "default" is reserved to be used as the name of ``ImageCache/default`` in Kingfisher, /// and you should not use this name for any of your custom caches. Otherwise, different caches might become mixed /// up and corrupted. public convenience init( name: String, cacheDirectoryURL: URL?, diskCachePathClosure: DiskCachePathClosure? = nil ) throws { if name.isEmpty { fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") } let memoryStorage = ImageCache.createMemoryStorage() let config = ImageCache.createConfig( name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure ) let diskStorage = try DiskStorage.Backend(config: config) self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) } convenience init( noThrowName name: String, cacheDirectoryURL: URL?, diskCachePathClosure: DiskCachePathClosure? ) { if name.isEmpty { fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") } let memoryStorage = ImageCache.createMemoryStorage() let config = ImageCache.createConfig( name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure ) let diskStorage = DiskStorage.Backend(noThrowConfig: config, creatingDirectory: true) self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) } private static func createMemoryStorage() -> MemoryStorage.Backend { let totalMemory = ProcessInfo.processInfo.physicalMemory let costLimit = totalMemory / 4 let memoryStorage = MemoryStorage.Backend(config: .init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit))) return memoryStorage } private static func createConfig( name: String, cacheDirectoryURL: URL?, diskCachePathClosure: DiskCachePathClosure? = nil ) -> DiskStorage.Config { var diskConfig = DiskStorage.Config( name: name, sizeLimit: 0, directory: cacheDirectoryURL ) if let closure = diskCachePathClosure { diskConfig.cachePathBlock = closure } return diskConfig } deinit { NotificationCenter.default.removeObserver(self) } // MARK: Storing Images /// Stores an image to the cache. /// /// - Parameters: /// - image: The image that to be stored. /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for /// further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for /// caching in disk. It checks the image format based on the `original` data to determine the appropriate image /// format to use. For other types of `serializer`, it depends on their implementation details on how to use this /// original data. /// - key: The key used for caching the image. /// - options: The options which contains configurations for caching the image. /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. /// Otherwise, it is cached in both memory storage and disk storage. The default is `true`. /// - completionHandler: A closure which is invoked when the cache operation finishes. open func store( _ image: KFCrossPlatformImage, original: Data? = nil, forKey key: String, options: KingfisherParsedOptionsInfo, toDisk: Bool = true, completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil ) { let identifier = options.processor.identifier let callbackQueue = options.callbackQueue let computedKey = key.computedKey(with: identifier) // Memory storage should not throw. memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration) guard toDisk else { if let completionHandler = completionHandler { let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) callbackQueue.execute { completionHandler(result) } } return } ioQueue.async { let serializer = options.cacheSerializer if let data = serializer.data(with: image, original: original) { self.syncStoreToDisk( data, forKey: key, forcedExtension: options.forcedExtension, processorIdentifier: identifier, callbackQueue: callbackQueue, expiration: options.diskCacheExpiration, writeOptions: options.diskStoreWriteOptions, completionHandler: completionHandler) } else { guard let completionHandler = completionHandler else { return } let diskError = KingfisherError.cacheError( reason: .cannotSerializeImage(image: image, original: original, serializer: serializer)) let result = CacheStoreResult( memoryCacheResult: .success(()), diskCacheResult: .failure(diskError)) callbackQueue.execute { completionHandler(result) } } } } /// Stores an image in the cache. /// /// - Parameters: /// - image: The image to be stored. /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for /// further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for /// caching in disk. It checks the image format based on the `original` data to determine the appropriate image /// format to use. For other types of `serializer`, it depends on their implementation details on how to use this /// original data. /// - key: The key used for caching the image. /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the /// image, pass the identifier of the processor to this parameter. /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the /// disk storage configuration instead. /// - serializer: The ``CacheSerializer`` used to convert the `image` and `original` to the data that will be /// stored to disk. By default, the ``DefaultCacheSerializer/default`` will be used. /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. /// Otherwise, it is cached in both memory storage and disk storage. The default is `true`. /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is /// ``CallbackQueue/untouch``. Under this default ``CallbackQueue/untouch`` queue, if `toDisk` is `false`, it /// means the `completionHandler` will be invoked from the caller queue of this method; if `toDisk` is `true`, /// the `completionHandler` will be called from an internal file IO queue. To change this behavior, specify /// another ``CallbackQueue`` value. /// - completionHandler: A closure that is invoked when the cache operation finishes. open func store( _ image: KFCrossPlatformImage, original: Data? = nil, forKey key: String, processorIdentifier identifier: String = "", forcedExtension: String? = nil, cacheSerializer serializer: any CacheSerializer = DefaultCacheSerializer.default, toDisk: Bool = true, callbackQueue: CallbackQueue = .untouch, completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil ) { struct TempProcessor: ImageProcessor { let identifier: String func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { return nil } } let options = KingfisherParsedOptionsInfo([ .processor(TempProcessor(identifier: identifier)), .cacheSerializer(serializer), .callbackQueue(callbackQueue), .forcedCacheFileExtension(forcedExtension) ]) store( image, original: original, forKey: key, options: options, toDisk: toDisk, completionHandler: completionHandler ) } /// Store some data to the disk. /// /// - Parameters: /// - data: The data to be stored. /// - key: The key used for caching the data. /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the /// image, pass the identifier of the processor to this parameter. /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the /// disk storage configuration instead. /// - expiration: The expiration policy used by this storage action. /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is /// ``CallbackQueue/untouch``. Under this default ``CallbackQueue/untouch`` queue, if `toDisk` is `false`, it /// means the `completionHandler` will be invoked from the caller queue of this method; if `toDisk` is `true`, /// the `completionHandler` will be called from an internal file IO queue. To change this behavior, specify /// another ``CallbackQueue`` value. /// - completionHandler: A closure that is invoked when the cache operation finishes. open func storeToDisk( _ data: Data, forKey key: String, processorIdentifier identifier: String = "", forcedExtension: String? = nil, expiration: StorageExpiration? = nil, callbackQueue: CallbackQueue = .untouch, completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil) { ioQueue.async { self.syncStoreToDisk( data, forKey: key, forcedExtension: forcedExtension, processorIdentifier: identifier, callbackQueue: callbackQueue, expiration: expiration, completionHandler: completionHandler ) } } private func syncStoreToDisk( _ data: Data, forKey key: String, forcedExtension: String?, processorIdentifier identifier: String = "", callbackQueue: CallbackQueue = .untouch, expiration: StorageExpiration? = nil, writeOptions: Data.WritingOptions = [], completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil) { let computedKey = key.computedKey(with: identifier) let result: CacheStoreResult do { try self.diskStorage.store( value: data, forKey: computedKey, expiration: expiration, writeOptions: writeOptions, forcedExtension: forcedExtension ) result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) } catch { let diskError: KingfisherError if let error = error as? KingfisherError { diskError = error } else { diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error)) } result = CacheStoreResult( memoryCacheResult: .success(()), diskCacheResult: .failure(diskError) ) } if let completionHandler = completionHandler { callbackQueue.execute { completionHandler(result) } } } // MARK: Removing Images /// Removes the image for the given key from the cache. /// /// - Parameters: /// - key: The key used for caching the image. /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the /// image, pass the identifier of the processor to this parameter. /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the /// disk storage configuration instead. /// - fromMemory: Whether this image should be removed from memory storage or not. If `false`, the image won't be /// removed from the memory storage. The default is `true`. /// - fromDisk: Whether this image should be removed from the disk storage or not. If `false`, the image won't be /// removed from the disk storage. The default is `true`. /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is /// ``CallbackQueue/untouch``. /// - completionHandler: A closure that is invoked when the cache removal operation finishes. open func removeImage( forKey key: String, processorIdentifier identifier: String = "", forcedExtension: String? = nil, fromMemory: Bool = true, fromDisk: Bool = true, callbackQueue: CallbackQueue = .untouch, completionHandler: (@Sendable () -> Void)? = nil ) { removeImage( forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension, fromMemory: fromMemory, fromDisk: fromDisk, callbackQueue: callbackQueue, completionHandler: { _ in completionHandler?() } // This is a version which ignores error. ) } func removeImage( forKey key: String, processorIdentifier identifier: String = "", forcedExtension: String?, fromMemory: Bool = true, fromDisk: Bool = true, callbackQueue: CallbackQueue = .untouch, completionHandler: (@Sendable ((any Error)?) -> Void)? = nil) { let computedKey = key.computedKey(with: identifier) if fromMemory { memoryStorage.remove(forKey: computedKey) } @Sendable func callHandler(_ error: (any Error)?) { if let completionHandler = completionHandler { callbackQueue.execute { completionHandler(error) } } } if fromDisk { ioQueue.async{ do { try self.diskStorage.remove(forKey: computedKey, forcedExtension: forcedExtension) callHandler(nil) } catch { callHandler(error) } } } else { callHandler(nil) } } // MARK: Getting Images /// Retrieves an image for a given key from the cache, either from memory storage or disk storage. /// /// - Parameters: /// - key: The key used for caching the image. /// - options: The ``KingfisherParsedOptionsInfo`` options setting used for retrieving the image. /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. /// The default is ``CallbackQueue/mainCurrentOrAsync``. /// - completionHandler: A closure that is invoked when the image retrieval operation finishes. If the image /// retrieval operation finishes without any problems, an ``ImageCacheResult`` value will be sent to this closure /// as a result. Otherwise, a ``KingfisherError`` result with detailed failure reason will be sent. open func retrieveImage( forKey key: String, options: KingfisherParsedOptionsInfo, callbackQueue: CallbackQueue = .mainCurrentOrAsync, completionHandler: (@Sendable (Result) -> Void)?) { // No completion handler. No need to start working and early return. guard let completionHandler = completionHandler else { return } // Try to check the image from memory cache first. if let image = retrieveImageInMemoryCache(forKey: key, options: options) { callbackQueue.execute { completionHandler(.success(.memory(image))) } } else if options.fromMemoryCacheOrRefresh { callbackQueue.execute { completionHandler(.success(.none)) } } else { // Begin to disk search. self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) { result in switch result { case .success(let image): guard let image = image else { // No image found in disk storage. callbackQueue.execute { completionHandler(.success(.none)) } return } // Cache the disk image to memory. // We are passing `false` to `toDisk`, the memory cache does not change // callback queue, we can call `completionHandler` without another dispatch. var cacheOptions = options cacheOptions.callbackQueue = .untouch self.store( image, forKey: key, options: cacheOptions, toDisk: false) { _ in callbackQueue.execute { completionHandler(.success(.disk(image))) } } case .failure(let error): callbackQueue.execute { completionHandler(.failure(error)) } } } } } /// Retrieves an image for a given key from the cache, either from memory storage or disk storage. /// /// /// - Parameters: /// - key: The key used for caching the image. /// - options: The ``KingfisherOptionsInfo`` options setting used for retrieving the image. /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. /// The default is ``CallbackQueue/mainCurrentOrAsync``. /// - completionHandler: A closure that is invoked when the image retrieval operation finishes. If the image /// retrieval operation finishes without any problems, an ``ImageCacheResult`` value will be sent to this closure /// as a result. Otherwise, a ``KingfisherError`` result with detailed failure reason will be sent. /// /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead, /// override the version ``ImageCache/retrieveImageInDiskCache(forKey:options:callbackQueue:completionHandler:)`` /// accepts a ``KingfisherParsedOptionsInfo`` value. open func retrieveImage( forKey key: String, options: KingfisherOptionsInfo? = nil, callbackQueue: CallbackQueue = .mainCurrentOrAsync, completionHandler: (@Sendable (Result) -> Void)? ) { retrieveImage( forKey: key, options: KingfisherParsedOptionsInfo(options), callbackQueue: callbackQueue, completionHandler: completionHandler) } /// Retrieves an image associated with a given key from the memory storage. /// /// - Parameters: /// - key: The key used for caching the image. /// - options: The ``KingfisherParsedOptionsInfo`` options setting used to fetch the image. /// - Returns: The image stored in the memory cache if it exists and is valid. If the image does not exist or has /// already expired, `nil` is returned. open func retrieveImageInMemoryCache( forKey key: String, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { let computedKey = key.computedKey(with: options.processor.identifier) return memoryStorage.value( forKey: computedKey, extendingExpiration: options.memoryCacheAccessExtendingExpiration ) } /// Retrieves an image associated with a given key from the memory storage. /// /// - Parameters: /// - key: The key used for caching the image. /// - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image. /// - Returns: The image stored in the memory cache if it exists and is valid. If the image does not exist or has /// already expired, `nil` is returned. /// /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead, /// override the version ``ImageCache/retrieveImageInMemoryCache(forKey:options:)-2xj0`` that accepts a /// ``KingfisherParsedOptionsInfo`` value. open func retrieveImageInMemoryCache( forKey key: String, options: KingfisherOptionsInfo? = nil) -> KFCrossPlatformImage? { return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options)) } func retrieveImageInDiskCache( forKey key: String, options: KingfisherParsedOptionsInfo, callbackQueue: CallbackQueue = .untouch, completionHandler: @escaping @Sendable (Result) -> Void) { let computedKey = key.computedKey(with: options.processor.identifier) let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue) loadingQueue.execute { do { var image: KFCrossPlatformImage? = nil if let data = try self.diskStorage.value( forKey: computedKey, forcedExtension: options.forcedExtension, extendingExpiration: options.diskCacheAccessExtendingExpiration ) { image = options.cacheSerializer.image(with: data, options: options) } if options.backgroundDecode { image = image?.kf.decoded(scale: options.scaleFactor) } callbackQueue.execute { [image] in completionHandler(.success(image)) } } catch let error as KingfisherError { callbackQueue.execute { completionHandler(.failure(error)) } } catch { assertionFailure("The internal thrown error should be a `KingfisherError`.") } } } /// Retrieves an image associated with a given key from the disk storage. /// /// - Parameters: /// - key: The key used for caching the image. /// - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image. /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. /// The default is ``CallbackQueue/untouch``. /// - completionHandler: A closure that is invoked when the operation is finished. open func retrieveImageInDiskCache( forKey key: String, options: KingfisherOptionsInfo? = nil, callbackQueue: CallbackQueue = .untouch, completionHandler: @escaping @Sendable (Result) -> Void) { retrieveImageInDiskCache( forKey: key, options: KingfisherParsedOptionsInfo(options), callbackQueue: callbackQueue, completionHandler: completionHandler) } // MARK: Cleaning /// Clears the memory and disk storage of this cache. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. /// /// - Parameter handler: A closure that is invoked when the cache clearing operation finishes. /// This `handler` will be called from the main queue. public func clearCache(completion handler: (@Sendable () -> Void)? = nil) { clearMemoryCache() clearDiskCache(completion: handler) } /// Clears the memory storage of this cache. @objc public func clearMemoryCache() { memoryStorage.removeAll() } /// Clears the disk storage of this cache. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. /// /// - Parameter handler: A closure that is invoked when the cache clearing operation finishes. /// This `handler` will be called from the main queue. open func clearDiskCache(completion handler: (@Sendable () -> Void)? = nil) { ioQueue.async { do { try self.diskStorage.removeAll() } catch _ { } if let handler = handler { DispatchQueue.main.async { handler() } } } } /// Clears the expired images from the memory and disk storage. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. open func cleanExpiredCache(completion handler: (@Sendable () -> Void)? = nil) { cleanExpiredMemoryCache() cleanExpiredDiskCache(completion: handler) } /// Clears the expired images from the memory storage. open func cleanExpiredMemoryCache() { memoryStorage.removeExpired() } /// Clears the expired images from disk storage. /// /// This is an async operation. @objc func cleanExpiredDiskCache() { cleanExpiredDiskCache(completion: nil) } /// Clears the expired images from disk storage. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. /// /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. /// This `handler` will be called from the main queue. open func cleanExpiredDiskCache(completion handler: (@Sendable () -> Void)? = nil) { ioQueue.async { do { var removed: [URL] = [] let removedExpired = try self.diskStorage.removeExpiredValues() removed.append(contentsOf: removedExpired) let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues() removed.append(contentsOf: removedSizeExceeded) if !removed.isEmpty { DispatchQueue.main.async { [removed] in let cleanedHashes = removed.map { $0.lastPathComponent } NotificationCenter.default.post( name: .KingfisherDidCleanDiskCache, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes]) } } if let handler = handler { DispatchQueue.main.async { handler() } } } catch {} } } #if !os(macOS) && !os(watchOS) /// Clears the expired images from disk storage when the app is in the background. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. /// /// In most cases, you should not call this method explicitly. It will be called automatically when a /// `UIApplicationDidEnterBackgroundNotification` is received. @MainActor @objc public func backgroundCleanExpiredDiskCache() { // if 'sharedApplication()' is unavailable, then return guard let sharedApplication = KingfisherWrapper.shared else { return } actor BackgroundTaskState { private var value: UIBackgroundTaskIdentifier? = nil func setValue(_ newValue: UIBackgroundTaskIdentifier) { value = newValue } func takeValidValueAndInvalidate() -> UIBackgroundTaskIdentifier? { guard let task = value, task != .invalid else { return nil } value = .invalid return task } } let taskState = BackgroundTaskState() let endBackgroundTaskIfNeeded: @Sendable () -> Void = { Task { @MainActor in guard let bgTask = await taskState.takeValidValueAndInvalidate() else { return } guard let sharedApplication = KingfisherWrapper.shared else { return } #if compiler(>=6) sharedApplication.endBackgroundTask(bgTask) #else await sharedApplication.endBackgroundTask(bgTask) #endif } } let createdTask = sharedApplication.beginBackgroundTask( withName: "Kingfisher:backgroundCleanExpiredDiskCache", expirationHandler: endBackgroundTaskIfNeeded ) Task { await taskState.setValue(createdTask) } cleanExpiredDiskCache { Task { @MainActor in endBackgroundTaskIfNeeded() } } } #endif // MARK: Image Cache State /// Returns the cache type for a given `key` and `identifier` combination. /// /// This method is used to check whether an image is cached in the current cache. It also provides information on /// which kind of cache the image can be found in the return value. /// /// - Parameters: /// - key: The key used for caching the image. /// - identifier: The processor identifier used for this image. The default value is the /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the /// disk storage configuration instead. /// /// - Returns: A ``CacheType`` instance that indicates the cache status. ``CacheType/none`` indicates that the /// image is not in the cache or that it has already expired. open func imageCachedType( forKey key: String, processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, forcedExtension: String? = nil ) -> CacheType { let computedKey = key.computedKey(with: identifier) if memoryStorage.isCached(forKey: computedKey) { return .memory } if diskStorage.isCached(forKey: computedKey, forcedExtension: forcedExtension) { return .disk } return .none } /// Checks cache type for a given key and processor identifier combination asynchronously. /// /// This method is an opt-in alternative to ``imageCachedType(forKey:processorIdentifier:forcedExtension:)``. /// It performs any disk existence/meta check on the cache's I/O queue to avoid blocking the calling thread. /// /// - Parameters: /// - key: The key used for caching the image. /// - identifier: The processor identifier used for this image. The default value is the /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the /// disk storage configuration instead. /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. Default is `.mainCurrentOrAsync`. /// - completionHandler: Called with the resolved ``CacheType``. public func imageCachedTypeAsync( forKey key: String, processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, forcedExtension: String? = nil, callbackQueue: CallbackQueue = .mainCurrentOrAsync, completionHandler: @escaping @Sendable (CacheType) -> Void ) { let computedKey = key.computedKey(with: identifier) // Memory cache check remains synchronous (no I/O). if memoryStorage.isCached(forKey: computedKey) { callbackQueue.execute { completionHandler(.memory) } return } // Disk cache check on the I/O queue. ioQueue.async { [weak self] in guard let self else { callbackQueue.execute { completionHandler(.none) } return } let cached = self.diskStorage.isCached(forKey: computedKey, forcedExtension: forcedExtension) let result: CacheType = cached ? .disk : .none callbackQueue.execute { completionHandler(result) } } } /// Checks cache type for a given key and processor identifier combination asynchronously. /// /// This is an `async`/`await` convenience wrapper of /// ``imageCachedTypeAsync(forKey:processorIdentifier:forcedExtension:callbackQueue:completionHandler:)``. /// /// - Parameters: /// - key: The key used for caching the image. /// - identifier: The processor identifier used for this image. /// - forcedExtension: The expected extension of the file. /// /// - Returns: A ``CacheType`` instance that indicates the cache status. public func imageCachedTypeAsync( forKey key: String, processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, forcedExtension: String? = nil ) async -> CacheType { await withCheckedContinuation { continuation in imageCachedTypeAsync( forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension, callbackQueue: .untouch ) { cacheType in continuation.resume(returning: cacheType) } } } /// Returns whether the file exists in the cache for a given `key` and `identifier` combination. /// /// - Parameters: /// - key: The key used for caching the image. /// - identifier: The processor identifier used for this image. The default value is the /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the /// disk storage configuration instead. /// /// - Returns: A `Bool` value indicating whether a cache matches the given `key` and `identifier` combination. /// /// > The return value does not contain information about the kind of storage the cache matches from. /// > To obtain information about the cache type according to ``CacheType``, use /// ``ImageCache/imageCachedType(forKey:processorIdentifier:forcedExtension:)`` instead. public func isCached( forKey key: String, processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, forcedExtension: String? = nil ) -> Bool { return imageCachedType(forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension).cached } /// Retrieves the hash used as the cache file name for the key. /// /// - Parameters: /// - key: The key used for caching the image. /// - identifier: The processor identifier used for this image. The default value is the /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the /// disk storage configuration instead. /// /// - Returns: The hash used as the cache file name. /// /// > By default, for a given combination of `key` and `identifier`, the ``ImageCache`` instance uses the value /// returned by this method as the cache file name. You can use this value to check and match the cache file if /// needed. open func hash( forKey key: String, processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, forcedExtension: String? = nil ) -> String { let computedKey = key.computedKey(with: identifier) return diskStorage.cacheFileName(forKey: computedKey, forcedExtension: forcedExtension) } /// Calculates the size taken by the disk storage. /// /// It represents the total file size of all cached files in the ``ImageCache/diskStorage`` on disk in bytes. /// /// - Parameter handler: Called when the size calculation is complete. This closure is invoked from the main queue. open func calculateDiskStorageSize( completion handler: @escaping (@Sendable (Result) -> Void) ) { ioQueue.async { do { let size = try self.diskStorage.totalSize() DispatchQueue.main.async { handler(.success(size)) } } catch let error as KingfisherError { DispatchQueue.main.async { handler(.failure(error)) } } catch { assertionFailure("The internal thrown error should be a `KingfisherError`.") } } } /// Retrieves the cache path for the key. /// /// It is useful for projects with a web view or for anyone who needs access to the local file path. /// For instance, replacing the `` tag in your HTML. /// /// - Parameters: /// - key: The key used for caching the image. /// - identifier: The processor identifier used for this image. The default value is the /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the /// disk storage configuration instead. /// /// - Returns: The disk path of the cached image under the given `key` and `identifier`. /// /// > This method does not guarantee that there is an image already cached in the returned path. It simply provides /// > the path where the image should be if it exists in the disk storage. /// > /// > You could use the ``ImageCache/isCached(forKey:processorIdentifier:forcedExtension:)`` method to check whether the image is /// cached under that key on disk if necessary. open func cachePath( forKey key: String, processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, forcedExtension: String? = nil ) -> String { let computedKey = key.computedKey(with: identifier) return diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension).path } /// Returns the file URL if a disk cache file is existing for the target key, identifier and forcedExtension /// combination. Otherwise, if the requested cache value is not on the disk as a file, `nil`. /// /// - Parameters: /// - key: The key used for caching the item. /// - identifier: The processor identifier used for this image. It involves into calculating the final cache key. /// - forcedExtension: The expected extension of the file. /// - Returns: The file URL if a disk cache file is existing for the combination. Otherwise, `nil`. open func cacheFileURLIfOnDisk( forKey key: String, processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, forcedExtension: String? = nil ) -> URL? { let computedKey = key.computedKey(with: identifier) return diskStorage.isCached( forKey: computedKey, forcedExtension: forcedExtension ) ? diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension) : nil } // MARK: - Concurrency /// Stores an image to the cache. /// /// - Parameters: /// - image: The image that to be stored. /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for /// further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for /// caching in disk. It checks the image format based on the `original` data to determine the appropriate image /// format to use. For other types of `serializer`, it depends on their implementation details on how to use this /// original data. /// - key: The key used for caching the image. /// - options: The options which contains configurations for caching the image. /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. /// Otherwise, it is cached in both memory storage and disk storage. The default is `true`. open func store( _ image: KFCrossPlatformImage, original: Data? = nil, forKey key: String, options: KingfisherParsedOptionsInfo, toDisk: Bool = true ) async throws { try await withCheckedThrowingContinuation { continuation in store(image, original: original, forKey: key, options: options, toDisk: toDisk) { continuation.resume(with: $0.diskCacheResult) } } } /// Stores an image in the cache. /// /// - Parameters: /// - image: The image to be stored. /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for /// further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for /// caching in disk. It checks the image format based on the `original` data to determine the appropriate image /// format to use. For other types of `serializer`, it depends on their implementation details on how to use this /// original data. /// - key: The key used for caching the image. /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the /// image, pass the identifier of the processor to this parameter. /// - forcedExtension: The file extension, if exists. /// - serializer: The ``CacheSerializer`` used to convert the `image` and `original` to the data that will be /// stored to disk. By default, the ``DefaultCacheSerializer/default`` will be used. /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. /// Otherwise, it is cached in both memory storage and disk storage. The default is `true`. open func store( _ image: KFCrossPlatformImage, original: Data? = nil, forKey key: String, processorIdentifier identifier: String = "", forcedExtension: String? = nil, cacheSerializer serializer: any CacheSerializer = DefaultCacheSerializer.default, toDisk: Bool = true ) async throws { try await withCheckedThrowingContinuation { continuation in store( image, original: original, forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension, cacheSerializer: serializer, toDisk: toDisk) { // Only `diskCacheResult` can fail continuation.resume(with: $0.diskCacheResult) } } } open func storeToDisk( _ data: Data, forKey key: String, processorIdentifier identifier: String = "", forcedExtension: String? = nil, expiration: StorageExpiration? = nil ) async throws { try await withCheckedThrowingContinuation { continuation in storeToDisk( data, forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension, expiration: expiration) { // Only `diskCacheResult` can fail continuation.resume(with: $0.diskCacheResult) } } } /// Removes the image for the given key from the cache. /// /// - Parameters: /// - key: The key used for caching the image. /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the /// image, pass the identifier of the processor to this parameter. /// - forcedExtension: The file extension, if exists. /// - fromMemory: Whether this image should be removed from memory storage or not. If `false`, the image won't be /// removed from the memory storage. The default is `true`. /// - fromDisk: Whether this image should be removed from the disk storage or not. If `false`, the image won't be /// removed from the disk storage. The default is `true`. open func removeImage( forKey key: String, processorIdentifier identifier: String = "", forcedExtension: String? = nil, fromMemory: Bool = true, fromDisk: Bool = true ) async throws { return try await withCheckedThrowingContinuation { continuation in removeImage( forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension, fromMemory: fromMemory, fromDisk: fromDisk, completionHandler: { error in if let error { continuation.resume(throwing: error) } else { continuation.resume() } } ) } } /// Retrieves an image for a given key from the cache, either from memory storage or disk storage. /// /// - Parameters: /// - key: The key used for caching the image. /// - options: The ``KingfisherParsedOptionsInfo`` options setting used for retrieving the image. /// - Returns: /// If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value. /// /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework. open func retrieveImage( forKey key: String, options: KingfisherParsedOptionsInfo ) async throws -> ImageCacheResult { try await withCheckedThrowingContinuation { continuation in retrieveImage(forKey: key, options: options) { continuation.resume(with: $0) } } } /// Retrieves an image for a given key from the cache, either from memory storage or disk storage. /// /// - Parameters: /// - key: The key used for caching the image. /// - options: The ``KingfisherOptionsInfo`` options setting used for retrieving the image. /// /// - Returns: If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value. /// /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework. /// /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead, /// override the version ``ImageCache/retrieveImage(forKey:options:callbackQueue:completionHandler:)-1jjo3`` that /// accepts a ``KingfisherParsedOptionsInfo`` value. open func retrieveImage( forKey key: String, options: KingfisherOptionsInfo? = nil ) async throws -> ImageCacheResult { try await withCheckedThrowingContinuation { continuation in retrieveImage(forKey: key, options: options) { continuation.resume(with: $0) } } } /// Retrieves an image associated with a given key from the disk storage. /// /// - Parameters: /// - key: The key used for caching the image. /// - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image. /// /// - Returns: The image stored in the disk cache if it exists and is valid. If the image does not exist or has /// already expired, `nil` is returned. /// /// - Returns: If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value. /// /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework. /// ``KingfisherParsedOptionsInfo`` value. open func retrieveImageInDiskCache( forKey key: String, options: KingfisherOptionsInfo? = nil ) async throws -> KFCrossPlatformImage? { try await withCheckedThrowingContinuation { continuation in retrieveImageInDiskCache(forKey: key, options: options) { continuation.resume(with: $0) } } } /// Clears the memory and disk storage of this cache. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns. open func clearCache() async { await withCheckedContinuation { continuation in clearCache { continuation.resume() } } } /// Clears the disk storage of this cache. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns. open func clearDiskCache() async { await withCheckedContinuation { continuation in clearDiskCache { continuation.resume() } } } /// Clears the expired images from the memory and disk storage. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns. open func cleanExpiredCache() async { await withCheckedContinuation { continuation in cleanExpiredCache { continuation.resume() } } } /// Clears the expired images from disk storage. /// /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns. open func cleanExpiredDiskCache() async { await withCheckedContinuation { continuation in cleanExpiredDiskCache { continuation.resume() } } } /// Calculates the size taken by the disk storage. /// /// It represents the total file size of all cached files in the ``ImageCache/diskStorage`` on disk in bytes. open var diskStorageSize: UInt { get async throws { try await withCheckedThrowingContinuation { continuation in calculateDiskStorageSize { continuation.resume(with: $0) } } } } } // Concurrency #if !os(macOS) && !os(watchOS) // MARK: - For App Extensions extension UIApplication: KingfisherCompatible { } extension KingfisherWrapper where Base: UIApplication { public static var shared: UIApplication? { let selector = NSSelectorFromString("sharedApplication") guard Base.responds(to: selector) else { return nil } guard let unmanaged = Base.perform(selector) else { return nil } return unmanaged.takeUnretainedValue() as? UIApplication } } #endif extension String { func computedKey(with identifier: String) -> String { if identifier.isEmpty { return self } else { return appending("@\(identifier)") } } } ================================================ FILE: Sources/Cache/MemoryStorage.swift ================================================ // // MemoryStorage.swift // Kingfisher // // Created by Wei Wang on 2018/10/15. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents the concepts related to storage that stores a specific type of value in memory. /// /// This serves as a namespace for memory storage types. A ``MemoryStorage/Backend`` with a particular /// ``MemoryStorage/Config`` is used to define the storage. /// /// Refer to these composite types for further details. public enum MemoryStorage { /// Represents a storage that stores a specific type of value in memory. /// /// It provides fast access but has a limited storage size. The stored value type needs to conform to the /// ``CacheCostCalculable`` protocol, and its ``CacheCostCalculable/cacheCost`` will be used to determine the cost /// of the cache item's size in the memory. /// /// You can configure a ``MemoryStorage/Backend`` in its ``MemoryStorage/Backend/init(config:)`` method by passing /// a ``MemoryStorage/Config`` value or by modifying the ``MemoryStorage/Backend/config`` property after it's /// created. /// /// The ``MemoryStorage`` backend has an upper limit on the total cost size in memory and item count. All items in /// the storage have an expiration date. When retrieved, if the target item is already expired, it will be /// recognized as if it does not exist in the storage. /// /// The `MemoryStorage` also includes a scheduled self-cleaning task to evict expired items from memory. /// /// > This class is thready safe. public final class Backend: @unchecked Sendable where T: Sendable { let storage = NSCache>() // Keys track the objects once inside the storage. // // For object removing triggered by user, the corresponding key would be also removed. However, for the object // removing triggered by cache rule/policy of system, the key will be remained there until next `removeExpired` // happens. // // Breaking the strict tracking could save additional locking behaviors and improve the cache performance. // See https://github.com/onevcat/Kingfisher/issues/1233 var keys = Set() private var cleanTimer: Timer? = nil private let lock = NSLock() /// The configuration used in this storage. /// /// It is a value you can set and use to configure the storage as needed. public var config: Config { didSet { storage.totalCostLimit = config.totalCostLimit storage.countLimit = config.countLimit cleanTimer?.invalidate() cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in guard let self = self else { return } self.removeExpired() } } } /// Creates a ``MemoryStorage/Backend`` with a given ``MemoryStorage/Config`` value. /// /// - Parameter config: The configuration used to create the storage. It determines the maximum size limitation, /// default expiration settings, and more. public init(config: Config) { self.config = config storage.totalCostLimit = config.totalCostLimit storage.countLimit = config.countLimit cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in guard let self = self else { return } self.removeExpired() } } /// Removes the expired values from the storage. public func removeExpired() { lock.lock() defer { lock.unlock() } for key in keys { let nsKey = key as NSString guard let object = storage.object(forKey: nsKey) else { // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule. // We didn't remove the key yet until now, since we do not want to introduce additional lock. // See https://github.com/onevcat/Kingfisher/issues/1233 keys.remove(key) continue } if object.isExpired { storage.removeObject(forKey: nsKey) keys.remove(key) } } } /// Stores a value in the storage under the specified key and expiration policy. /// /// - Parameters: /// - value: The value to be stored. /// - key: The key to which the `value` will be stored. /// - expiration: The expiration policy used by this storage action. public func store( value: T, forKey key: String, expiration: StorageExpiration? = nil) { storeNoThrow(value: value, forKey: key, expiration: expiration) } // The no throw version for storing value in cache. Kingfisher knows the detail so it // could use this version to make syntax simpler internally. func storeNoThrow( value: T, forKey key: String, expiration: StorageExpiration? = nil) { lock.lock() defer { lock.unlock() } let expiration = expiration ?? config.expiration // The expiration indicates that already expired, no need to store. guard !expiration.isExpired else { return } let object: StorageObject if config.keepWhenEnteringBackground { object = BackgroundKeepingStorageObject(value, expiration: expiration) } else { object = StorageObject(value, expiration: expiration) } storage.setObject(object, forKey: key as NSString, cost: value.cacheCost) keys.insert(key) } /// Gets a value from the storage. /// /// - Parameters: /// - key: The cache key of the value. /// - extendingExpiration: The expiration policy used by this retrieval action. /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`. public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? { guard let object = storage.object(forKey: key as NSString) else { return nil } if object.isExpired { return nil } object.extendExpiration(extendingExpiration) return object.value } /// Determines whether there is valid cached data under a given key. /// /// - Parameter key: The cache key of the value. /// - Returns: `true` if there is valid data under the key, otherwise `false`. public func isCached(forKey key: String) -> Bool { guard let _ = value(forKey: key, extendingExpiration: .none) else { return false } return true } /// Removes a value from a specified key. /// /// - Parameter key: The cache key of the value. public func remove(forKey key: String) { lock.lock() defer { lock.unlock() } storage.removeObject(forKey: key as NSString) keys.remove(key) } /// Removes all values in this storage. public func removeAll() { lock.lock() defer { lock.unlock() } storage.removeAllObjects() keys.removeAll() } } } extension MemoryStorage { /// Represents the configuration used in a ``MemoryStorage/Backend``. public struct Config { /// The total cost limit of the storage. /// /// This counts up the value of ``CacheCostCalculable/cacheCost``. If adding this object to the cache causes /// the cache’s total cost to rise above totalCostLimit, the cache may automatically evict objects until its /// total cost falls below this value. public var totalCostLimit: Int /// The item count limit of the memory storage. /// /// The default value is `Int.max`, which means no hard limitation of the item count. public var countLimit: Int = .max /// The ``StorageExpiration`` used in this memory storage. /// /// The default is `.seconds(300)`, which means that the memory cache will expire in 5 minutes if not accessed. public var expiration: StorageExpiration = .seconds(300) /// The time interval between the storage performing cleaning work for sweeping expired items. public var cleanInterval: TimeInterval /// Determine whether newly added items to memory cache should be purged when the app goes to the background. /// /// By default, cached items in memory will be purged as soon as the app goes to the background to ensure a /// minimal memory footprint. Enabling this prevents this behavior and keeps the items alive in the cache even /// when your app is not in the foreground. /// /// The default value is `false`. After setting it to `true`, only newly added cache objects are affected. /// Existing objects that were already in the cache while this value was `false` will still be purged when the /// app enters the background. public var keepWhenEnteringBackground: Bool = false /// Creates a configuration from a given ``MemoryStorage/Config/totalCostLimit`` value and a /// ``MemoryStorage/Config/cleanInterval``. /// /// - Parameters: /// - totalCostLimit: The total cost limit of the storage in bytes. /// - cleanInterval: The time interval between the storage performing cleaning work for sweeping expired items. /// The default is 120, which means auto eviction happens once every two minutes. /// /// > Other properties of the ``MemoryStorage/Config`` will use their default values when created. public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) { self.totalCostLimit = totalCostLimit self.cleanInterval = cleanInterval } } } extension MemoryStorage { class BackgroundKeepingStorageObject: StorageObject, NSDiscardableContent { var accessing = true func beginContentAccess() -> Bool { if value != nil { accessing = true } else { accessing = false } return accessing } func endContentAccess() { accessing = false } func discardContentIfPossible() { value = nil } func isContentDiscarded() -> Bool { return value == nil } } class StorageObject { var value: T? let expiration: StorageExpiration private(set) var estimatedExpiration: Date init(_ value: T, expiration: StorageExpiration) { self.value = value self.expiration = expiration self.estimatedExpiration = expiration.estimatedExpirationSinceNow } func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) { switch extendingExpiration { case .none: return case .cacheTime: self.estimatedExpiration = expiration.estimatedExpirationSinceNow case .expirationTime(let expirationTime): self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow } } var isExpired: Bool { return estimatedExpiration.isPast } } } ================================================ FILE: Sources/Cache/Storage.swift ================================================ // // Storage.swift // Kingfisher // // Created by Wei Wang on 2018/10/15. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Constants for certain time intervals. struct TimeConstants { // Seconds in a day, a.k.a 86,400s, roughly. static let secondsInOneDay = 86_400 } /// Represents the expiration strategy utilized in storage. public enum StorageExpiration: Sendable { /// The item never expires. case never /// The item expires after a duration of the provided number of seconds from now. case seconds(TimeInterval) /// The item expires after a duration of the provided number of days from now. case days(Int) /// The item expires after a specified date. case date(Date) /// Indicates that the item has already expired. /// /// Use this to bypass the cache. case expired func estimatedExpirationSince(_ date: Date) -> Date { switch self { case .never: return .distantFuture case .seconds(let seconds): return date.addingTimeInterval(seconds) case .days(let days): let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay * days) return date.addingTimeInterval(duration) case .date(let ref): return ref case .expired: return .distantPast } } var estimatedExpirationSinceNow: Date { estimatedExpirationSince(Date()) } var isExpired: Bool { timeInterval <= 0 } var timeInterval: TimeInterval { switch self { case .never: return .infinity case .seconds(let seconds): return seconds case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay * days) case .date(let ref): return ref.timeIntervalSinceNow case .expired: return -(.infinity) } } } /// Represents the expiration extension strategy used in storage after access. public enum ExpirationExtending: Sendable { /// The item expires after the original time, without extension after access. case none /// The item expiration extends to the original cache time after each access. case cacheTime /// The item expiration extends by the provided time after each access. case expirationTime(_ expiration: StorageExpiration) } /// Represents types for which the memory cost can be calculated. public protocol CacheCostCalculable { var cacheCost: Int { get } } /// Represents types that can be converted to and from data. public protocol DataTransformable { /// Converts the current value to a `Data` representation. /// - Returns: The data object which can represent the value of the conforming type. /// - Throws: If any error happens during the conversion. func toData() throws -> Data /// Convert some data to the value. /// - Parameter data: The data object which should represent the conforming value. /// - Returns: The converted value of the conforming type. /// - Throws: If any error happens during the conversion. static func fromData(_ data: Data) throws -> Self /// An empty object of `Self`. /// /// > In the cache, when the data is not actually loaded, this value will be returned as a placeholder. /// > This variable should be returned quickly without any heavy operation inside. static var empty: Self { get } } ================================================ FILE: Sources/Documentation.docc/CommonTasks/CommonTasks.md ================================================ # Common Tasks Below is a code snippet designed to address the most commonly encountered tasks. You are encouraged to freely integrate this snippet into your upcoming projects. @Metadata { @PageImage(purpose: card, source: "common-tasks-card")) @PageColor(blue) } ## Overview This document provides a comprehensive guide to the most prevalent use cases. The included code snippet is tailored for iOS development. Nevertheless, it can be adapted for other platforms, such as macOS or tvOS, with minimal modifications. This typically involves substituting specific classes (for instance, replacing `UIImage` with `NSImage`). To explore detailed instructions for specific components within the Kingfisher framework, please refer to the subsequent documentation: #### Common Tasks for Main Components @Links(visualStyle: list) { - - - } #### Other Topics @Links(visualStyle: list) { - - - - - - } ## Most Common Tasks The view extension-based APIs for `UIImageView`, `NSImageView`, `UIButton`, and `NSButton` are recommended as your primary choice. They simplify and enhance the elegance of your code. ### Setting Image with a `URL` ```swift let url = URL(string: "https://example.com/image.jpg") imageView.kf.setImage(with: url) ``` This code performs the following actions: 1. Verifies if an image is cached using the key `url.absoluteString`. 2. Retrieves and assigns the image to `imageView.image` if found in cache (memory or disk). 3. If absent, initiates a request and downloads from `url`. 4. Transforms the downloaded data into a `UIImage`. 5. Stores the image in both memory and disk caches. 6. Updates `imageView.image` with the new image. Subsequent calls to `setImage` with the same URL will only execute steps 1 and 2, unless the cache has been cleared. ### Showing a Placeholder ```swift let image = UIImage(named: "default_profile_icon") imageView.kf.setImage(with: url, placeholder: image) ``` The `imageView` will display the `image` as the placeholder during its download from the `url`. > You can also employ a custom `UIView` or `NSView` as a placeholder by making it conform to the `Placeholder` protocol: > > ```swift > class MyView: UIView { /* Implementation of your view */ } > > extension MyView: Placeholder { /* This can be left empty */ } > > imageView.kf.setImage(with: url, placeholder: MyView()) > ``` > > The instance of `MyView` will be dynamically added to or removed from the `imageView` as required. ### Showing a Loading Indicator while Downloading ```swift imageView.kf.indicatorType = .activity imageView.kf.setImage(with: url) ``` This shows a `UIActivityIndicatorView` in center of image view while downloading. ### Fading in Downloaded Image For UIKit/AppKit: ```swift imageView.kf.setImage(with: url, options: [.transition(.fade(0.2))]) ``` For SwiftUI (recommended): ```swift KFImage(url) .fade(duration: 0.2) // Or use native SwiftUI transitions: .loadTransition(.opacity, animation: .easeInOut(duration: 0.2)) ``` > 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. ### Completion Handler ```swift imageView.kf.setImage(with: url) { result in // `result` is either a `.success(RetrieveImageResult)` or a `.failure(KingfisherError)` switch result { case .success(let value): // The image was set to image view: print(value.image) // From where the image was retrieved: // - .none - Just downloaded. // - .memory - Got from memory cache. // - .disk - Got from disk cache. print(value.cacheType) // The source object which contains information like `url`. print(value.source) case .failure(let error): print(error) // The error happens } } ``` ### Getting an Image without Setting to UI Occasionally, you might need to retrieve an image using Kingfisher without assigning it to an image view. In such cases, use ``KingfisherManager/retrieveImage(with:options:progressBlock:)-80fw1`` ```swift KingfisherManager.shared.retrieveImage(with: url) { result in // Do something with `result` } ``` ================================================ FILE: Sources/Documentation.docc/CommonTasks/CommonTasks_Cache.md ================================================ # Common Tasks - Cache Common tasks related to the ``ImageCache`` in Kingfisher. ## Overview Kingfisher employs a hybrid ``ImageCache`` for managing cached images, comprising both memory and disk storage. It offers high-level APIs for cache management. Unless otherwise specified, the ``ImageCache/default`` instance is used throughout Kingfisher. ### Using another cache key By default, the URL is converted into a string to generate the cache key. For network URLs, `absoluteString` is utilized. You can customize the key by creating an ``ImageResource`` object with a specified key. ```swift let resource = ImageResource( downloadURL: url, cacheKey: "my_cache_key" ) imageView.kf.setImage(with: resource) ``` Kingfisher uses the `cacheKey` to locate images in the cache. Ensure you use a distinct key for each different image. #### Checking whether an image in the cache ```swift let cache = ImageCache.default let cached = cache.isCached(forKey: cacheKey) // To know where the cached image is: let cacheType = cache.imageCachedType(forKey: cacheKey) // `.memory`, `.disk` or `.none`. ``` > Note: ``ImageCache/imageCachedType(forKey:processorIdentifier:forcedExtension:)`` may touch the disk cache > synchronously when checking `.disk`. If you want to avoid blocking the calling thread (for example, in a scrolling > UI on the main thread), use the opt-in async API instead: > > ```swift > cache.imageCachedTypeAsync(forKey: cacheKey) { cacheType in > // `.memory`, `.disk` or `.none`. > } > > // Or with async/await: > let cacheType = await cache.imageCachedTypeAsync(forKey: cacheKey) > ``` If a processor is applied when retrieving an image, the processed image will be cached. In this scenario, remember to also include the processor identifier when manipulating the cache: ```swift let processor = RoundCornerImageProcessor(cornerRadius: 20) imageView.kf.setImage(with: url, options: [.processor(processor)]) // Later cache.isCached(forKey: cacheKey, processorIdentifier: processor.identifier) ``` #### Getting an image from the cache ```swift cache.retrieveImage(forKey: "cacheKey") { result in switch result { case .success(let value): print(value.cacheType) // If the `cacheType is `.none`, `image` will be `nil`. print(value.image) case .failure(let error): print(error) } } ``` #### Set limit for the cache For memory storage, you can set its ``MemoryStorage/Config/totalCostLimit`` and ``MemoryStorage/Config/countLimit``: ```swift // Limit memory cache size to 300 MB. cache.memoryStorage.config.totalCostLimit = 300 * 1024 * 1024 // Limit memory cache to hold 150 images at most. cache.memoryStorage.config.countLimit = 150 ``` The default ``MemoryStorage/Config/totalCostLimit`` for the memory cache is set to 25% of the device's total memory, with no limit on the ``MemoryStorage/Config/countLimit``. For disk storage, you have the option to set a ``DiskStorage/Config/sizeLimit`` to manage the space used on the file system. ```swift // Limit disk cache size to 1 GB. cache.diskStorage.config.sizeLimit = 1000 * 1024 * 1024 ``` #### Set the default expiration for cache Both memory and disk storage in Kingfisher have default expiration settings. Images in memory storage expire 5 minutes after the last access, whereas images in disk storage expire after one week. These values can be modified as follows: ```swift // Set memory image expires after 10 minutes. cache.memoryStorage.config.expiration = .seconds(600) // Set disk image never expires. cache.diskStorage.config.expiration = .never ``` To override this default expiration for a specific image when caching it, include an option as follows during image setting: ```swift // This image will never expire in memory cache. imageView.kf.setImage(with: url, options: [.memoryCacheExpiration(.never)]) ``` The expired memory cache is purged every 2 minutes by default. To adjust this frequency: ```swift // Check memory clean up every 30 seconds. cache.memoryStorage.config.cleanInterval = 30 ``` #### Store images to cache manually By default, view extension methods and ``KingfisherManager`` automatically store retrieved images in the cache. However, you can also manually store an image to the cache: ```swift let image: UIImage = //... cache.store(image, forKey: cacheKey) ``` If you possess the original data of the image, pass it along to ``ImageCache``. This assists Kingfisher in determining the appropriate format for storing the image: ```swift let data: Data = //... let image: UIImage = //... cache.store(image, original: data, forKey: cacheKey) ``` #### Remove images from cache manually Kingfisher manages its cache automatically. But you still can manually remove a certain image from cache: ```swift cache.removeImage(forKey: cacheKey) ``` Or, with more control: ```swift cache.removeImage( forKey: cacheKey, processorIdentifier: processor.identifier, fromMemory: false, fromDisk: true) { print("Removed!") } ``` #### Clear the cache ```swift // Remove all. cache.clearMemoryCache() cache.clearDiskCache { print("Done") } // Remove only expired. cache.cleanExpiredMemoryCache() cache.cleanExpiredDiskCache { print("Done") } ``` #### Report the disk storage size ```swift ImageCache.default.calculateDiskStorageSize { result in switch result { case .success(let size): print("Disk cache size: \(Double(size) / 1024 / 1024) MB") case .failure(let error): print(error) } } ``` #### Create your own cache and use it ```swift // The `name` parameter is used to identify the disk cache bound to the `ImageCache`. let cache = ImageCache(name: "my-own-cache") imageView.kf.setImage(with: url, options: [.targetCache(cache)]) ``` #### Skipping cache searching, force downloading image again ```swift imageView.kf.setImage(with: url, options: [.forceRefresh]) ``` #### Only search cache for the image, do not download if not existing This makes your app to an "offline" mode. ```swift imageView.kf.setImage(with: url, options: [.onlyFromCache]) ``` If the image does not exist in the cache, an ``KingfisherError/CacheErrorReason/imageNotExisting(key:)`` error will be triggered. #### Waiting for cache to finish Storing images in the disk cache is asynchronous and doesn't need to be completed before setting the image view and invoking the completion handler in view extension methods. This means that the disk cache might not be fully updated at the time the completion handler is executed, as shown below: ```swift imageView.kf.setImage(with: url) { _ in ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) { result in switch result { case .success(let image): // `image` might be `nil` here. case .failure: break } } } ``` For most scenarios, this asynchronous behavior isn't an issue. However, if your logic relies on the existence of the disk cache, use the `.waitForCache` option. With this option, Kingfisher will delay the execution of the handler until the disk cache operation is complete: ```swift imageView.kf.setImage(with: url, options: [.waitForCache]) { _ in ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) { result in switch result { case .success(let image): // `image` exists. case .failure: break } } } ``` This consideration applies specifically to disk image caching, which involves asynchronous I/O operations. In contrast, memory cache operations are synchronous, ensuring that the image is always available in the memory cache. ================================================ FILE: Sources/Documentation.docc/CommonTasks/CommonTasks_Downloader.md ================================================ # Common Tasks - Downloader Common tasks related to the ``ImageDownloader`` in Kingfisher. ## Overview ``ImageDownloader`` wraps a `URLSession` for downloading an image from the Internet. Similar to ``ImageCache``, there is a ``ImageDownloader/default`` downloader for downloading tasks. ### Download an image manually Typically, you might use Kingfisher's view extension methods or ``KingfisherManager`` for image retrieval. These methods prioritize searching the cache to avoid unnecessary downloads. If you need to download an image without caching it, consider the following approach: ```swift let downloader = ImageDownloader.default downloader.downloadImage(with: url) { result in switch result { case .success(let value): print(value.image) case .failure(let error): print(error) } } ``` ### Modify a request before sending When managing access to your image resources with permission controls, you can customize the request using a ``KingfisherOptionsInfoItem/requestModifier(_:)``: ```swift let modifier = AnyModifier { request in var r = request r.setValue("abc", forHTTPHeaderField: "Access-Token") return r } downloader.downloadImage(with: url, options: [.requestModifier(modifier)]) { result in // ... } // This option also works for view extension methods. imageView.kf.setImage(with: url, options: [.requestModifier(modifier)]) ``` ### Use async request modifier If an asynchronous operation is required before modifying the request, create a type that conforms to ``AsyncImageDownloadRequestModifier``: ```swift class AsyncModifier: AsyncImageDownloadRequestModifier { var onDownloadTaskStarted: ((DownloadTask?) -> Void)? func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) { var r = request someAsyncOperation { result in r.someProperty = result.property reportModified(r) } } } ``` Similarly, use the ``KingfisherOptionsInfoItem/requestModifier(_:)`` to apply this modifier. In such scenarios, the ``KingfisherWrapper/setImage(with:placeholder:options:progressBlock:completionHandler:)-8lmr3`` or ``ImageDownloader/downloadImage(with:options:completionHandler:)-2ztyq`` method will no longer return a ``DownloadTask`` directly, as the download task isn't initiated instantly. To reference the task, monitor the ``AsyncImageDownloadRequestModifier/onDownloadTaskStarted`` callback. ```swift let modifier = AsyncModifier() modifier.onDownloadTaskStarted = { task in if let task = task { print("A download task started: \(task)") } } let nilTask = imageView.kf.setImage(with: url, options: [.requestModifier(modifier)]) ``` ### Cancel a download task Once the download has started, a ``DownloadTask`` will be created and returned. This can be used to cancel an ongoing download task. ```swift let task = downloader.downloadImage(with: url) { result in // ... case .failure(let error): print(error.isTaskCancelled) // true } } // After some time, but before the download task completes. task?.cancel() ``` If you call ``DownloadTask/cancel()`` after the task has already finished, no action will be taken. Likewise, the view extension methods return a ``DownloadTask`` as well. This allows you to store the task and cancel it if needed: ```swift let task = imageView.kf.set(with: url) task?.cancel() ``` Alternatively, you can invoke ``KingfisherWrapper/cancelDownloadTask()-2gg15`` on the image view to cancel the **current downloading task**. ```swift let task1 = imageView.kf.set(with: url1) let task2 = imageView.kf.set(with: url2) imageView.kf.cancelDownloadTask() // `task2` will be cancelled, but `task1` is still running. // However, the downloaded image for `task1` will not be set because the image view expects a result from `url2`. ``` ### Authentication with `NSURLCredential` The ``ImageDownloader`` defaults to `.performDefaultHandling` upon receiving a server challenge. To supply custom credentials, configure an ``ImageDownloader/authenticationChallengeResponder``: ```swift // In ViewController ImageDownloader.default.authenticationChallengeResponder = self extension ViewController: AuthenticationChallengeResponsable { var disposition: URLSession.AuthChallengeDisposition { /* */ } let credential: URLCredential? { /* */ } func downloader( _ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { // Provide your `AuthChallengeDisposition` and `URLCredential` completionHandler(disposition, credential) } func downloader( _ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { // Provide your `AuthChallengeDisposition` and `URLCredential` completionHandler(disposition, credential) } } ``` ### Set customize timeout The default download timeout for a request is 15 seconds. To customize this for the downloader: ```swift // Set the timeout to 1 minute. downloader.downloadTimeout = 60 ``` For setting a timeout specific to a request, utilize a ``KingfisherOptionsInfoItem/requestModifier(_:)``: ```swift let modifier = AnyModifier { request in var r = request r.timeoutInterval = 60 return r } downloader.downloadImage(with: url, options: [.requestModifier(modifier)]) ``` ================================================ FILE: Sources/Documentation.docc/CommonTasks/CommonTasks_Processor.md ================================================ # Common tasks - Processor Common tasks related to the ``ImageProcessor`` in Kingfisher. ## Overview ``ImageProcessor`` is used to transform an image (or data) into another image. By supplying a processor to ``KingfisherManager`` when setting the image, it can be applied to the downloaded data. The processed image will then be sent to the image view and stored in the cache. ### Use the default processor ```swift // Just without anything imageView.kf.setImage(with: url) // It equals to imageView.kf.setImage(with: url, options: [.processor(DefaultImageProcessor.default)]) ``` > The ``DefaultImageProcessor`` converts downloaded data into a corresponding image object. > It supports PNG, JPEG, and GIF formats. ### Built-in Processors @Row { @Column(size: 3) { ```swift // Round corner RoundCornerImageProcessor(cornerRadius: 20) ``` } @Column { ![A screenshot of the power picker user interface with four powers displayed – ice, fire, wind, and lightning](common-tasks-card) } } ```swift // Round corner let processor = RoundCornerImageProcessor(cornerRadius: 20) // Downsampling let processor = DownsamplingImageProcessor(size: CGSize(width: 100, height: 100)) // Cropping let processor = CroppingImageProcessor(size: CGSize(width: 100, height: 100), anchor: CGPoint(x: 0.5, y: 0.5)) // Blur let processor = BlurImageProcessor(blurRadius: 5.0) // Overlay with a color & fraction let processor = OverlayImageProcessor(overlay: .red, fraction: 0.7) // Tint with a color let processor = TintImageProcessor(tint: .blue) // Adjust color let processor = ColorControlsProcessor(brightness: 1.0, contrast: 0.7, saturation: 1.1, inputEV: 0.7) // Black & White let processor = BlackWhiteProcessor() // Blend (iOS) let processor = BlendImageProcessor(blendMode: .darken, alpha: 1.0, backgroundColor: .lightGray) // Compositing let processor = CompositingImageProcessor(compositingOperation: .darken, alpha: 1.0, backgroundColor: .lightGray) // Use the process in view extension methods. imageView.kf.setImage(with: url, options: [.processor(processor)]) ``` ### Multiple Processors ```swift // First blur the image, then make it round cornered. let processor = BlurImageProcessor(blurRadius: 4) |> RoundCornerImageProcessor(cornerRadius: 20) imageView.kf.setImage(with: url, options: [.processor(processor)]) ``` ### Creating your own processor Make a type conforming to `ImageProcessor` by implementing `identifier` and `process`: > important: ``ImageProcessor/identifier`` is used to determine the cache key when this processor is applied. It is your > responsibility to keep it the same for processors with the same properties/functionality. ```swift struct MyProcessor: ImageProcessor { let someValue: Int var identifier: String { "com.yourdomain.myprocessor-\(someValue)" } // Convert input data/image to target image and return it. func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { switch item { case .image(let image): // A previous processor already converted the image to an image object. // You can do whatever you want to apply to the image and return the result. return image case .data(let data): // Your own way to convert some data to an image. return createAnImage(data: data) } } } ``` Then pass it to the ``KingfisherWrapper/setImage(with:placeholder:options:completionHandler:)-9h820`` methods: ```swift let processor = MyProcessor(someValue: 10) let url = URL(string: "https://example.com/my_image.png") imageView.kf.setImage(with: url, options: [.processor(processor)]) ``` ### Creating a processor from CIFilter If you have a prepared `CIFilter`, you can create a processor quickly from it. ```swift struct MyCIFilter: CIImageProcessor { let identifier = "com.yourdomain.myCIFilter" let filter = Filter { input in guard let filter = CIFilter(name: "xxx") else { return nil } filter.setValue(input, forKey: kCIInputBackgroundImageKey) return filter.outputImage } } ``` ================================================ FILE: Sources/Documentation.docc/CommonTasks/CommonTasks_Serializer.md ================================================ # Common Tasks - Serializer ``CacheSerializer`` is utilized to convert data into an image object for retrieval from disk cache, and conversely, for storing images to the disk cache. ### Use the default serializer ```swift // Just without anything imageView.kf.setImage(with: url) // It equals to imageView.kf.setImage(with: url, options: [.cacheSerializer(DefaultCacheSerializer.default)]) ``` ``DefaultCacheSerializer`` is responsible for converting cached data into a corresponding image object and vice versa. It supports PNG, JPEG, and GIF formats by default. When storing an image to disk, if the `original` data is available (for example, when the image is just downloaded), ``DefaultCacheSerializer`` uses it to determine the format. If `original` is `nil` (for example, when the image is retrieved from cache), Kingfisher will try to infer the format: if the image still carries embedded GIF data, it will be stored as GIF; otherwise it falls back to encoding as PNG. ### Notes for animated images and custom processors For animated images, Kingfisher keeps the original GIF bytes as an internal associated object on the image instance. If a custom ``ImageProcessor`` creates and returns a new `UIImage`/`NSImage` instance from an animated image, this internal animated data is not automatically carried over, and the disk cache may fall back to encoding as PNG (first frame only). To avoid this: - For animated inputs, return the input image directly when possible. - If you need to create a new image instance in the `.image` branch, copy Kingfisher internal states to the new image by calling ``KingfisherWrapper/copyKingfisherState(to:)``. - If your goal is to avoid re-encoding, consider caching original data by using a serializer with ``CacheSerializer/originalDataUsed`` enabled (e.g. configure ``DefaultCacheSerializer/preferCacheOriginalData``). ### Enforce a format To enforce a specific image format, use ``FormatIndicatedCacheSerializer``, which offers serializers for all supported formats: ``FormatIndicatedCacheSerializer/png``, ``FormatIndicatedCacheSerializer/jpeg``, and ``FormatIndicatedCacheSerializer/gif``. #### Use PNG serializer when rounding image corner While ``DefaultCacheSerializer`` aims to preserve the original format of input image data, there are scenarios where this behavior might not meet your needs. For example, when using a ``RoundCornerImageProcessor``, it's often desirable to maintain an alpha channel for transparency around the corners. JPEG images, lacking an alpha channel, would not support this transparency when saved. To ensure the presence of an alpha channel by converting images to PNG, you can set the PNG serializer explicitly: ```swift let roundCorner = RoundCornerImageProcessor(cornerRadius: 20) imageView.kf.setImage(with: url, options: [.processor(roundCorner), .cacheSerializer(FormatIndicatedCacheSerializer.png)] ) ``` ### Creating customized serializer Make a type conforming to `CacheSerializer` by implementing `data(with:original:)` and `image(with:options:)`: To create a type that conforms to ``CacheSerializer``, implement the ``CacheSerializer/data(with:original:)`` and ``CacheSerializer/image(with:options:)``: ```swift struct MyCacheSerializer: CacheSerializer { func data(with image: Image, original: Data?) -> Data? { return MyFramework.data(of: image) } func image(with data: Data, options: KingfisherParsedOptionsInfo?) -> Image? { return MyFramework.createImage(from: data) } } ``` Then pass it to the ``KingfisherWrapper/setImage(with:placeholder:options:completionHandler:)-9h820`` methods: ```swift let serializer = MyCacheSerializer() let url = URL(string: "https://yourdomain.com/example.png") imageView.kf.setImage(with: url, options: [.cacheSerializer(serializer)]) ``` ================================================ FILE: Sources/Documentation.docc/Documentation.md ================================================ # ``Kingfisher`` @Metadata { @PageImage( purpose: icon, source: "logo", alt: "The logo icon of Kingfisher") @PageColor(blue) } A lightweight, pure-Swift library for downloading and caching images from the web. ## Overview Kingfisher 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, regardless you are using UIKit, AppKit or SwiftUI. With Kingfisher, you can easily: - **Download** the images from a remote URL and display it in an image view or button. - **Cache** the images in both the memory and the disk. When loading for the next time, it shows immediately without downloading again. - **Process** the downloaded images with pre-defined or customized processors. ### Featured @Links(visualStyle: detailedGrid) { - - } ## Topics ### Essentials - - ### Loading Images in Simple Way - ``KingfisherCompatible`` - ``KingfisherWrapper/setImage(with:placeholder:options:completionHandler:)-8qfkr`` - ``KingfisherManager`` - ``Source`` ### Loading Options - ``KingfisherOptionsInfoItem`` ### Image Downloader - - ``ImageDownloader`` - ``ImagePrefetcher`` - ``DownloadTask`` ### Image Processor @Links(visualStyle: detailedGrid) { - - ``ImageProcessor`` } ### Image Cache & Serializer - - - ``ImageCache`` - ``CacheSerializer`` ### GIF - ``AnimatedImageView`` - ``GIFAnimatedImage`` ### Live Photo - - ``KingfisherWrapper/setImage(with:options:completionHandler:)-1to8a`` ### SwiftUI - ``KFImage`` ### Help & Communication - - ================================================ FILE: Sources/Documentation.docc/GettingStarted.md ================================================ # Getting Started @Metadata { @PageImage(purpose: card, source: "getting-started-card")) @PageColor(blue) } Installs Kingfisher to your project, setup everything and some starter examples of the core functionality. ## Overview Kingfisher is designed to facilitate the downloading and caching of remote images in the simplest way possible. As such, the basic usage of Kingfisher is straightforward. We offer two step-by-step tutorials to help you understand and utilize Kingfisher's fundamental features in both UIKit and SwiftUI environments. The tutorials will cover the following aspects: ##### Installing Kingfisher Learn how to integrate Kingfisher into your project setup. ##### Loading and Displaying Images Discover how to effortlessly fetch and display images from remote URLs using convenient view extensions. ##### Processing Images with Processors Understand how to manipulate and transform images using the ImageProcessor functionality. ##### Inspecting and Managing Image Cache Gain insights into how to check the image cache status and handle image caching. ## Tutorials By following these tutorials, you will acquire a preliminary understanding of Kingfisher, laying the groundwork for potential advanced usage in the future. Choose the approach you prefer to begin the tutorial (UIKit or SwiftUI): @Links(visualStyle: list) { - - } > tip: In addition to UIKit and SwiftUI, Kingfisher also offers support for use in AppKit. This extends Kingfisher's > versatility across different Apple platforms, providing a unified API for handling remote images. > > If you are interested in utilizing Kingfisher within an AppKit context, we recommend referring to the UIKit > tutorials as a starting point. Most of the concept, even the APIs, are shared. ================================================ FILE: Sources/Documentation.docc/MigrationGuide/Migration-To-6.md ================================================ # Migrating from v5 to v6 Migrating Kingfisher from version 5 to version 6. ## Overview Kingfisher 6.0 contains some breaking changes if you want to upgrade from the previous version. Depending 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. ### SwiftUI support Kingfisher 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. In 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. That means, there is no `KingfisherSwiftUI` or `Kingfisher/SwiftUI` anymore. If you installed it through: - Carthage: Remove `KingfisherSwiftUI` from "Linked Frameworks and Libraries" and all "KingfisherSwiftUI.framework" related lines from the "copy-framework". - 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. - 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. When it is done, you can now replace any `import KingfisherSwiftUI` with `import Kingfisher`. ### Removing legacy deprecated code All 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. If you are curious about what are exactly removed, check [these commits](https://github.com/onevcat/Kingfisher/pull/1525/files). ================================================ FILE: Sources/Documentation.docc/MigrationGuide/Migration-To-7.md ================================================ # Migrating from v6 to v7 Migrating Kingfisher from version 6 to version 7. ## Overview Kingfisher 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. ### Deploy target The UIKit/AppKit part of Kingfisher now supports from: - iOS 12.0 - macOS 10.14 - tvOS 12.0 - watchOS 5.0 > 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. The SwiftUI part of Kingfisher now supports from - iOS 14 - macOS 11.0 - tvOS 14.0 - watchOS 7.0 > 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. ### Migration Steps The main breaking changes happens to the SwiftUI support. By following the steps you should be able to migrate to the new version. - 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. - 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. - 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(_:)``. - All of the `isLoaded` parameter are also removed from the chain-able ``KF`` shorthand. - 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. ================================================ FILE: Sources/Documentation.docc/MigrationGuide/Migration-To-8.md ================================================ # Migrating from v7 to v8 This guide assists you in updating Kingfisher from version 7 to version 8. ## Overview Kingfisher 8.0 introduces breaking changes from its predecessor. This document highlights the major updates and significant API modifications. ## Deployment Target Starting with Kingfisher 8.0, the minimum supported versions are: - iOS 13.0 - macOS 10.15 - tvOS 13.0 - watchOS 6.0 - visionOS 1.0 ## Migration Steps and Insights First, ensure there are no existing warnings from Kingfisher. Several deprecated methods and properties have been removed in version 8. For the breaking changes, review the sections below for any utilized features and symbols. ### MainActor Requirement As 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: ```swift class Foo { func bar() { UIImageView().kf.setImage(with: URL(string: "https://example.com/image.png")) } } ``` > warning: > > Call to main actor-isolated instance method 'setImage(with:placeholder:options:completionHandler:)' in a synchronous nonisolated context. Try to limit the access to the `MainActor`. For example, add the `MainActor` attribute to the method: ```swift class Foo { @MainActor func bar() { UIImageView().kf.setImage(with: URL(string: "https://example.com/image.png")) } } ``` The 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. ### Disk Cache Changes Version 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. ### Swift Concurrency APIs Kingfisher 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. Ensure your implementations conform to these changes. #### `ImageDownloadRedirectHandler` Protocol The `handleHTTPRedirection(for:response:newRequest:completionHandler:)` method has been replaced with an asynchronous counterpart. Update your implementation accordingly: ```swift // Old extension YourType: ImageDownloadRedirectHandler { func handleHTTPRedirection( for task: Kingfisher.SessionDataTask, response: HTTPURLResponse, newRequest: URLRequest, completionHandler: @escaping (URLRequest?) -> Void ) { // Do something with the result, potentially in async way requestUpdater.update(newRequest) { result in completionHandler(result) } } } ``` ```swift // New extension YourType: ImageDownloadRedirectHandler { func handleHTTPRedirection( for task: Kingfisher.SessionDataTask, response: HTTPURLResponse, newRequest: URLRequest ) async -> URLRequest? { let result = await requestUpdater.update(newRequest) return result } } ``` #### `AsyncImageDownloadRequestModifier` Protocol The `modified(for:reportModified:)` method is now asynchronous. Reimplement it if used: ```swift // Old extension YourType: AsyncImageDownloadRequestModifier { func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) { reportModified(request) } } ``` ```swift // New extension YourType: AsyncImageDownloadRequestModifier { func modified(for request: URLRequest) async -> URLRequest? { return request } } ``` #### `AuthenticationChallengeResponsible` Protocol The following methods have been updated to async versions: - `downloader(_:didReceive:completionHandler:)` - `downloader(_:task:didReceive:completionHandler:)` Ensure your implementation is current: ```swift // Old extension YourType: AuthenticationChallengeResponsible { func downloader( _ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { generateCredential { credential in completionHandler(.useCredential, credential) } } func downloader( _ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { generateCredential { credential in completionHandler(.useCredential, credential) } } } ``` ```swift // New extension YourType: AuthenticationChallengeResponsible { func downloader( _ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { let credential = await generateCredential() return (.useCredential, credential) } func downloader( _ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { let credential = await generateCredential() return (.useCredential, credential) } } ``` ### Type Adjustments #### `ColorElement` `Filter.ColorElement` has evolved from a typealias for a tuple to a `struct`. Instantiate `ColorElement` using its initializer: ```swift let brightness, contrast, saturation, inputEV: CGFloat // Old let colorElement: Filter.ColorElement = (brightness, contrast, saturation, inputEV) // New let colorElement = Filter.ColorElement(brightness, contrast, saturation, inputEV) ``` #### `DownloadTask` `DownloadTask` has been redefined as a `class` instead of a `struct`. For `ImageDownloader.download` methods that previously returned optional `DownloadTask`` values, now return non-optional values instead. For example: ```swift // old open func downloadImage( with url: URL, options: KingfisherParsedOptionsInfo, completionHandler: (@Sendable (Result) -> Void)? = nil ) -> DownloadTask? // new open func downloadImage( with url: URL, options: KingfisherParsedOptionsInfo, completionHandler: (@Sendable (Result) -> Void)? = nil ) -> DownloadTask ``` To check if a download task is valid, instead of checking `nil`, use `isInitialized` instead: ```swift // old let downloadTask: DownloadTask? = downloader.downloadImage(with: url, options: options) func doSomethingWithTask() { if let task = downloadTask { // Do something with the task, for example, cancel it } } // new let downloadTask: DownloadTask = downloader.downloadImage(with: url, options: options) func doSomethingWithTask() { if downloadTask.isInitialized { // Do something with the task, for example, cancel it } } ``` ##### Cancel Token of DownloadTask In 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. Typically, there is no need to interact directly with the cancel token; you can simply invoke the `cancel()` method to terminate an ongoing download task. ================================================ FILE: Sources/Documentation.docc/MigrationGuide.md ================================================ # Migration Guide How to migrate from an earlier version of Kingfisher to the latest one. @Links(visualStyle: list) { - } ### Archived If you are still using an even earlier version, check the archived guide below and follow them to migrate to v7 first. @Links(visualStyle: list) { - - } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-SampleCell-1.swift ================================================ import UIKit class SampleCell: UITableViewCell { } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-SampleCell-2.swift ================================================ import UIKit class SampleCell: UITableViewCell { var sampleImageView: UIImageView = { let imageView = UIImageView(frame: .zero) imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-SampleCell-3.swift ================================================ import UIKit class SampleCell: UITableViewCell { var sampleImageView: UIImageView = { let imageView = UIImageView(frame: .zero) imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() var sampleLabel: UILabel = { let label = UILabel(frame: .zero) label.translatesAutoresizingMaskIntoConstraints = false return label }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(sampleImageView) NSLayoutConstraint.activate([ sampleImageView.widthAnchor.constraint(equalToConstant: 64), sampleImageView.heightAnchor.constraint(equalToConstant: 64), sampleImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12), sampleImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) contentView.addSubview(sampleLabel) NSLayoutConstraint.activate([ sampleLabel.leadingAnchor.constraint(equalTo: sampleImageView.trailingAnchor, constant: 12), sampleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-1.swift ================================================ import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-10.swift ================================================ override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(KingfisherManager.shared) tableView.dataSource = self view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) ]) } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-11.swift ================================================ override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(KingfisherManager.shared) tableView.dataSource = self view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) ]) DispatchQueue.main.asyncAfter(deadline: .now() + 5) { KingfisherManager.shared.cache.calculateDiskStorageSize { result in switch result { case .success(let size): print("Size: \(Double(size) / 1024 / 1024) MB") case .failure(let error): print("Some error: \(error)") } } } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-12.swift ================================================ override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(KingfisherManager.shared) tableView.dataSource = self view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) ]) DispatchQueue.main.asyncAfter(deadline: .now() + 5) { KingfisherManager.shared.cache.calculateDiskStorageSize { result in switch result { case .success(let size): let sizeInMB = Double(size) / 1024 / 1024 let alert = UIAlertController(title: nil, message: String(format: "Kingfisher Disk Cache: %.2fMB", sizeInMB), preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Purge", style: .destructive) { _ in }) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) self.present(alert, animated: true) case .failure(let error): print("Some error: \(error)") } } } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-13.swift ================================================ override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(KingfisherManager.shared) tableView.dataSource = self view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) ]) DispatchQueue.main.asyncAfter(deadline: .now() + 5) { KingfisherManager.shared.cache.calculateDiskStorageSize { result in switch result { case .success(let size): let sizeInMB = Double(size) / 1024 / 1024 let alert = UIAlertController(title: nil, message: String(format: "Kingfisher Disk Cache: %.2fMB", sizeInMB), preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Purge", style: .destructive) { _ in KingfisherManager.shared.cache.clearCache { self.tableView.reloadData() } }) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) self.present(alert, animated: true) case .failure(let error): print("Some error: \(error)") } } } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-2.swift ================================================ import UIKit import Kingfisher class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(KingfisherManager.shared) } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-3.swift ================================================ import UIKit import Kingfisher class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(KingfisherManager.shared) } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-4.swift ================================================ import UIKit import Kingfisher class ViewController: UIViewController { lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero) tableView.register(SampleCell.self, forCellReuseIdentifier: "SampleCell") tableView.translatesAutoresizingMaskIntoConstraints = false tableView.rowHeight = 80 return tableView }() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(KingfisherManager.shared) } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-5.swift ================================================ import UIKit import Kingfisher class ViewController: UIViewController { lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero) tableView.register(SampleCell.self, forCellReuseIdentifier: "SampleCell") tableView.translatesAutoresizingMaskIntoConstraints = false tableView.rowHeight = 80 return tableView }() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(KingfisherManager.shared) tableView.dataSource = self view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) ]) } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell cell.sampleLabel.text = "Index \(indexPath.row)" cell.sampleImageView.backgroundColor = .lightGray return cell } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-6-0.swift ================================================ extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell cell.sampleLabel.text = "Index \(indexPath.row)" cell.sampleImageView.backgroundColor = .lightGray return cell } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-6.swift ================================================ extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell cell.sampleLabel.text = "Index \(indexPath.row)" let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" let url = URL(string: "\(urlPrefix)-1.jpg") cell.sampleImageView.kf.setImage(with: url) cell.sampleImageView.backgroundColor = .lightGray return cell } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-7.swift ================================================ extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 10 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell cell.sampleLabel.text = "Index \(indexPath.row)" let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" let url = URL(string: "\(urlPrefix)-\(indexPath.row + 1).jpg") cell.sampleImageView.kf.setImage(with: url) cell.sampleImageView.backgroundColor = .lightGray return cell } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-8.swift ================================================ extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 10 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell cell.sampleLabel.text = "Index \(indexPath.row)" let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" let url = URL(string: "\(urlPrefix)-\(indexPath.row + 1).jpg") cell.sampleImageView.kf.indicatorType = .activity let roundCorner = RoundCornerImageProcessor(radius: .widthFraction(0.5), roundingCorners: [.topLeft, .bottomRight]) let pngSerializer = FormatIndicatedCacheSerializer.png cell.sampleImageView.kf.setImage( with: url, options: [.processor(roundCorner), .cacheSerializer(pngSerializer)] ) cell.sampleImageView.backgroundColor = .clear return cell } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/01-ViewController-9.swift ================================================ // cell.sampleImageView.kf.setImage(with: url, options: [.processor(roundCorner)]) cell.sampleImageView.kf.setImage(with: url, options: [.processor(roundCorner)]) { result in switch result { case .success(let imageResult): print("Image loaded from cache: \(imageResult.cacheType)") case .failure(let error): print("Error: \(error)") } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-1.swift ================================================ import SwiftUI struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } .padding() } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-10.swift ================================================ @State var showAlert = false @State var cacheSizeResult: Result? = nil var body: some View { List { Button("Check Cache") { KingfisherManager.shared.cache.calculateDiskStorageSize { result in cacheSizeResult = result showAlert = true } } .alert( "Disk Cache", isPresented: $showAlert, presenting: cacheSizeResult, actions: { result in // TODO: Actions }, message: { result in switch result { case .success(let size): Text("Size: \(Double(size) / 1024 / 1024) MB") case .failure(let error): Text(error.localizedDescription) } }) ForEach(0 ..< 10) { i in HStack { KFImage(url(at: i)) // ... } } }.listStyle(.plain) } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-11.swift ================================================ @State var showAlert = false @State var cacheSizeResult: Result? = nil var body: some View { List { Button("Check Cache") { KingfisherManager.shared.cache.calculateDiskStorageSize { result in cacheSizeResult = result showAlert = true } } .alert( "Disk Cache", isPresented: $showAlert, presenting: cacheSizeResult, actions: { result in switch result { case .success: Button("Clear") { KingfisherManager.shared.cache.clearCache() } Button("Cancel", role: .cancel) {} case .failure: Button("OK") { } } }, message: { result in switch result { case .success(let size): Text("Size: \(Double(size) / 1024 / 1024) MB") case .failure(let error): Text(error.localizedDescription) } }) ForEach(0 ..< 10) { i in HStack { KFImage(url(at: i)) // ... } } }.listStyle(.plain) } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-2.swift ================================================ import SwiftUI import Kingfisher struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } .padding() .onAppear { print(KingfisherManager.shared) } } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-3.swift ================================================ import SwiftUI import Kingfisher struct ContentView: View { var body: some View { List { ForEach(0 ..< 10) { i in HStack { Rectangle().fill(Color.gray) .frame(width: 64, height: 64) Text("Index \(i)") } } }.listStyle(.plain) } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-4.swift ================================================ import SwiftUI import Kingfisher struct ContentView: View { func url(at index: Int) -> URL? { let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" return URL(string: "\(urlPrefix)-\(index + 1).jpg") } var body: some View { List { ForEach(0 ..< 10) { i in HStack { KFImage(url(at: i)) .resizable() .frame(width: 64, height: 64) Text("Index \(i)") } } }.listStyle(.plain) } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-5.swift ================================================ import SwiftUI import Kingfisher struct ContentView: View { func url(at index: Int) -> URL? { let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" return URL(string: "\(urlPrefix)-\(index + 1).jpg") } var body: some View { List { ForEach(0 ..< 10) { i in HStack { KFImage(url(at: i)) .resizable() .roundCorner( radius: .widthFraction(0.5), roundingCorners: [.topLeft, .bottomRight] ) .serialize(as: .PNG) .frame(width: 64, height: 64) Text("Index \(i)") } } }.listStyle(.plain) } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-6.swift ================================================ import SwiftUI import Kingfisher struct ContentView: View { func url(at index: Int) -> URL? { let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" return URL(string: "\(urlPrefix)-\(index + 1).jpg") } var body: some View { List { ForEach(0 ..< 10) { i in HStack { KFImage(url(at: i)) .resizable() .roundCorner( radius: .widthFraction(0.5), roundingCorners: [.topLeft, .bottomRight] ) .serialize(as: .PNG) .onSuccess { result in print("Image loaded from cache: \(result.cacheType)") } .onFailure { error in print("Error: \(error)") } .frame(width: 64, height: 64) Text("Index \(i)") } } }.listStyle(.plain) } } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-7.swift ================================================ var body: some View { List { ForEach(0 ..< 10) { i in HStack { KFImage(url(at: i)) // ... } } }.listStyle(.plain) } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-8.swift ================================================ var body: some View { List { Button("Check Cache") { KingfisherManager.shared.cache.calculateDiskStorageSize { result in switch result { case .success(let size): print("Size: \(Double(size) / 1024 / 1024) MB") case .failure(let error): print("Some error: \(error)") } } } ForEach(0 ..< 10) { i in HStack { KFImage(url(at: i)) // ... } } }.listStyle(.plain) } ================================================ FILE: Sources/Documentation.docc/Resources/code-files/02-ContentView-9.swift ================================================ @State var showAlert = false @State var cacheSizeResult: Result? = nil var body: some View { List { Button("Check Cache") { KingfisherManager.shared.cache.calculateDiskStorageSize { result in cacheSizeResult = result showAlert = true } } ForEach(0 ..< 10) { i in HStack { KFImage(url(at: i)) // ... } } }.listStyle(.plain) } ================================================ FILE: Sources/Documentation.docc/Topics/Topic_ImageDataProvider.md ================================================ # Understanding the ImageDataProvider Loading a local image or loading from data. ## Overview Kingfisher supports setting images from a local data source, allowing you to leverage its features for processing and managing local image data, bypassing the need for network downloads. This allows for uniform API calls for both remote and local images, facilitating the reuse of familiar concepts, such as existing processors and cache serializers. ### Image from local file ``LocalFileImageDataProvider`` is a type that conforms to ``ImageDataProvider``. It is specifically designed for loading images from local file URLs: ```swift let url = URL(fileURLWithPath: path) let provider = LocalFileImageDataProvider(fileURL: url) imageView.kf.setImage(with: provider) ``` You can also pass options to it: ```swift let processor = RoundCornerImageProcessor(cornerRadius: 20) imageView.kf.setImage(with: provider, options: [.processor(processor)]) ``` ### Image from Base64 string Utilize ``Base64ImageDataProvider`` to supply an image from base64 encoded string. All standard features, including caching and image processing, function identically to how they operate when retrieving images via a URL. ```swift let provider = Base64ImageDataProvider(base64String: "\/9j\/4AAQSkZJRgABAQA...", cacheKey: "some-cache-key") imageView.kf.setImage(with: provider) ``` ### Generating image from AVAsset Employ ``AVAssetImageDataProvider`` to create an image from a video URL or `AVAsset` at a specified time, leveraging Kingfisher's capabilities for handling video-based image sources. ```swift let provider = AVAssetImageDataProvider( assetURL: URL(string: "https://example.com/your_video.mp4")!, seconds: 15.0 ) ``` ### Creating a customize ``ImageDataProvider`` To create your own image data provider, implement the ``ImageDataProvider`` protocol. This requires implementing a ``ImageDataProvider/cacheKey`` for unique identification and a ``ImageDataProvider/data(handler:)`` method to supply image data: ```swift struct UserNameLetterIconImageProvider: ImageDataProvider { var cacheKey: String { return letter } let letter: String init(userNameFirstLetter: String) { self.letter = userNameFirstLetter } func data(handler: @escaping (Result) -> Void) { // You can ignore these detail below. // It generates some data for an image with `letter` being rendered in the center. let rect = CGRect(x: 0, y: 0, width: 250, height: 250) let renderer = UIGraphicsImageRenderer(size: rect.size) let data = renderer.pngData { context in UIColor.systemYellow.setFill() context.fill(rect) let attributes = [ NSAttributedString.Key.foregroundColor: UIColor.white, .font: UIFont.systemFont(ofSize: 200) ] let textSize = letter.size(withAttributes: attributes) let textRect = CGRect( x: (rect.width - textSize.width) / 2, y: (rect.height - textSize.height) / 2, width: textSize.width, height: textSize.height) letter.draw(in: textRect, withAttributes: attributes) } // Provide the image data in `handler`. handler(.success(data)) } } // Set image for user "John" let provider = UserNameLetterIconImageProvider(userNameFirstLetter: "J") imageView.kf.setImage( with: provider, options: [.processor(RoundCornerImageProcessor(radius: .point(75)))] ) ``` This generates a result like: @Image(source: imagedataprovider-sample) You might have noticed that ``ImageDataProvider/data(handler:)`` includes a callback. This allows you to supply the image data asynchronously from a different thread, which is useful if processing on the main thread is too heavy. ================================================ FILE: Sources/Documentation.docc/Topics/Topic_Indicator.md ================================================ # Loading Indicator Setting and customizing indicator while loading. #### Using the standard indicator ```swift imageView.kf.indicatorType = .activity imageView.kf.setImage(with: url) ``` #### Using an image as indicator ```swift let path = Bundle.main.path(forResource: "loader", ofType: "gif")! let data = try! Data(contentsOf: URL(fileURLWithPath: path)) imageView.kf.indicatorType = .image(imageData: data) imageView.kf.setImage(with: url) ``` #### Using a customized view ```swift struct MyIndicator: Indicator { let view: UIView = UIView() func startAnimatingView() { view.isHidden = false } func stopAnimatingView() { view.isHidden = true } init() { view.backgroundColor = .red } } let i = MyIndicator() imageView.kf.indicatorType = .custom(indicator: i) ``` #### Updating indicator with percentage progress ```swift imageView.kf.setImage(with: url, progressBlock: { receivedSize, totalSize in let percentage = (Float(receivedSize) / Float(totalSize)) * 100.0 print("downloading progress: \(percentage)%") myIndicator.percentage = percentage }) ``` The `progressBlock` is called only when the server's response includes a "Content-Length" in the header. ================================================ FILE: Sources/Documentation.docc/Topics/Topic_LivePhoto.md ================================================ # Loading Live Photos Load and cache Live Photos from network sources using Kingfisher. ## Overview Kingfisher 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. ## Live Photo Data Preparation Before 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`. If 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. Assuming you have a valid `PHAsset` from the Photos framework, here's a sample of how to extract its data: ```swift let asset: PHAsset = // ... your PHAsset if !asset.mediaSubtypes.contains(.photoLive) { print("Not a live photo") return } let resources = PHAssetResource.assetResources(for: asset) var allData = [Data]() let group = DispatchGroup() group.notify(queue: .main) { allData.forEach { data in // Upload data to your server serverRequest.upload(data) } } resources.forEach { resource in group.enter() var data = Data() PHAssetResourceManager.default().requestData(for: resource, options: nil) { chunk in data.append(chunk) } completionHandler: { error in defer { group.leave() } if let error = error { print("Error: \(error)") return } allData.append(data) } } ``` Important notes: - This is a basic example showing how to retrieve data from a live photo asset. - 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. - Do not modify the metadata or actual data of the resources, as this may cause problems when loading in Kingfisher later. - 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. - You can use [`PHAssetResource.originalFilename`](https://developer.apple.com/documentation/photokit/phassetresource/1623985-originalfilename) to get and preserve the original file extension. ## Loading Live Photos ### Step 1: Import Required Frameworks and Set Up PHLivePhotoView ```swift import Kingfisher import PhotosUI let livePhotoView = PHLivePhotoView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) view.addSubview(livePhotoView) ``` ### Step 2: Prepare URLs ```swift let imageURL = URL(string: "https://example.com/image.heic")! let videoURL = URL(string: "https://example.com/video.mov")! let urls = [imageURL, videoURL] ``` ### Step 3: Load the Live Photo ```swift livePhotoView.kf.setImage(with: urls) { result in switch result { case .success(let retrieveResult): print("Live photo loaded: \(retrieveResult.livePhoto)") print("Cache type: \(retrieveResult.loadingInfo.cacheType)") case .failure(let error): print("Error: \(error)") } } ``` The loaded live photo will be stored in the disk cache of Kingfisher to boost future loading requests. ## Notes - Verify that the provided URLs are valid and accessible. - Loading may take time, especially for resources fetched over the network. - Certain `KingfisherOptionsInfo` options, such as custom processors, are not supported for Live Photos. - 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. ## Conclusion By 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. ================================================ FILE: Sources/Documentation.docc/Topics/Topic_LowDataMode.md ================================================ # Low Data Mode Loading image and customizing behaviors for the Low Data Mode. ## Overview Starting with iOS 13, Apple has introduced the option for users to enable [Low Data Mode](https://support.apple.com/en-us/102433) to reduce cellular and Wi-Fi data usage. To accommodate this setting, you can offer an alternative version of your image, typically in lower resolution. Kingfisher will automatically switch to this version when Low Data Mode is activated, helping to conserve data. ```swift imageView.kf.setImage( with: highResolutionURL, options: [.lowDataSource(.network(lowResolutionURL)] ) ``` In the scenario described, if the user has not applied any network restrictions, the `highResolutionURL` will be utilized for fetching the image. However, if the device is in Low Data Mode and the `highResolutionURL` version is not found in the cache, the `lowResolutionURL` will be selected as the fallback option to save data. Given that the `.lowDataSource` option accepts any `Source` parameter, not just a URL, you have the flexibility to pass in a local image provider. This approach effectively eliminates the need for a downloading task, allowing for the use of locally stored images when operating under Low Data Mode or other restrictive network conditions. ```swift imageView.kf.setImage( with: highResolutionURL, options: [ .lowDataSource( .provider(LocalFileImageDataProvider(fileURL: localFileURL)) ) ] ) ``` > For more about this topic, check and ``ImageDataProvider`` documentation. > tip: If the `.lowDataSource` option is not specified, the `highResolutionURL` will be used by default, regardless of > the Low Data Mode setting on the device. ================================================ FILE: Sources/Documentation.docc/Topics/Topic_PerformanceTips.md ================================================ # Performance Tips Some useful tips for better performance when using Kingfisher. ### Cancelling unnecessary downloading tasks Once a download task is initiated, it will continue until completion, even if you set a different URL to the image view. ```swift imageView.kf.setImage(with: url1) { result in // `result` is `.failure(.imageSettingError(.notCurrentSourceTask))` // due to another `setImage` below. // // But the download (and cache) is done normally. } // Set again immediately. imageView.kf.setImage(with: url2) { result in // `result` is `.success` } ``` Even if the setting for `url1` ends in a `.failure` because it was overridden by `url2`, the download task itself completes. The downloaded image data is processed and cached accordingly. The download and caching of the image at `url1` consume network resources, CPU time, memory, and battery. If there's a likelihood the image from `url1` will be displayed to the user again, these resources are well spent. If you are certain that the image from `url1` is no longer needed, cancelling the download before initiating another one can be a better idea: ```swift imageView.kf.setImage(with: url1) { result in // `result` is `.failure(.requestError(.taskCancelled))` // Now the download task is cancelled. } imageView.kf.cancelDownloadTask() imageView.kf.setImage(with: url2) { result in // `result` is `.success` } ``` This approach is particularly useful in table views or collection views. When users scroll through the list quickly, many image downloading tasks may be initiated. To optimize performance, you can cancel unnecessary tasks using the `didEndDisplaying` delegate method. ```swift func collectionView( _ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { // This will cancel the unfinished downloading task when the cell disappearing. cell.imageView.kf.cancelDownloadTask() } ``` ### Cache original image when using a processor If your goal is to either: 1. Use different processors on the same image to obtain various versions. 2. Apply a non-default processor to an image and later display the original. Consider using the ``KingfisherOptionsInfoItem/cacheOriginalImage`` option. This option not only caches the processed image but also stores the original downloaded image in the cache. ```swift let p1 = MyProcessor() imageView.kf.setImage(with: url, options: [.processor(p1), .cacheOriginalImage]) ``` Both the image processed by `p1` and the original downloaded image are cached. Later, when processing with another processor: ```swift let p2 = AnotherProcessor() imageView.kf.setImage(with: url, options: [.processor(p2)]) ``` Kingfisher is clear enough to verify that the original image for the URL is cached. Instead of downloading the image again, Kingfisher will reuse the original image and apply `p2` to it directly. ### Downsampling the excessively high resolution images In scenarios where you need to display large images in a table view or collection view cell, it's optimal to use smaller thumbnails to decrease download times and memory usage. However, if your server doesn't provide thumbnails, the ``DownsamplingImageProcessor`` comes to the rescue. It downsamples high-resolution images to a specified size before they're loaded into memory, effectively optimizing performance: ```swift imageView.kf.setImage( with: resource, placeholder: placeholderImage, options: [ .processor(DownsamplingImageProcessor(size: imageView.size)), .scaleFactor(UIScreen.main.scale), .cacheOriginalImage ]) ``` ``DownsamplingImageProcessor`` is commonly used alongside ``KingfisherOptionsInfoItem/scaleFactor(_:)`` and ``KingfisherOptionsInfoItem/cacheOriginalImage`` options. This combination ensures images are scaled appropriately for your UI's pixel density while also caching the original high-resolution image to avoid future downloads, providing an efficient balance between image quality and resource utilization. ================================================ FILE: Sources/Documentation.docc/Topics/Topic_Prefetch.md ================================================ # Prefetching images before actually loading Preloading images before actually required. Feeding them to the table view or collection view to improve the display speed. ## Overview Use ``ImagePrefetcher`` to prefetch and cache images that are likely to be displayed later. This improves loading times and ensures smoother image display. ### Prefetch some images ```swift let urls = [ "https://example.com/image1.jpg", "https://example.com/image2.jpg" ].map { URL(string: $0)! } let prefetcher = ImagePrefetcher(urls: urls) { skippedResources, failedResources, completedResources in print("These resources are prefetched: \(completedResources)") } prefetcher.start() // Later when you need to display these images: imageView.kf.setImage(with: urls[0]) anotherImageView.kf.setImage(with: urls[1]) ``` ### Prefetch images for table view or collection view Starting with iOS 10, Apple introduced cell prefetching behavior, which can seamlessly integrate with Kingfisher's ``ImagePrefetcher``. ```swift override func viewDidLoad() { super.viewDidLoad() collectionView?.prefetchDataSource = self } extension ViewController: UICollectionViewDataSourcePrefetching { func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { let urls = indexPaths.flatMap { URL(string: $0.urlString) } ImagePrefetcher(urls: urls).start() } } ``` See [WWDC 16 - Session 219](https://developer.apple.com/videos/play/wwdc2016/219/) for more about changing of it in iOS 10. ================================================ FILE: Sources/Documentation.docc/Topics/Topic_Retry.md ================================================ # Retry the Image Loading Managing the retry mechanism when an error happens during loading. ## Overview Use ``KingfisherOptionsInfoItem/retryStrategy(_:)`` along with a `RetryStrategy` implementation to easily set up a retry mechanism for image setting operations when an error occurs. This combination allows you to define retry logic, including the number of retries and the conditions under which a retry should be attempted, ensuring a more resilient image loading process. ## Built-in Retry Strategies Kingfisher provides two built-in retry strategies to handle different scenarios: ### DelayRetryStrategy ``DelayRetryStrategy`` is a time-based retry strategy that allows you to specify the `maxRetryCount` and the `retryInterval` to easily configure retry behavior. This setup enables quick implementation of a retry mechanism: ```swift let retry = DelayRetryStrategy( maxRetryCount: 5, retryInterval: .seconds(3) ) imageView.kf.setImage(with: url, options: [.retryStrategy(retry)]) ``` This implements a retry mechanism that attempts to reload the target URL up to 5 times, with a fixed 3-second interval between each try. #### Other retry interval For a more dynamic approach, you can also select `.accumulated(3)` as the retry interval results in progressively increasing delays between attempts, specifically `3 -> 6 -> 9 -> 12 -> 15` seconds for each subsequent retry. Additionally, for ultimate flexibility, `.custom` allows you to define a unique pattern for retry intervals, tailoring the retry logic to your specific requirements. ### NetworkRetryStrategy ``NetworkRetryStrategy`` is a network-aware retry strategy that handles network connectivity issues. It only retries when the network becomes available after a disconnection, this is suitable to handle unstable user connection. ```swift // Basic usage - retries immediately when network becomes available let networkRetry = NetworkRetryStrategy() imageView.kf.setImage(with: url, options: [.retryStrategy(networkRetry)]) // With timeout - stops waiting after specified duration let networkRetryWithTimeout = NetworkRetryStrategy(timeoutInterval: 30.0) imageView.kf.setImage(with: url, options: [.retryStrategy(networkRetryWithTimeout)]) ``` ## Custom Retry Strategies If you need more control for the retry strategy, implement your own type that conforms to ``RetryStrategy``. ================================================ FILE: Sources/Documentation.docc/Tutorials/GettingStartedSwiftUI.tutorial ================================================ @Tutorial(time: 10) { @Intro(title: "Getting Started with Kingfisher (SwiftUI)") { Install Kingfisher and learn basic usage of the framework with SwiftUI. @Image(source: getting-started-card, alt: "Title image of the tutorial. A kingfisher bird standing on a tree.") } @Section(title: "Overview") { @ContentAndMedia { This tutorial guides you through building a SwiftUI `List` that displays rounded images of kingfisher birds, downloaded using the Kingfisher library. It includes: - Setting Up `List`: Quick setup for a basic list. - Using Kingfisher: Download and display bird images. - Image Processing: Convert images to rounded corners for display. - Cache Size Button: A feature to check cache usage. At the final stage of this tutorial, you will have a list like this: @Image(source:preview-3-swiftui.png, alt:"The first image is loaded into the image view in cell.") } @Steps { } } @Section(title: "Installing") { @ContentAndMedia { After creating your SwiftUI app, the first step is to install Kingfisher. For this, we use Swift Package Manager. > There are also other way to add Kingfisher to your project, such as CocoaPods or manually. Check the related documentation for more information. @Image(source: create-project-swiftui.png, alt: "") } @Steps { @Step { Choose "File" → "Add Package Dependencies…". In the pop-up window, enter the URL below to the search bar, and click the "Add Package" button. `https://github.com/onevcat/Kingfisher.git` @Image(source: add-dependency.png, alt: "Add Kingfisher as the dependency of your project.") } @Step { After downloading, add the `Kingfisher` library to your created project. @Image(source: add-to-project.png, alt: "") } @Step { 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. @Image(source: add-library-swiftui.png, alt: "") } @Step { To verify the installation. Choose "ContentView.swift" file. @Code(name: "ContentView.swift", file: 02-ContentView-1.swift) } @Step { Import `Kingfisher`. And try to print the `KingfisherManager.shared` in the `onAppear`. If you see something like "Kingfisher.KingfisherManager" in the Xcode debugger console, it means Kingfisher is ready in your project. @Code(name: "ContentView.swift", file: 02-ContentView-2.swift) } } } @Section(title: "Loading image with Kingfisher") { @ContentAndMedia { In this section, we will create a `List` and use Kingfisher to load some images from the network. } @Steps { @Step { Setting up a `List` in SwiftUI is easy. With the `ContentView` from the SwiftUI template. @Code(name: "ContentView.swift", file: 02-ContentView-2.swift) } @Step { Replace the `body` of the `ContentView` with a `List` and the embedded `ForEach`. @Code(name: "ContentView.swift", file: 02-ContentView-3.swift) { @Image(source: preview-1-swiftui.png, alt: "") } } @Step { To load an image from network, the easiest way is using the `KFImage` struct provided in Kingfisher. It accepts a URL, loads and shows the image when its `onAppear` is called. `KFImage` has a set of similar APIs to SwiftUI's `Image` type. Here we call `resizable()` to allow the image fit into the given frame. @Code(name: "ContentView.swift", file: 02-ContentView-4.swift) { @Image(source: preview-2-swiftui.png, alt: "") } } @Step { Kingfisher also comes with a bundle of useful processors and helper methods. For example, we can add some partial round corner effect in a simple way. @Code(name: "ContentView.swift", file: 02-ContentView-5.swift) { @Image(source:preview-3-swiftui.png, alt:"") } Besides of the `.roundCorner`, we also apply a `.serialize(as: .PNG)` to forcibly convert the loaded JPG file to PNG format when storing in the disk cache. This is necessary since JPG format does not contain an alpha channel, which is necessary when storing a round corner image. } @Step { The `KFImage` has a few other modifiers too, including some life cycle handlers like `.onSuccess` or `.onFailure`. @Code(name: "ContentView.swift", file: 02-ContentView-6.swift) Restart the app. If the images were loaded during your previous use of the app, you should see "Image loaded from cache: disk" in the Xcode console. } } } @Section(title: "Manipulating the Cache") { @ContentAndMedia { In this final part, we'll look at basic tasks related to image caching, like finding out the size of the disk cache and clearing all the cache. Usually, Kingfisher handles cache management automatically, so you don't need to think about it much. But if you need more detailed control over how caching works, this section will give you helpful tips and information. } @Steps { @Step { First, we need to find out how much space the image cache is using. Normally, you would check the cache size or clear the cache using a button. In this example, we will add a button to the first row of the list. @Code(name: "ContentView.swift", file: 02-ContentView-7.swift) } @Step { Add a `Button` at the top of the `List`. In its event block, call `calculateDiskStorageSize(completion:)` and print the disk cache size. @Code(name: "ContentView.swift", file: 02-ContentView-8.swift) { @Image(source:preview-4-swiftui.png, alt:"Added a button to the top of the list.") } } @Step { To trigger an alert in SwiftUI, we add two `@State` to the `ContentView`. In the `calculateDiskStorageSize` handler, we set both states. @Code(name: "ContentView.swift", file: 02-ContentView-9.swift) } @Step { Add an `.alert` modifier to the `Button`. When the `showAlert` state is set, an alert with the information of disk cache size is presented. @Code(name: "ContentView.swift", file: 02-ContentView-10.swift) } @Step { Lastly, use the `clearCache` function to remove all images from both the memory and disk caches. After this, when you restart the app or trigger a full reloading for the `List`, the images will be downloaded again from the internet. You'll notice "Image loaded from cache: none" displayed in the console, indicating the images are not being loaded from the cache this time. @Code(name: "ContentView.swift", file: 02-ContentView-11.swift) { @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.") } 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. } } } @Section(title: "Next Steps") { @ContentAndMedia { Congratulations! You have now mastered some basic uses of Kingfisher: including loading images from the web or cache using the `KFImage` type, processing images before display using a modifier, and basic methods for inspecting and clearing the cache. Kingfisher also contains a considerable number of other features, and it has been designed to be simple to use while considering flexibility. As you deepen your understanding of the framework, we hope you will gradually grow to like it. Next, we recommend that you start using Kingfisher in your projects to help you accomplish tasks. You can also read the and its related articles to get a better understanding. When you encounter problems, come back to consult the documentation or ask the community. Have a nice day! } @Steps { } } } ================================================ FILE: Sources/Documentation.docc/Tutorials/GettingStartedUIKit.tutorial ================================================ @Tutorial(time: 15) { @Intro(title: "Getting Started with Kingfisher (UIKit)") { Install Kingfisher and learn basic usage of the framework with UIKit. @Image(source: "getting-started-card", alt: "Title image of the tutorial. A kingfisher bird standing on a tree.") } @Section(title: "Overview") { @ContentAndMedia { This tutorial guides you through building a UITableView list that displays rounded images of kingfisher birds, downloaded using the Kingfisher library. It includes: - Setting Up `UITableView`: Quick setup for a basic list. - Using Kingfisher: Download and display bird images. - Image Processing: Convert images to rounded corners for display. - Cache Size Button: A feature to check cache usage. At the final stage of this tutorial, you will have a list like this: @Image(source:preview-4.png, alt:"The first image is loaded into the image view in cell.") } @Steps { } } @Section(title: "Installing") { @ContentAndMedia { After creating your UIKit app, the first step is to install Kingfisher. For this, we use Swift Package Manager. > There are also other way to add Kingfisher to your project, such as CocoaPods or manually. Check the related documentation for more information. @Image(source: create-project.png, alt: "") } @Steps { @Step { Choose "File" → "Add Package Dependencies…". In the pop-up window, enter the URL below to the search bar, and click the "Add Package" button. `https://github.com/onevcat/Kingfisher.git` @Image(source: add-dependency.png, alt: "Add Kingfisher as the dependency of your project.") } @Step { After downloading, add the `Kingfisher` library to your created project. @Image(source: add-to-project.png, alt: "") } @Step { 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. @Image(source: add-library.png, alt: "") } @Step { To verify the installation. Choose "ViewController.swift" file. @Code(name: "ViewController.swift", file: 01-ViewController-1.swift) } @Step { Import `Kingfisher`. And try to print the `KingfisherManager.shared`. If you see something like "Kingfisher.KingfisherManager" in the Xcode debugger console, it means Kingfisher is ready in your project. @Code(name: "ViewController.swift", file: 01-ViewController-2.swift) } } } @Section(title: "Creating the Table View") { @ContentAndMedia { Creating and setting up `UITableView` is not the focus of this tutorial, as it does not involve Kingfisher. However, we will later use Kingfisher to manage images in the `UIImageView` within the table cells. @Image(source: preview-1.png, alt: "") } @Steps { @Step { Create a `SampleCell` file. We will use it as the cell type of the table view. @Code(name: "SampleCell.swift", file: 01-SampleCell-1.swift) } @Step { Add a `sampleImageView` to the class. It is the main target image view we are going to set later. @Code(name: "SampleCell.swift", file: 01-SampleCell-2.swift) } @Step { Add other necessary views and layout code. (Boring, just copy it!) @Code(name: "SampleCell.swift", file: 01-SampleCell-3.swift) } @Step { In the "ViewController.swift". @Code(name: "ViewController.swift", file: 01-ViewController-3.swift) } @Step { Add a `tableView` to the view controller. @Code(name: "ViewController.swift", file: 01-ViewController-4.swift) } @Step { Extend `ViewController` to conform the `UITableViewDataSource`. For the sake of simplicity, we will only return one cell at first. @Code(name: "ViewController.swift", file: 01-ViewController-5.swift) { @Image(source:preview-1.png, alt:"An iOS app with a list which contains a single cell.") } Run the app, now you should see a list which contains a single cell with a light grey placeholder and a text label. } } } @Section(title: "Loading image with Kingfisher") { @ContentAndMedia { Kingfisher simplifies the task of loading images from remote URLs. It also offers a range of user-friendly processors and helper methods. In this section, we will cover how to use these features to streamline common tasks. @Image(source: preview-4.png, alt: "") } @Steps { @Step { The simplest way to start loading a remote image into an image view in Kingfisher, is using the `kf` wrapper and its method. In the code above, we already have a `sampleImageView` in the cell. @Code(name: "ViewController.swift", file: 01-ViewController-6-0.swift) } @Step { To load the first image, call `kf.setImage(with:)` on the image view, with the desired URL. @Code(name: "ViewController.swift", file: 01-ViewController-6.swift) { @Image(source:preview-2.png, alt:"The first image is loaded into the image view in cell.") } Now, running the app again, you can see the image is already loaded and set to the image view. } @Step { To actually load the images based on the index, we can try to add more cells. We prepared 10 kingfisher images, let's change the item count to 10: @Code(name: "ViewController.swift", file: 01-ViewController-7.swift) { @Image(source:preview-3.png, alt:"The first image is loaded into the image view in cell.") } Kingfisher also downloads and caches these images. Now, even if you turn off the network of your iOS device (or the simulator), and restart the app, these images can be loaded from cache and still displayed. } @Step { Kingfisher also comes with a bundle of useful processors and helper methods. For example, we can add a loading indicator and some partial round corner effect easily. @Code(name: "ViewController.swift", file: 01-ViewController-8.swift) { @Image(source:preview-4.png, alt:"The first image is loaded into the image view in cell.") } Besides of the `RoundCornerImageProcessor`, we also apply a `pngSerializer` to forcibly convert the loaded JPG file to PNG format when storing in the disk cache. This is necessary since JPG format does not contain an alpha channel, which is necessary when storing a round corner image. } @Step { The `setImage(with:)` method accepts other parameters, including a completion handler too. Let us add some logs before we continue to the next section. @Code(name: "ViewController.swift", file: 01-ViewController-9.swift) Now, running the app again, in the console you can see some text like "Image loaded from cache: disk". This is because the images are already in the disk cache, and they are loaded from the disk locally. By scrolling the table view up and down and triggering the cell reuse, it should print things like "Image loaded from cache: memory", which indicates the images are already cache in memory. } } } @Section(title: "Manipulating the Cache") { @ContentAndMedia { In this final part, we'll look at basic tasks related to image caching, like finding out the size of the disk cache and clearing all the cache. Usually, Kingfisher handles cache management automatically, so you don't need to think about it much. But if you need more detailed control over how caching works, this section will give you helpful tips and information. } @Steps { @Step { First, we need to find out how much space the image cache is using. Normally, you would check the cache size or clear the cache using a button. But to keep things simple, we won't add any extra buttons to this example. @Code(name: "ViewController.swift", file: 01-ViewController-10.swift) } @Step { In the `viewDidLoad` method, we use the `asyncAfter` method on the `DispatchQueue.main` queue. There we start a process to calculate the current size of the disk cache. The size we get tells us how much disk space Kingfisher is using for caching images. @Code(name: "ViewController.swift", file: 01-ViewController-11.swift) } @Step { To make it clear, we can create an alert and display it to the user, with a button to clear the cache manually. @Code(name: "ViewController.swift", file: 01-ViewController-12.swift) { @Image(source:preview-5.png, alt:"An alert which shows the disk cache size used by Kingfisher, with a button to purge the cache.") } } @Step { Lastly, use the `clearCache` function to remove all images from both the memory and disk caches. After this, when you refresh the table view's data, the images will be downloaded again from the internet. You'll notice "Image loaded from cache: none" displayed in the console, indicating the images are not being loaded from the cache this time. @Code(name: "ViewController.swift", file: 01-ViewController-13.swift) 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. } } } @Section(title: "Next Steps") { @ContentAndMedia { Congratulations! You have now mastered some basic uses of Kingfisher: including loading images from the web or cache using the `UIImageView` extension, processing images before display using ``ImageProcessor``, and basic methods for inspecting and clearing the cache. Kingfisher also contains a considerable number of other features, and it has been designed to be simple to use while considering flexibility. As you deepen your understanding of the framework, we hope you will gradually grow to like it. Next, we recommend that you start using Kingfisher in your projects to help you accomplish tasks. You can also read the and its related articles to get a better understanding. When you encounter problems, come back to consult the documentation or ask the community. Have a nice day! } @Steps { } } } ================================================ FILE: Sources/Documentation.docc/Tutorials/Tutorials.tutorial ================================================ @Tutorials(name: "Kingfisher Tutorials") { @Intro(title: "Kingfisher Tutorials") { Getting started with Kingfisher by following a sample app. } @Chapter(name: "Getting Started with Kingfisher (UIKit)") { @Image(source: logo) Installs Kingfisher and basic usage of the framework with UIKit. @TutorialReference(tutorial: "doc:GettingStartedUIKit") } @Chapter(name: "Getting Started with Kingfisher (SwiftUI)") { @Image(source: logo) Installs Kingfisher and basic usage of the framework with SwiftUI. @TutorialReference(tutorial: "doc:GettingStartedSwiftUI") } } ================================================ FILE: Sources/Extensions/CPListItem+Kingfisher.swift ================================================ // // CPListItem+Kingfisher.swift // Kingfisher // // Created by Wayne Hartman on 2021-08-29. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(CarPlay) && !targetEnvironment(macCatalyst) import CarPlay @available(iOS 14.0, *) @MainActor extension KingfisherWrapper where Base: CPListItem { // MARK: Setting Image /// Sets an image to the image view with a source. /// /// - Parameters: /// - source: The `Source` object contains information about the image. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieved and set finished. /// - Returns: A task represents the image downloading. /// /// - Note: /// /// Internally, this method will use `KingfisherManager` to get the requested source /// Since this method will perform UI changes, you must call it from the main thread. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. /// @discardableResult public func setImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? [])) return setImage( with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets an image to the image view with a requested resource. /// /// - Parameters: /// - resource: The `Resource` object contains information about the image. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieved and set finished. /// - Returns: A task represents the image downloading. /// /// - Note: /// /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache /// or network. Since this method will perform UI changes, you must call it from the main thread. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. /// @discardableResult public func setImage( with resource: (any Resource)?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { return setImage( with: resource?.convertToSource(), placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler) } func setImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { var mutatingSelf = self return setImage( with: source, imageAccessor: ImagePropertyAccessor( setImage: { image, _ in /** * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, * which allows >=14.5 SDK to set a `nil` image. This compile check allows * newer SDK users to set the image to `nil`, while still allowing older SDK * users to compile the framework. */ #if compiler(>=5.4) self.base.setImage(image) #else if let image = image { self.base.setImage(image) } #endif }, getImage: { self.base.image } ), taskAccessor: TaskPropertyAccessor( setTaskIdentifier: { mutatingSelf.taskIdentifier = $0 }, getTaskIdentifier: { mutatingSelf.taskIdentifier }, setTask: { mutatingSelf.imageTask = $0 } ), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, completionHandler: completionHandler ) } // MARK: Cancelling Image /// Cancel the image download task bounded to the image view if it is running. /// Nothing will happen if the downloading has already finished. public func cancelDownloadTask() { imageTask?.cancel() } } @MainActor private var taskIdentifierKey: Void? @MainActor private var imageTaskKey: Void? // MARK: Properties @MainActor extension KingfisherWrapper where Base: CPListItem { public private(set) var taskIdentifier: Source.Identifier.Value? { get { let box: Box? = getAssociatedObject(base, &taskIdentifierKey) return box?.value } set { let box = newValue.map { Box($0) } setRetainedAssociatedObject(base, &taskIdentifierKey, box) } } private var imageTask: DownloadTask? { get { return getAssociatedObject(base, &imageTaskKey) } set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } } #endif ================================================ FILE: Sources/Extensions/HasImageComponent+Kingfisher.swift ================================================ // // KingfisherHasImageComponent+Kingfisher.swift // Kingfisher // // Created by JH on 2023/12/5. // // Copyright (c) 2023 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. public protocol KingfisherImageSettable: KingfisherCompatible { @MainActor func setImage( _ image: KFCrossPlatformImage?, options: KingfisherParsedOptionsInfo ) @MainActor func getImage() -> KFCrossPlatformImage? } public protocol KingfisherHasImageComponent: KingfisherImageSettable { @MainActor var image: KFCrossPlatformImage? { set get } } extension KingfisherHasImageComponent { @MainActor public func setImage(_ image: KFCrossPlatformImage?, options: KingfisherParsedOptionsInfo) { self.image = image } @MainActor public func getImage() -> KFCrossPlatformImage? { image } } #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit @available(macOS 13.0, *) extension NSComboButton: KingfisherHasImageComponent {} @available(macOS 13.0, *) extension NSColorWell: KingfisherHasImageComponent {} extension NSTableViewRowAction: KingfisherHasImageComponent {} extension NSMenuItem: KingfisherHasImageComponent {} extension NSPathControlItem: KingfisherHasImageComponent {} extension NSToolbarItem: KingfisherHasImageComponent {} extension NSTabViewItem: KingfisherHasImageComponent {} extension NSStatusItem: KingfisherHasImageComponent {} extension NSCell: KingfisherHasImageComponent {} #endif #if canImport(UIKit) && !os(watchOS) import UIKit @available(iOS 13.0, tvOS 13.0, *) extension UIAction: KingfisherHasImageComponent {} @available(iOS 13.0, tvOS 13.0, *) extension UICommand: KingfisherHasImageComponent {} extension UIBarItem: KingfisherHasImageComponent {} #endif #if canImport(WatchKit) import WatchKit extension WKInterfaceImage: KingfisherHasImageComponent { @MainActor public var image: KFCrossPlatformImage? { get { nil } set { setImage(newValue) } } } #endif #if canImport(TVUIKit) import TVUIKit extension TVMonogramView: KingfisherHasImageComponent {} #endif struct ImagePropertyAccessor: Sendable { let setImage: @Sendable @MainActor (ImageType?, KingfisherParsedOptionsInfo) -> Void let getImage: @Sendable @MainActor () -> ImageType? } struct TaskPropertyAccessor: Sendable { let setTaskIdentifier: @Sendable @MainActor (Source.Identifier.Value?) -> Void let getTaskIdentifier: @Sendable @MainActor () -> Source.Identifier.Value? let setTask: @Sendable @MainActor (DownloadTask?) -> Void } @MainActor extension KingfisherWrapper where Base: KingfisherImageSettable { // MARK: Setting Image /// Sets an image to the image view with a ``Source``. /// /// - Parameters: /// - source: The ``Source`` object that defines data information from the network or a data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// /// ```swift /// // Set image from a network source. /// let url = URL(string: "https://example.com/image.png")! /// imageView.kf.setImage(with: .network(url)) /// /// // Or set image from a data provider. /// let provider = LocalFileImageDataProvider(fileURL: fileURL) /// imageView.kf.setImage(with: .provider(provider)) /// ``` /// /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension /// methods. So the code above is equivalent to: /// /// ```swift /// imageView.kf.setImage(with: url) /// imageView.kf.setImage(with: provider) /// ``` /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setImage( with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets an image to the image view with a ``Source``. /// /// - Parameters: /// - source: The ``Source`` object that defines data information from the network or a data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// /// ```swift /// // Set image from a network source. /// let url = URL(string: "https://example.com/image.png")! /// imageView.kf.setImage(with: .network(url)) /// /// // Or set image from a data provider. /// let provider = LocalFileImageDataProvider(fileURL: fileURL) /// imageView.kf.setImage(with: .provider(provider)) /// ``` /// /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension /// methods. So the code above is equivalent to: /// /// ```swift /// imageView.kf.setImage(with: url) /// imageView.kf.setImage(with: provider) /// ``` /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: source, placeholder: placeholder, options: options, progressBlock: nil, completionHandler: completionHandler ) } /// Sets an image to the image view with a requested ``Resource``. /// /// - Parameters: /// - resource: The ``Resource`` object contains information about the resource. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// /// ```swift /// // Set image from a URL resource. /// let url = URL(string: "https://example.com/image.png")! /// imageView.kf.setImage(with: url) /// ``` /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with resource: (any Resource)?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: resource?.convertToSource(), placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets an image to the image view with a requested ``Resource``. /// /// - Parameters: /// - resource: The ``Resource`` object contains information about the resource. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// /// ```swift /// // Set image from a URL resource. /// let url = URL(string: "https://example.com/image.png")! /// imageView.kf.setImage(with: url) /// ``` /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with resource: (any Resource)?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: resource, placeholder: placeholder, options: options, progressBlock: nil, completionHandler: completionHandler ) } /// Sets an image to the image view with a ``ImageDataProvider``. /// /// - Parameters: /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with provider: (any ImageDataProvider)?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: provider.map { .provider($0) }, placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets an image to the image view with a ``ImageDataProvider``. /// /// - Parameters: /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with provider: (any ImageDataProvider)?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: provider, placeholder: placeholder, options: options, progressBlock: nil, completionHandler: completionHandler ) } func setImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: source, imageAccessor: ImagePropertyAccessor( setImage: { base.setImage($0, options: $1) }, getImage: { base.getImage() } ), taskAccessor: TaskPropertyAccessor( setTaskIdentifier: { var mutatingSelf = self mutatingSelf.taskIdentifier = $0 }, getTaskIdentifier: { self.taskIdentifier }, setTask: { task in var mutatingSelf = self mutatingSelf.imageTask = task } ), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, completionHandler: completionHandler ) } } @MainActor extension KingfisherWrapper { func setImage( with source: Source?, imageAccessor: ImagePropertyAccessor, taskAccessor: TaskPropertyAccessor, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { guard let source = source else { imageAccessor.setImage(placeholder, parsedOptions) taskAccessor.setTaskIdentifier(nil) completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil } var options = parsedOptions // Always set placeholder while there is no image/placeholder yet. #if os(watchOS) let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading #else let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading || imageAccessor.getImage() == nil #endif if usePlaceholderDuringLoading { imageAccessor.setImage(placeholder, options) } let issuedIdentifier = Source.Identifier.next() taskAccessor.setTaskIdentifier(issuedIdentifier) if let block = progressBlock { options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } let task = KingfisherManager.shared.retrieveImage( with: source, options: options, downloadTaskUpdated: { task in Task { @MainActor in taskAccessor.setTask(task) } }, progressiveImageSetter: { imageAccessor.setImage($0, options) }, referenceTaskIdentifierChecker: { issuedIdentifier == taskAccessor.getTaskIdentifier() }, completionHandler: { result in CallbackQueueMain.currentOrAsync { guard issuedIdentifier == taskAccessor.getTaskIdentifier() else { let reason: KingfisherError.ImageSettingErrorReason do { let value = try result.get() reason = .notCurrentSourceTask(result: value, error: nil, source: source) } catch { reason = .notCurrentSourceTask(result: nil, error: error, source: source) } let error = KingfisherError.imageSettingError(reason: reason) completionHandler?(.failure(error)) return } taskAccessor.setTask(nil) taskAccessor.setTaskIdentifier(nil) switch result { case .success(let value): imageAccessor.setImage(value.image, options) case .failure: if let image = options.onFailureImage { imageAccessor.setImage(image, options) } } completionHandler?(result) } } ) taskAccessor.setTask(task) return task } } // MARK: - Associated Object @MainActor private var taskIdentifierKey: Void? @MainActor private var imageTaskKey: Void? @MainActor extension KingfisherWrapper where Base: KingfisherImageSettable { // MARK: Properties public private(set) var taskIdentifier: Source.Identifier.Value? { get { let box: Box? = getAssociatedObject(base, &taskIdentifierKey) return box?.value } set { let box = newValue.map { Box($0) } setRetainedAssociatedObject(base, &taskIdentifierKey, box) } } private var imageTask: DownloadTask? { get { return getAssociatedObject(base, &imageTaskKey) } set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } /// Cancels the image download task of the image view if it is running. /// /// Nothing will happen if the downloading has already finished. public func cancelDownloadTask() { imageTask?.cancel() } } ================================================ FILE: Sources/Extensions/ImageView+Kingfisher.swift ================================================ // // ImageView+Kingfisher.swift // Kingfisher // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) #if os(macOS) import AppKit #else import UIKit #endif @MainActor extension KingfisherWrapper where Base: KFCrossPlatformImageView { // MARK: Setting Image /// Sets an image to the image view with a ``Source``. /// /// - Parameters: /// - source: The ``Source`` object that defines data information from the network or a data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// /// ```swift /// // Set image from a network source. /// let url = URL(string: "https://example.com/image.png")! /// imageView.kf.setImage(with: .network(url)) /// /// // Or set image from a data provider. /// let provider = LocalFileImageDataProvider(fileURL: fileURL) /// imageView.kf.setImage(with: .provider(provider)) /// ``` /// /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension /// methods. So the code above is equivalent to: /// /// ```swift /// imageView.kf.setImage(with: url) /// imageView.kf.setImage(with: provider) /// ``` /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with source: Source?, placeholder: (any Placeholder)? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setImage(with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler) } /// Sets an image to the image view with a ``Source``. /// /// - Parameters: /// - source: The ``Source`` object that defines data information from the network or a data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// /// ```swift /// // Set image from a network source. /// let url = URL(string: "https://example.com/image.png")! /// imageView.kf.setImage(with: .network(url)) /// /// // Or set image from a data provider. /// let provider = LocalFileImageDataProvider(fileURL: fileURL) /// imageView.kf.setImage(with: .provider(provider)) /// ``` /// /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension /// methods. So the code above is equivalent to: /// /// ```swift /// imageView.kf.setImage(with: url) /// imageView.kf.setImage(with: provider) /// ``` /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with source: Source?, placeholder: (any Placeholder)? = nil, options: KingfisherOptionsInfo? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: source, placeholder: placeholder, options: options, progressBlock: nil, completionHandler: completionHandler ) } /// Sets an image to the image view with a requested ``Resource``. /// /// - Parameters: /// - resource: The ``Resource`` object contains information about the resource. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// /// ```swift /// // Set image from a URL resource. /// let url = URL(string: "https://example.com/image.png")! /// imageView.kf.setImage(with: url) /// ``` /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with resource: (any Resource)?, placeholder: (any Placeholder)? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: resource?.convertToSource(), placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler) } /// Sets an image to the image view with a requested ``Resource``. /// /// - Parameters: /// - resource: The ``Resource`` object contains information about the resource. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// /// ```swift /// // Set image from a URL resource. /// let url = URL(string: "https://example.com/image.png")! /// imageView.kf.setImage(with: url) /// ``` /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with resource: (any Resource)?, placeholder: (any Placeholder)? = nil, options: KingfisherOptionsInfo? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: resource, placeholder: placeholder, options: options, progressBlock: nil, completionHandler: completionHandler ) } /// Sets an image to the image view with a ``ImageDataProvider``. /// /// - Parameters: /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with provider: (any ImageDataProvider)?, placeholder: (any Placeholder)? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: provider.map { .provider($0) }, placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler) } /// Sets an image to the image view with a ``ImageDataProvider``. /// /// - Parameters: /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with provider: (any ImageDataProvider)?, placeholder: (any Placeholder)? = nil, options: KingfisherOptionsInfo? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: provider, placeholder: placeholder, options: options, progressBlock: nil, completionHandler: completionHandler ) } func setImage( with source: Source?, placeholder: (any Placeholder)? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { var mutatingSelf = self guard let source = source else { mutatingSelf.placeholder = placeholder mutatingSelf.taskIdentifier = nil completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil } var options = parsedOptions let isEmptyImage = base.image == nil && self.placeholder == nil if !options.keepCurrentImageWhileLoading || isEmptyImage { // Always set placeholder while there is no image/placeholder yet. mutatingSelf.placeholder = placeholder } let maybeIndicator = indicator maybeIndicator?.startAnimatingView() let issuedIdentifier = Source.Identifier.next() mutatingSelf.taskIdentifier = issuedIdentifier if base.shouldPreloadAllAnimation() { options.preloadAllAnimationData = true } if let block = progressBlock { options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } let task = KingfisherManager.shared.retrieveImage( with: source, options: options, downloadTaskUpdated: { task in Task { @MainActor in mutatingSelf.imageTask = task } }, progressiveImageSetter: { self.base.image = $0 }, referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, completionHandler: { result in CallbackQueueMain.currentOrAsync { maybeIndicator?.stopAnimatingView() guard issuedIdentifier == self.taskIdentifier else { let reason: KingfisherError.ImageSettingErrorReason do { let value = try result.get() reason = .notCurrentSourceTask(result: value, error: nil, source: source) } catch { reason = .notCurrentSourceTask(result: nil, error: error, source: source) } let error = KingfisherError.imageSettingError(reason: reason) completionHandler?(.failure(error)) return } mutatingSelf.imageTask = nil mutatingSelf.taskIdentifier = nil switch result { case .success(let value): guard self.needsTransition(options: options, cacheType: value.cacheType) else { mutatingSelf.placeholder = nil self.base.image = value.image completionHandler?(result) return } self.makeTransition(image: value.image, transition: options.transition) { completionHandler?(result) } case .failure: if let image = options.onFailureImage { mutatingSelf.placeholder = nil self.base.image = image } completionHandler?(result) } } } ) mutatingSelf.imageTask = task return task } // MARK: Cancelling Downloading Task /// Cancels the image download task of the image view if it is running. /// /// Nothing will happen if the downloading has already finished. public func cancelDownloadTask() { imageTask?.cancel() } private func needsTransition(options: KingfisherParsedOptionsInfo, cacheType: CacheType) -> Bool { switch options.transition { case .none: return false #if os(macOS) case .fade: // Fade is only a placeholder for SwiftUI on macOS. return false #else default: if options.forceTransition { return true } if cacheType == .none { return true } return false #endif } } private func makeTransition(image: KFCrossPlatformImage, transition: ImageTransition, done: @escaping () -> Void) { #if !os(macOS) // Force hiding the indicator without transition first. UIView.transition( with: self.base, duration: 0.0, options: [], animations: { self.indicator?.stopAnimatingView() }, completion: { _ in var mutatingSelf = self mutatingSelf.placeholder = nil UIView.transition( with: self.base, duration: transition.duration, options: [transition.animationOptions, .allowUserInteraction], animations: { transition.animations?(self.base, image) }, completion: { finished in transition.completion?(finished) done() } ) } ) #else done() #endif } } // MARK: - Associated Object @MainActor private var taskIdentifierKey: Void? @MainActor private var indicatorKey: Void? @MainActor private var indicatorTypeKey: Void? @MainActor private var placeholderKey: Void? @MainActor private var imageTaskKey: Void? @MainActor extension KingfisherWrapper where Base: KFCrossPlatformImageView { // MARK: Properties public private(set) var taskIdentifier: Source.Identifier.Value? { get { let box: Box? = getAssociatedObject(base, &taskIdentifierKey) return box?.value } set { let box = newValue.map { Box($0) } setRetainedAssociatedObject(base, &taskIdentifierKey, box) } } /// Specifies which indicator type is going to be used. /// /// The default is ``IndicatorType/none``, which means no indicator will be shown while downloading. public var indicatorType: IndicatorType { get { return getAssociatedObject(base, &indicatorTypeKey) ?? .none } set { switch newValue { case .none: indicator = nil case .activity: indicator = ActivityIndicator() case .image(let data): indicator = ImageIndicator(imageData: data) case .custom(let anIndicator): indicator = anIndicator } setRetainedAssociatedObject(base, &indicatorTypeKey, newValue) } } /// Holds any type that conforms to the protocol ``Indicator``. /// /// The protocol `Indicator` has a `view` property that will be shown when loading an image. /// It will be `nil` if ``KingfisherWrapper/indicatorType`` is ``IndicatorType/none``. public private(set) var indicator: (any Indicator)? { get { let box: Box? = getAssociatedObject(base, &indicatorKey) return box?.value } set { // Remove previous if let previousIndicator = indicator { previousIndicator.view.removeFromSuperview() } // Add new if let newIndicator = newValue { // Set default indicator layout let view = newIndicator.view base.addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false view.centerXAnchor.constraint( equalTo: base.centerXAnchor, constant: newIndicator.centerOffset.x).isActive = true view.centerYAnchor.constraint( equalTo: base.centerYAnchor, constant: newIndicator.centerOffset.y).isActive = true switch newIndicator.sizeStrategy(in: base) { case .intrinsicSize: break case .full: view.heightAnchor.constraint(equalTo: base.heightAnchor, constant: 0).isActive = true view.widthAnchor.constraint(equalTo: base.widthAnchor, constant: 0).isActive = true case .size(let size): view.heightAnchor.constraint(equalToConstant: size.height).isActive = true view.widthAnchor.constraint(equalToConstant: size.width).isActive = true } newIndicator.view.isHidden = true } // Save in associated object // Wrap newValue with Box to workaround an issue that Swift does not recognize // and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872 setRetainedAssociatedObject(base, &indicatorKey, newValue.map(Box.init)) } } private var imageTask: DownloadTask? { get { return getAssociatedObject(base, &imageTaskKey) } set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } /// Represents the ``Placeholder`` used for this image view. /// /// A ``Placeholder`` will be shown in the view while it is downloading an image. public private(set) var placeholder: (any Placeholder)? { get { return getAssociatedObject(base, &placeholderKey) } set { if let previousPlaceholder = placeholder { previousPlaceholder.remove(from: base) } if let newPlaceholder = newValue { newPlaceholder.add(to: base) } else { base.image = nil } setRetainedAssociatedObject(base, &placeholderKey, newValue) } } } extension KFCrossPlatformImageView { @objc func shouldPreloadAllAnimation() -> Bool { return true } } #endif ================================================ FILE: Sources/Extensions/NSButton+Kingfisher.swift ================================================ // // NSButton+Kingfisher.swift // Kingfisher // // Created by Jie Zhang on 14/04/2016. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit @MainActor extension KingfisherWrapper where Base: NSButton { // MARK: Setting Image /// Sets an image to the button with a ``Source``. /// /// - Parameters: /// - source: The ``Source`` object that defines data information from the network or a data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setImage( with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets an image to the button with a ``Resource``. /// /// - Parameters: /// - resource: The ``Resource`` object that defines data information from the network or a data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with resource: (any Resource)?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setImage( with: resource?.convertToSource(), placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler) } func setImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { var mutatingSelf = self return setImage( with: source, imageAccessor: ImagePropertyAccessor( setImage: { image, _ in base.image = image }, getImage: { base.image }), taskAccessor: TaskPropertyAccessor( setTaskIdentifier: { mutatingSelf.taskIdentifier = $0 }, getTaskIdentifier: { mutatingSelf.taskIdentifier }, setTask: { mutatingSelf.imageTask = $0 }), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, completionHandler: completionHandler ) } // MARK: Cancelling Downloading Task /// Cancels the image download task of the button if it is running. /// Nothing will happen if the downloading has already finished. public func cancelImageDownloadTask() { imageTask?.cancel() } // MARK: Setting Alternate Image @discardableResult public func setAlternateImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setAlternateImage( with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets an alternate image to the button with a ``Resource``. /// /// - Parameters: /// - resource: The ``Resource`` object that defines data information from the network or a data provider. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI /// changes, it is your responsibility to call it from the main thread. /// /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setAlternateImage( with resource: (any Resource)?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { return setAlternateImage( with: resource?.convertToSource(), placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler) } func setAlternateImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { var mutatingSelf = self return setImage( with: source, imageAccessor: ImagePropertyAccessor( setImage: { image, _ in base.alternateImage = image }, getImage: { base.alternateImage }), taskAccessor: TaskPropertyAccessor( setTaskIdentifier: { mutatingSelf.alternateTaskIdentifier = $0 }, getTaskIdentifier: { mutatingSelf.alternateTaskIdentifier }, setTask: { mutatingSelf.alternateImageTask = $0 } ), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, completionHandler: completionHandler ) } // MARK: Cancelling Alternate Image Downloading Task /// Cancels the image download task of the image view if it is running. /// /// Nothing will happen if the downloading has already finished. public func cancelAlternateImageDownloadTask() { alternateImageTask?.cancel() } } // MARK: - Associated Object @MainActor private var taskIdentifierKey: Void? @MainActor private var imageTaskKey: Void? @MainActor private var alternateTaskIdentifierKey: Void? @MainActor private var alternateImageTaskKey: Void? @MainActor extension KingfisherWrapper where Base: NSButton { // MARK: Properties public private(set) var taskIdentifier: Source.Identifier.Value? { get { let box: Box? = getAssociatedObject(base, &taskIdentifierKey) return box?.value } set { let box = newValue.map { Box($0) } setRetainedAssociatedObject(base, &taskIdentifierKey, box) } } private var imageTask: DownloadTask? { get { return getAssociatedObject(base, &imageTaskKey) } set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } public private(set) var alternateTaskIdentifier: Source.Identifier.Value? { get { let box: Box? = getAssociatedObject(base, &alternateTaskIdentifierKey) return box?.value } set { let box = newValue.map { Box($0) } setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box) } } private var alternateImageTask: DownloadTask? { get { return getAssociatedObject(base, &alternateImageTaskKey) } set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)} } } #endif ================================================ FILE: Sources/Extensions/NSTextAttachment+Kingfisher.swift ================================================ // // NSTextAttachment+Kingfisher.swift // Kingfisher // // Created by Benjamin Briggs on 22/07/2019. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) #if os(macOS) import AppKit #else import UIKit #endif @MainActor extension KingfisherWrapper where Base: NSTextAttachment { // MARK: Setting Image /// Sets an image to the text attachment with a source. /// /// - Parameters: /// - source: The ``Source`` object that defines data information from the network or a data provider. /// - attributedView: The owner of the attributed string to which this `NSTextAttachment` is added. /// - placeholder: A placeholder to show while retrieving the image from the given `source`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the requested source. Since this method will /// perform UI changes, it is your responsibility of calling it from the main thread. /// /// The retrieved image will be set to the `NSTextAttachment.image` property. Because it is not an image view-based /// rendering, options related to the view, such as ``KingfisherOptionsInfoItem/transition(_:)``, are not supported. /// /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task is done. It gives the view a /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an /// attributed string with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. /// /// Here is a typical use case: /// /// ```swift /// let label: UILabel = // ... /// /// let textAttachment = NSTextAttachment() /// textAttachment.kf.setImage( /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, /// attributedView: label, /// options: [ /// .processor( /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) /// |> RoundCornerImageProcessor(cornerRadius: 15)) /// ] /// ) /// /// let attributedText = NSMutableAttributedString(string: "Hello World") /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) /// label.attributedText = attributedText /// ``` @discardableResult public func setImage( with source: Source?, attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setImage( with: source, attributedView: attributedView, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets an image to the text attachment with a source. /// /// - Parameters: /// - resource: The ``Resource`` object that defines data information from the network or a data provider. /// - attributedView: The owner of the attributed string to which this `NSTextAttachment` is added. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieval and setting are finished. /// - Returns: A task that represents the image downloading. /// /// Internally, this method will use ``KingfisherManager`` to get the requested source. Since this method will /// perform UI changes, it is your responsibility of calling it from the main thread. /// /// The retrieved image will be set to the `NSTextAttachment.image` property. Because it is not an image view-based /// rendering, options related to the view, such as ``KingfisherOptionsInfoItem/transition(_:)``, are not supported. /// /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task is done. It gives the view a /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an /// attributed string with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. /// /// Here is a typical use case: /// /// ```swift /// let label: UILabel = // ... /// /// let textAttachment = NSTextAttachment() /// textAttachment.kf.setImage( /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, /// attributedView: label, /// options: [ /// .processor( /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) /// |> RoundCornerImageProcessor(cornerRadius: 15)) /// ] /// ) /// /// let attributedText = NSMutableAttributedString(string: "Hello World") /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) /// label.attributedText = attributedText /// ``` @discardableResult public func setImage( with resource: (any Resource)?, attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setImage( with: resource.map { .network($0) }, attributedView: attributedView, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler ) } func setImage( with source: Source?, attributedView: @escaping @Sendable () -> KFCrossPlatformView, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { var mutatingSelf = self guard let source = source else { base.image = placeholder mutatingSelf.taskIdentifier = nil completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil } var options = parsedOptions if !options.keepCurrentImageWhileLoading { base.image = placeholder } let issuedIdentifier = Source.Identifier.next() mutatingSelf.taskIdentifier = issuedIdentifier if let block = progressBlock { options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } let task = KingfisherManager.shared.retrieveImage( with: source, options: options, progressiveImageSetter: { self.base.image = $0 }, referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, completionHandler: { result in CallbackQueueMain.currentOrAsync { guard issuedIdentifier == self.taskIdentifier else { let reason: KingfisherError.ImageSettingErrorReason do { let value = try result.get() reason = .notCurrentSourceTask(result: value, error: nil, source: source) } catch { reason = .notCurrentSourceTask(result: nil, error: error, source: source) } let error = KingfisherError.imageSettingError(reason: reason) completionHandler?(.failure(error)) return } mutatingSelf.imageTask = nil mutatingSelf.taskIdentifier = nil switch result { case .success(let value): self.base.image = value.image let view = attributedView() #if canImport(UIKit) view.setNeedsDisplay() #else view.setNeedsDisplay(view.bounds) #endif case .failure: if let image = options.onFailureImage { self.base.image = image } } completionHandler?(result) } } ) mutatingSelf.imageTask = task return task } // MARK: Cancelling Image /// Cancel the image download task bound to the text attachment if it is running. /// /// Nothing will happen if the downloading has already finished. public func cancelDownloadTask() { imageTask?.cancel() } } @MainActor private var taskIdentifierKey: Void? @MainActor private var imageTaskKey: Void? // MARK: Properties @MainActor extension KingfisherWrapper where Base: NSTextAttachment { public private(set) var taskIdentifier: Source.Identifier.Value? { get { let box: Box? = getAssociatedObject(base, &taskIdentifierKey) return box?.value } set { let box = newValue.map { Box($0) } setRetainedAssociatedObject(base, &taskIdentifierKey, box) } } private var imageTask: DownloadTask? { get { return getAssociatedObject(base, &imageTaskKey) } set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } } #endif ================================================ FILE: Sources/Extensions/PHLivePhotoView+Kingfisher.swift ================================================ // // PHLivePhotoView+Kingfisher.swift // Kingfisher // // Created by onevcat on 2024/10/04. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(watchOS) // Only a placeholder. public struct RetrieveLivePhotoResult: @unchecked Sendable { } #else @preconcurrency import PhotosUI /// A result type that contains the information of a retrieved live photo. /// /// This struct is used to encapsulate the result of a live photo retrieval operation, including the loading information, /// the retrieved `PHLivePhoto` object, and any additional information provided by the result handler. /// /// - Note: The `info` dictionary is considered sendable based on the documentation for "Result Handler Info Dictionary Keys". /// See: [Result Handler Info Dictionary Keys](https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys) public struct RetrieveLivePhotoResult: @unchecked Sendable { /// The loading information of the live photo. public let loadingInfo: LivePhotoLoadingInfoResult /// The retrieved live photo object which is given by the /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method from /// the result handler. public let livePhoto: PHLivePhoto? // According to "Result Handler Info Dictionary Keys", we can trust the `info` in handler is sendable. // https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys /// The additional information provided by the result handler when retrieving the live photo. public let info: [AnyHashable : Any]? } @MainActor private var taskIdentifierKey: Void? @MainActor private var targetSizeKey: Void? @MainActor private var contentModeKey: Void? @MainActor extension KingfisherWrapper where Base: PHLivePhotoView { /// Gets the task identifier associated with the image view for the live photo task. public private(set) var taskIdentifier: Source.Identifier.Value? { get { let box: Box? = getAssociatedObject(base, &taskIdentifierKey) return box?.value } set { let box = newValue.map { Box($0) } setRetainedAssociatedObject(base, &taskIdentifierKey, box) } } /// The target size of the live photo view. It is used in the /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as /// the `targetSize` argument when loading the live photo. /// /// If not set, `.zero` will be used. public var targetSize: CGSize { get { getAssociatedObject(base, &targetSizeKey) ?? .zero } set { setRetainedAssociatedObject(base, &targetSizeKey, newValue) } } /// The content mode of the live photo view. It is used in the /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as /// the `contentMode` argument when loading the live photo. /// /// If not set, `.default` will be used. public var contentMode: PHImageContentMode { get { getAssociatedObject(base, &contentModeKey) ?? .default } set { setRetainedAssociatedObject(base, &contentModeKey, newValue) } } /// Sets a live photo to the view with an array of `URL`. /// /// - Parameters: /// - urls: The `URL`s defining the live photo resource. It should contains two URLs, one for the still image and /// one for the video. /// - options: An options set to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - completionHandler: Called when the image setting process finishes. /// - Returns: A task represents the image downloading. /// The return value will be `nil` if the image is set with a empty source. /// /// - Note: Not all options in ``KingfisherOptionsInfo`` are supported in this method, for example, the live photo /// does not support any custom processors. Different from the extension method for a normal image view on the /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future. /// /// - Note: To get refined control of the resources, use the ``setImage(with:options:completionHandler:)-1n4p2`` /// method with a ``LivePhotoSource`` object. /// /// Example: /// /// ```swift /// let urls = [ /// URL(string: "https://example.com/image.heic")!, // imageURL /// URL(string: "https://example.com/video.mov")! // videoURL /// ] /// let livePhotoView = PHLivePhotoView() /// livePhotoView.kf.setImage(with: urls) { result in /// switch result { /// case .success(let retrieveResult): /// print("Live photo loaded: \(retrieveResult.livePhoto).") /// print("Cache type: \(retrieveResult.loadingInfo.cacheType).") /// case .failure(let error): /// print("Error: \(error)") /// } /// ``` @discardableResult public func setImage( with urls: [URL], // placeholder: KFCrossPlatformImage? = nil, // Not supported yet options: KingfisherOptionsInfo? = nil, // progressBlock: DownloadProgressBlock? = nil, // Not supported yet completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> Task<(), Never>? { setImage( with: LivePhotoSource(urls: urls), options: options, completionHandler: completionHandler ) } /// Sets a live photo to the view with a ``LivePhotoSource``. /// /// - Parameters: /// - source: The ``LivePhotoSource`` object defining the live photo resource. /// - options: An options set to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. /// - completionHandler: Called when the image setting process finishes. /// - Returns: A task represents the image downloading. /// The return value will be `nil` if the image is set with a empty source. /// /// - Note: Not all options in ``KingfisherOptionsInfo`` are supported in this method, for example, the live photo /// does not support any custom processors. Different from the extension method for a normal image view on the /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future. /// /// Sample: /// ```swift /// let source = LivePhotoSource(urls: [ /// URL(string: "https://example.com/image.heic")!, // imageURL /// URL(string: "https://example.com/video.mov")! // videoURL /// ]) /// let livePhotoView = PHLivePhotoView() /// livePhotoView.kf.setImage(with: source) { result in /// switch result { /// case .success(let retrieveResult): /// print("Live photo loaded: \(retrieveResult.livePhoto).") /// print("Cache type: \(retrieveResult.loadingInfo.cacheType).") /// case .failure(let error): /// print("Error: \(error)") /// } /// ``` @discardableResult public func setImage( with source: LivePhotoSource?, // placeholder: KFCrossPlatformImage? = nil, // Not supported yet options: KingfisherOptionsInfo? = nil, // progressBlock: DownloadProgressBlock? = nil, // Not supported yet completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> Task<(), Never>? { var mutatingSelf = self // Empty source fails the loading early and clear the current task identifier. guard let source = source else { base.livePhoto = nil mutatingSelf.taskIdentifier = nil completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil } let issuedIdentifier = Source.Identifier.next() mutatingSelf.taskIdentifier = issuedIdentifier let taskIdentifierChecking = { issuedIdentifier == self.taskIdentifier } // Copy these associated values to prevent issues from reentrance. let targetSize = targetSize let contentMode = contentMode let task = Task { @MainActor in do { let loadingInfo = try await KingfisherManager.shared.retrieveLivePhoto( with: source, options: options, progressBlock: nil, // progressBlock, // Not supported yet referenceTaskIdentifierChecker: taskIdentifierChecking ) if let notCurrentTaskError = self.checkNotCurrentTask( issuedIdentifier: issuedIdentifier, result: .init(loadingInfo: loadingInfo, livePhoto: nil, info: nil), error: nil, source: source ) { completionHandler?(.failure(notCurrentTaskError)) return } PHLivePhoto.request( withResourceFileURLs: loadingInfo.fileURLs, placeholderImage: nil, targetSize: targetSize, contentMode: contentMode, resultHandler: { livePhoto, info in let result = RetrieveLivePhotoResult( loadingInfo: loadingInfo, livePhoto: livePhoto, info: info ) if let notCurrentTaskError = self.checkNotCurrentTask( issuedIdentifier: issuedIdentifier, result: result, error: nil, source: source ) { completionHandler?(.failure(notCurrentTaskError)) return } base.livePhoto = livePhoto if let error = info[PHLivePhotoInfoErrorKey] as? NSError { let failingReason: KingfisherError.ImageSettingErrorReason = .livePhotoResultError(result: result, error: error, source: source) completionHandler?(.failure(.imageSettingError(reason: failingReason))) return } // Since we are not returning the request ID, seems no way for user to cancel it if the // `request` method is called. However, we are sure the request method will always load the // image from disk, it should not be a problem. In case we still report the error in the // completion if (info[PHLivePhotoInfoCancelledKey] as? NSNumber)?.boolValue ?? false { completionHandler?(.failure( .requestError(reason: .livePhotoTaskCancelled(source: source))) ) return } // If the PHLivePhotoInfoIsDegradedKey value in your result handler’s info dictionary is true, // Photos will call your result handler again. if (info[PHLivePhotoInfoIsDegradedKey] as? NSNumber)?.boolValue == true { // This ensures `completionHandler` be only called once. return } completionHandler?(.success(result)) } ) } catch { if let notCurrentTaskError = self.checkNotCurrentTask( issuedIdentifier: issuedIdentifier, result: nil, error: error, source: source ) { completionHandler?(.failure(notCurrentTaskError)) return } if let kfError = error as? KingfisherError { completionHandler?(.failure(kfError)) } else if error is CancellationError { completionHandler?(.failure(.requestError(reason: .livePhotoTaskCancelled(source: source)))) } else { completionHandler?(.failure(.imageSettingError( reason: .livePhotoResultError(result: nil, error: error, source: source))) ) } } } return task } private func checkNotCurrentTask( issuedIdentifier: Source.Identifier.Value, result: RetrieveLivePhotoResult?, error: (any Error)?, source: LivePhotoSource ) -> KingfisherError? { if issuedIdentifier == self.taskIdentifier { return nil } return .imageSettingError(reason: .notCurrentLivePhotoSourceTask(result: result, error: error, source: source)) } } #endif ================================================ FILE: Sources/Extensions/UIButton+Kingfisher.swift ================================================ // // UIButton+Kingfisher.swift // Kingfisher // // Created by Wei Wang on 15/4/13. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) #if canImport(UIKit) import UIKit @MainActor extension KingfisherWrapper where Base: UIButton { // MARK: Setting Image /// Sets an image to the button for a specified state with a source. /// /// - Parameters: /// - source: The `Source` object contains information about the image. /// - state: The button state to which the image should be set. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieved and set finished. /// - Returns: A task represents the image downloading. /// /// - Note: /// Internally, this method will use `KingfisherManager` to get the requested source, from either cache /// or network. Since this method will perform UI changes, you must call it from the main thread. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. /// @discardableResult public func setImage( with source: Source?, for state: UIControl.State, placeholder: UIImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setImage( with: source, for: state, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets an image to the button for a specified state with a requested resource. /// /// - Parameters: /// - resource: The `Resource` object contains information about the resource. /// - state: The button state to which the image should be set. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieved and set finished. /// - Returns: A task represents the image downloading. /// /// - Note: /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache /// or network. Since this method will perform UI changes, you must call it from the main thread. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. /// @discardableResult public func setImage( with resource: (any Resource)?, for state: UIControl.State, placeholder: UIImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { return setImage( with: resource?.convertToSource(), for: state, placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler) } @discardableResult public func setImage( with source: Source?, for state: UIControl.State, placeholder: UIImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { var mutatingSelf = self return setImage( with: source, imageAccessor: ImagePropertyAccessor( setImage: { image, _ in base.setImage(image, for: state) }, getImage: { base.image(for: state) } ), taskAccessor: TaskPropertyAccessor( setTaskIdentifier: { setTaskIdentifier($0, for: state) }, getTaskIdentifier: { taskIdentifier(for: state) }, setTask: { mutatingSelf.imageTask = $0 } ), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, completionHandler: completionHandler ) } // MARK: Cancelling Downloading Task /// Cancels the image download task of the button if it is running. /// Nothing will happen if the downloading has already finished. public func cancelImageDownloadTask() { imageTask?.cancel() } // MARK: Setting Background Image /// Sets a background image to the button for a specified state with a source. /// /// - Parameters: /// - source: The `Source` object contains information about the image. /// - state: The button state to which the image should be set. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieved and set finished. /// - Returns: A task represents the image downloading. /// /// - Note: /// Internally, this method will use `KingfisherManager` to get the requested source /// Since this method will perform UI changes, you must call it from the main thread. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. /// @discardableResult public func setBackgroundImage( with source: Source?, for state: UIControl.State, placeholder: UIImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setBackgroundImage( with: source, for: state, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler ) } /// Sets a background image to the button for a specified state with a requested resource. /// /// - Parameters: /// - resource: The `Resource` object contains information about the resource. /// - state: The button state to which the image should be set. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. /// - completionHandler: Called when the image retrieved and set finished. /// - Returns: A task represents the image downloading. /// /// - Note: /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache /// or network. Since this method will perform UI changes, you must call it from the main thread. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. /// @discardableResult public func setBackgroundImage( with resource: (any Resource)?, for state: UIControl.State, placeholder: UIImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@Sendable (Result) -> Void)? = nil) -> DownloadTask? { return setBackgroundImage( with: resource?.convertToSource(), for: state, placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler) } func setBackgroundImage( with source: Source?, for state: UIControl.State, placeholder: UIImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { var mutatingSelf = self return setImage( with: source, imageAccessor: ImagePropertyAccessor( setImage: { image, _ in base.setBackgroundImage(image, for: state) }, getImage: { base.backgroundImage(for: state) } ), taskAccessor: TaskPropertyAccessor( setTaskIdentifier: { setBackgroundTaskIdentifier($0, for: state) }, getTaskIdentifier: { backgroundTaskIdentifier(for: state) }, setTask: { mutatingSelf.backgroundImageTask = $0 } ), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, completionHandler: completionHandler ) } // MARK: Cancelling Background Downloading Task /// Cancels the background image download task of the button if it is running. /// Nothing will happen if the downloading has already finished. public func cancelBackgroundImageDownloadTask() { backgroundImageTask?.cancel() } } // MARK: - Associated Object @MainActor private var taskIdentifierKey: Void? @MainActor private var imageTaskKey: Void? // MARK: Properties @MainActor extension KingfisherWrapper where Base: UIButton { private typealias TaskIdentifier = Box<[UInt: Source.Identifier.Value]> public func taskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { return taskIdentifierInfo.value[state.rawValue] } private func setTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { taskIdentifierInfo.value[state.rawValue] = identifier } private var taskIdentifierInfo: TaskIdentifier { return getAssociatedObject(base, &taskIdentifierKey) ?? { setRetainedAssociatedObject(base, &taskIdentifierKey, $0) return $0 } (TaskIdentifier([:])) } private var imageTask: DownloadTask? { get { return getAssociatedObject(base, &imageTaskKey) } set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } } @MainActor private var backgroundTaskIdentifierKey: Void? @MainActor private var backgroundImageTaskKey: Void? // MARK: Background Properties @MainActor extension KingfisherWrapper where Base: UIButton { public func backgroundTaskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { return backgroundTaskIdentifierInfo.value[state.rawValue] } private func setBackgroundTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { backgroundTaskIdentifierInfo.value[state.rawValue] = identifier } private var backgroundTaskIdentifierInfo: TaskIdentifier { return getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? { setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0) return $0 } (TaskIdentifier([:])) } private var backgroundImageTask: DownloadTask? { get { return getAssociatedObject(base, &backgroundImageTaskKey) } mutating set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) } } } #endif #endif ================================================ FILE: Sources/General/ImageSource/AVAssetImageDataProvider.swift ================================================ // // AVAssetImageDataProvider.swift // Kingfisher // // Created by onevcat on 2020/08/09. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) import Foundation import AVKit #if canImport(MobileCoreServices) import MobileCoreServices #else import CoreServices #endif #if compiler(>=6) extension AVAssetImageGenerator: @unchecked @retroactive Sendable { } #else extension AVAssetImageGenerator: @unchecked Sendable { } #endif /// A data provider to provide thumbnail data from a given AVKit asset. public struct AVAssetImageDataProvider: ImageDataProvider { /// The possible error might be caused by the ``AVAssetImageDataProvider``. public enum AVAssetImageDataProviderError: Error { /// The data provider process is cancelled. case userCancelled /// The retrieved image is invalid. /// - Parameter image: The image object that is not recognized as valid. case invalidImage(_ image: CGImage?) } /// The asset image generator bound to `self`. public let assetImageGenerator: AVAssetImageGenerator /// The time at which the image should be generated in the asset. public let time: CMTime private var internalKey: String { guard let url = (assetImageGenerator.asset as? AVURLAsset)?.url else { return UUID().uuidString } return url.cacheKey } /// The cache key used by `self`. public var cacheKey: String { return "\(internalKey)_\(time.seconds)" } /// Creates an asset image data provider. /// - Parameters: /// - assetImageGenerator: The asset image generator that controls data providing behaviors. /// - time: The time at which the image should be generated in the asset. public init(assetImageGenerator: AVAssetImageGenerator, time: CMTime) { self.assetImageGenerator = assetImageGenerator self.time = time } /// Creates an asset image data provider. /// - Parameters: /// - assetURL: The URL of asset for providing image data. /// - time: At which time in the asset the image should be generated. /// /// This method uses the `assetURL` parameter to create an `AVAssetImageGenerator` object, then calls /// the ``init(assetImageGenerator:time:)`` initializer. public init(assetURL: URL, time: CMTime) { let asset = AVAsset(url: assetURL) let generator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true self.init(assetImageGenerator: generator, time: time) } /// Creates an asset image data provider. /// /// - Parameters: /// - assetURL: The URL of asset for providing image data. /// - seconds: At which time in seconds in the asset the image should be generated. /// /// This method uses the `assetURL` parameter to create an `AVAssetImageGenerator` object, uses the `seconds` /// parameter to create a `CMTime`, then calls the ``init(assetImageGenerator:time:)`` initializer. /// public init(assetURL: URL, seconds: TimeInterval) { let time = CMTime(seconds: seconds, preferredTimescale: 600) self.init(assetURL: assetURL, time: time) } public func data(handler: @Sendable @escaping (Result) -> Void) { assetImageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { (requestedTime, image, imageTime, result, error) in if let error = error { handler(.failure(error)) return } if result == .cancelled { handler(.failure(AVAssetImageDataProviderError.userCancelled)) return } guard let cgImage = image, let data = cgImage.jpegData else { handler(.failure(AVAssetImageDataProviderError.invalidImage(image))) return } handler(.success(data)) } } } extension CGImage { var jpegData: Data? { guard let mutableData = CFDataCreateMutable(nil, 0) else { return nil } #if os(visionOS) guard let destination = CGImageDestinationCreateWithData( mutableData, UTType.jpeg.identifier as CFString , 1, nil ) else { return nil } #else guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else { return nil } #endif CGImageDestinationAddImage(destination, self, nil) guard CGImageDestinationFinalize(destination) else { return nil } return mutableData as Data } } #endif ================================================ FILE: Sources/General/ImageSource/ImageDataProvider.swift ================================================ // // ImageDataProvider.swift // Kingfisher // // Created by onevcat on 2018/11/13. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import ImageIO /// Represents a data provider to provide image data to Kingfisher when setting with /// ``Source/provider(_:)`` source. Compared to ``Source/network(_:)`` member, it gives a chance /// to load some image data in your own way, as long as you can provide the data /// representation for the image. public protocol ImageDataProvider: Sendable { /// The key used in cache. var cacheKey: String { get } /// Provides the data which represents image. Kingfisher uses the data you pass in the /// handler to process images and caches it for later use. /// /// - Parameter handler: The handler you should call when you prepared your data. /// If the data is loaded successfully, call the handler with /// a `.success` with the data associated. Otherwise, call it /// with a `.failure` and pass the error. /// /// - Note: If the `handler` is called with a `.failure` with error, /// a ``KingfisherError/ImageSettingErrorReason/dataProviderError(provider:error:)`` will be finally thrown out to /// you as the ``KingfisherError`` from the framework. func data(handler: @escaping @Sendable (Result) -> Void) /// The content URL represents this provider, if exists. var contentURL: URL? { get } } extension ImageDataProvider { func data() async throws -> Data { try await withCheckedThrowingContinuation { continuation in data(handler: { continuation.resume(with: $0) }) } } } public extension ImageDataProvider { var contentURL: URL? { return nil } func convertToSource() -> Source { .provider(self) } } /// Represents an image data provider for loading from a local file URL on disk. /// Uses this type for adding a disk image to Kingfisher. Compared to loading it /// directly, you can get benefit of using Kingfisher's extension methods, as well /// as applying ``ImageProcessor``s and storing the image to ``ImageCache`` of Kingfisher. public struct LocalFileImageDataProvider: ImageDataProvider { // MARK: Public Properties /// The file URL from which the image be loaded. public let fileURL: URL private let loadingQueue: ExecutionQueue // MARK: Initializers /// Creates an image data provider by supplying the target local file URL. /// /// - Parameters: /// - fileURL: The file URL from which the image be loaded. /// - cacheKey: The key is used for caching the image data. By default, /// the `absoluteString` of ``LocalFileImageDataProvider/fileURL`` is used. /// - loadingQueue: The queue where the file loading should happen. By default, the dispatch queue of /// `.global(qos: .userInitiated)` will be used. public init( fileURL: URL, cacheKey: String? = nil, loadingQueue: ExecutionQueue = .dispatch(DispatchQueue.global(qos: .userInitiated)) ) { self.fileURL = fileURL self.cacheKey = cacheKey ?? fileURL.localFileCacheKey self.loadingQueue = loadingQueue } // MARK: Protocol Conforming /// The key used in cache. public var cacheKey: String public func data(handler: @escaping @Sendable (Result) -> Void) { loadingQueue.execute { handler(Result(catching: { try Data(contentsOf: fileURL) })) } } public var data: Data { get async throws { try await withCheckedThrowingContinuation { continuation in loadingQueue.execute { do { let data = try Data(contentsOf: fileURL) continuation.resume(returning: data) } catch { continuation.resume(throwing: error) } } } } } /// The URL of the local file on the disk. public var contentURL: URL? { return fileURL } } /// Represents an image data provider for loading image from a given Base64 encoded string. public struct Base64ImageDataProvider: ImageDataProvider { // MARK: Public Properties /// The encoded Base64 string for the image. public let base64String: String // MARK: Initializers /// Creates an image data provider by supplying the Base64 encoded string. /// /// - Parameters: /// - base64String: The Base64 encoded string for an image. /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. public init(base64String: String, cacheKey: String) { self.base64String = base64String self.cacheKey = cacheKey } // MARK: Protocol Conforming /// The key used in cache. public var cacheKey: String public func data(handler: (Result) -> Void) { let data = Data(base64Encoded: base64String)! handler(.success(data)) } } /// Represents an image data provider for a raw data object. public struct RawImageDataProvider: ImageDataProvider { // MARK: Public Properties /// The raw data object to provide to Kingfisher image loader. public let data: Data // MARK: Initializers /// Creates an image data provider by the given raw `data` value and a `cacheKey` be used in Kingfisher cache. /// /// - Parameters: /// - data: The raw data represents an image. /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. public init(data: Data, cacheKey: String) { self.data = data self.cacheKey = cacheKey } // MARK: Protocol Conforming /// The key used in cache. public var cacheKey: String public func data(handler: @escaping (Result) -> Void) { handler(.success(data)) } } /// A data provider that creates a thumbnail from a URL using Core Graphics. public struct ThumbnailImageDataProvider: ImageDataProvider { public enum ThumbnailImageDataProviderError: Error { case invalidImageSource case invalidThumbnail case writeDataError case finalizeDataError } /// The URL from which to load the image public let url: URL /// The maximum size of the thumbnail in pixels public var maxPixelSize: CGFloat /// Whether to always create a thumbnail even if the image is smaller than maxPixelSize public var alwaysCreateThumbnail: Bool /// The cache key for this provider public var cacheKey: String /// Creates a new thumbnail data provider /// - Parameters: /// - url: The URL from which to load the image /// - maxPixelSize: The maximum size of the thumbnail in pixels /// - alwaysCreateThumbnail: Whether to always create a thumbnail even if the image is smaller than maxPixelSize public init( url: URL, maxPixelSize: CGFloat, alwaysCreateThumbnail: Bool = true, cacheKey: String? = nil ) { self.url = url self.maxPixelSize = maxPixelSize self.alwaysCreateThumbnail = alwaysCreateThumbnail self.cacheKey = cacheKey ?? "\(url.absoluteString)_thumb_\(maxPixelSize)_\(alwaysCreateThumbnail)" } public func data(handler: @escaping @Sendable (Result) -> Void) { DispatchQueue.global(qos: .userInitiated).async { do { guard let url = URL(string: url.absoluteString) else { throw KingfisherError.imageSettingError(reason: .emptySource) } guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { throw ThumbnailImageDataProviderError.invalidImageSource } let options = [ kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, kCGImageSourceCreateThumbnailFromImageAlways: alwaysCreateThumbnail, kCGImageSourceCreateThumbnailWithTransform: true ] guard let thumbnailRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { throw ThumbnailImageDataProviderError.invalidThumbnail } let data = NSMutableData() guard let destination = CGImageDestinationCreateWithData( data, CGImageSourceGetType(imageSource)!, 1, nil ) else { throw ThumbnailImageDataProviderError.writeDataError } CGImageDestinationAddImage(destination, thumbnailRef, nil) if CGImageDestinationFinalize(destination) { handler(.success(data as Data)) } else { throw ThumbnailImageDataProviderError.finalizeDataError } } catch { handler(.failure(error)) } } } } ================================================ FILE: Sources/General/ImageSource/LivePhotoSource.swift ================================================ // // LivePhotoSource.swift // Kingfisher // // Created by onevcat on 2024/10/01. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// A type represents a loadable resource for a Live Photo, which consists of a still image and a video. /// /// Kingfisher expects a ``LivePhotoSource`` value to load a Live Photo with its high-level APIs. /// A ``LivePhotoSource`` is typically a collection of two ``LivePhotoResource`` values, one for the still image and /// one for the video. public struct LivePhotoSource: Sendable { /// The resources of a Live Photo. public let resources: [LivePhotoResource] /// Creates a Live Photo source with given resources. /// - Parameter resources: The downloadable resource for a Live Photo. It should contain two resources, one for the /// still image and one for the video. public init(resources: [any Resource]) { let livePhotoResources = resources.map { LivePhotoResource(resource: $0) } self.init(livePhotoResources) } /// Creates a Live Photo source with given URLs. /// - Parameter urls: The URLs of the downloadable resources for a Live Photo. It should contain two URLs, one for /// the still image and one for the video. public init(urls: [URL]) { let resources = urls.map { KF.ImageResource(downloadURL: $0) } self.init(resources: resources) } /// Creates a Live Photo source with given resources. /// - Parameter resources: The resources for a Live Photo. It should contain two resources, one for the still image /// and one for the video. public init(_ resources: [LivePhotoResource]) { self.resources = resources } } /// A resource type representing a component of a Live Photo, which consists of a still image and a video. /// /// ``LivePhotoResource`` encapsulates the necessary information to download and cache a single component of a Live /// Photo: it is either a still image (typically in HEIF format with "heic" filename extension) or a video (typically in /// QuickTime format with "mov" filename extension). Multiple ``LivePhotoResource`` values (typically two, one for the /// image and one for the video) can form a ``LivePhotoSource``, which is expected by Kingfisher in its live photo /// loading high level APIs. /// /// The Live Photo data can be retrieved by `PHAssetResourceManager.requestData` method and uploaded to your server. /// You should not modify the metadata or other information of the data, otherwise, it is possible that the /// `PHLivePhoto` class cannot read and recognize it anymore. For more information, please refer to Apple's /// documentation of Photos framework. public struct LivePhotoResource: Sendable { /// The file type of a ``LivePhotoResource``. public enum FileType: Sendable, Equatable { /// File type HEIC. Usually it represents the still image in a Live Photo. case heic /// File type MOV. Usually it represents the video in a Live Photo. case mov /// Other file types with the file extension. case other(String) var fileExtension: String { switch self { case .heic: return "heic" case .mov: return "mov" case .other(let ext): return ext } } } /// The data source of a Live Photo resource. /// /// This is a general ``Source`` type, which can be either a network resource (as ``Source/network(_:)``) or a /// provided resource as ``Source/provider(_:)``. public let dataSource: Source /// The file type of the resource. public let referenceFileType: FileType var cacheKey: String { dataSource.cacheKey } var downloadURL: URL? { dataSource.url } /// Creates a Live Photo resource with given download URL, cache key and file type. /// - Parameters: /// - downloadURL: The URL to download the resource. /// - cacheKey: The cache key for the resource. If `nil`, Kingfisher will use the `absoluteString` of the URL as /// the cache key. /// - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL. /// /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded /// data. public init(downloadURL: URL, cacheKey: String? = nil, fileType: FileType? = nil) { let resource = KF.ImageResource(downloadURL: downloadURL, cacheKey: cacheKey) dataSource = .network(resource) referenceFileType = fileType ?? resource.guessedFileType } /// Creates a Live Photo resource with given resource and file type. /// - Parameters: /// - resource: The resource to download the data. /// - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL. /// /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded /// data. public init(resource: any Resource, fileType: FileType? = nil) { self.dataSource = .network(resource) referenceFileType = fileType ?? resource.guessedFileType } /// Creates a Live Photo resource with given data source and file type. /// - Parameters: /// - source: The data source of the resource. It can be either a network resource or a provided resource. /// - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL. /// /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded /// data. public init(source: Source, fileType: FileType? = nil) { self.dataSource = source referenceFileType = fileType ?? source.url?.guessedFileType ?? .other("") } } extension LivePhotoResource.FileType { func determinedFileExtension(_ data: Data) -> String? { switch self { case .mov: return "mov" case .heic: return "heic" case .other(let ext): if !ext.isEmpty { return ext } return Self.guessedFileExtension(from: data) } } static let fytpChunk: [UInt8] = [0x66, 0x74, 0x79, 0x70] // fytp (file type box) static let heicChunk: [UInt8] = [0x68, 0x65, 0x69, 0x63] // heic (HEIF) static let qtChunk: [UInt8] = [0x71, 0x74, 0x20, 0x20] // qt (QuickTime), .mov static func guessedFileExtension(from data: Data) -> String? { guard data.count >= 12 else { return nil } var buffer = [UInt8](repeating: 0, count: 12) data.copyBytes(to: &buffer, count: 12) guard Array(buffer[4..<8]) == fytpChunk else { return nil } let fileTypeChunk = Array(buffer[8..<12]) if fileTypeChunk == heicChunk { return "heic" } if fileTypeChunk == qtChunk { return "mov" } return nil } } extension Resource { var guessedFileType: LivePhotoResource.FileType { let pathExtension = downloadURL.pathExtension.lowercased() switch pathExtension { case "mov": return .mov case "heic": return .heic default: return .other(pathExtension) } } } ================================================ FILE: Sources/General/ImageSource/PHPickerResultImageDataProvider.swift ================================================ // // PHPickerResultImageDataProvider.swift // Kingfisher // // Created by nuomi1 on 2024-04-17. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation #if os(iOS) || os(macOS) || os(visionOS) import PhotosUI #if compiler(>=6) @available(iOS 14.0, macOS 13.0, *) extension PHPickerResult: @unchecked @retroactive Sendable { } #else @available(iOS 14.0, macOS 13.0, *) extension PHPickerResult: @unchecked Sendable { } #endif /// A data provider to provide image data from a given `PHPickerResult`. @available(iOS 14.0, macOS 13.0, *) public struct PHPickerResultImageDataProvider: ImageDataProvider { internal static func _cacheKey( providedCacheKey: String?, assetIdentifier: String?, contentTypeIdentifier: String, uuidString: () -> String ) -> String { if let providedCacheKey { return providedCacheKey } let id = assetIdentifier ?? uuidString() return "\(id)_\(contentTypeIdentifier)" } /// The possible error might be caused by the `PHPickerResultImageDataProvider`. /// - invalidImage: The retrieved image is invalid. public enum PHPickerResultImageDataProviderError: Error { /// An error happens during picking up image through the item provider of `PHPickerResult`. case pickerProviderError(any Error) /// The retrieved image is invalid. case invalidImage } /// The picker result bound to `self`. public let pickerResult: PHPickerResult /// The content type of the image. public let contentType: UTType /// The key used in cache. /// /// If you pass a custom key when creating the provider, it will be used. /// Otherwise, if the picker result contains a stable asset identifier, it will be used as the key. /// If no stable identifier is available, a random UUID will be generated and used for this provider instance. public let cacheKey: String /// Creates an image data provider from a given `PHPickerResult`. /// - Parameters: /// - pickerResult: The picker result to provide image data. /// - contentType: The content type of the image. Default is `UTType.image`. /// - cacheKey: Optional cache key to use. If set, it will be used as `self.cacheKey` directly. public init(pickerResult: PHPickerResult, contentType: UTType = UTType.image, cacheKey: String? = nil) { self.pickerResult = pickerResult self.contentType = contentType if cacheKey == nil && pickerResult.assetIdentifier == nil { assertionFailure("[Kingfisher] Should use `PHPhotoLibrary.shared()` to pick image.") } self.cacheKey = Self._cacheKey( providedCacheKey: cacheKey, assetIdentifier: pickerResult.assetIdentifier, contentTypeIdentifier: contentType.identifier, uuidString: { UUID().uuidString } ) } public func data(handler: @escaping @Sendable (Result) -> Void) { pickerResult.itemProvider.loadDataRepresentation(forTypeIdentifier: contentType.identifier) { data, error in if let error { handler(.failure(PHPickerResultImageDataProviderError.pickerProviderError(error))) return } guard let data else { handler(.failure(PHPickerResultImageDataProviderError.invalidImage)) return } handler(.success(data)) } } } #endif ================================================ FILE: Sources/General/ImageSource/PhotosPickerItemImageDataProvider.swift ================================================ // // PhotosPickerItemImageDataProvider.swift // Kingfisher // // Created by nuomi1 on 2026/1/7. // // Copyright (c) 2026 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation #if os(iOS) || os(macOS) || os(visionOS) import PhotosUI import SwiftUI /// A data provider to provide image data from a given `PhotosPickerItem`. @available(iOS 16.0, macOS 13.0, *) public struct PhotosPickerItemImageDataProvider: ImageDataProvider { internal static func _cacheKey( providedCacheKey: String?, itemIdentifier: String?, uuidString: () -> String ) -> String { if let providedCacheKey { return providedCacheKey } if let itemIdentifier { return itemIdentifier } return uuidString() } /// The possible error might be caused by the `PhotosPickerItemImageDataProvider`. /// - invalidImage: The retrieved image is invalid. public enum PhotosPickerItemImageDataProviderError: Error { /// An error happens during picking up image through the item provider of `PhotosPickerItem`. case pickerProviderError(any Error) /// The retrieved image is invalid. case invalidImage } /// The picker item bound to `self`. public let pickerItem: PhotosPickerItem /// The key used in cache. /// /// If you pass a custom key when creating the provider, it will be used. /// Otherwise, if the picker item provides a stable identifier, it will be used. /// If no stable identifier is available, a random UUID will be generated and used for this provider instance. public let cacheKey: String /// Creates an image data provider from a given `PhotosPickerItem`. /// - Parameters: /// - pickerItem: The picker item to provide image data. /// - cacheKey: Optional cache key to use. If set, it will be used as `self.cacheKey` directly. public init(pickerItem: PhotosPickerItem, cacheKey: String? = nil) { self.pickerItem = pickerItem if cacheKey == nil && pickerItem.itemIdentifier == nil { assertionFailure("[Kingfisher] Should use `PHPhotoLibrary.shared()` to pick image.") } self.cacheKey = Self._cacheKey( providedCacheKey: cacheKey, itemIdentifier: pickerItem.itemIdentifier, uuidString: { UUID().uuidString } ) } public func data(handler: @escaping @Sendable (Result) -> Void) { pickerItem.loadTransferable(type: Data.self, completionHandler: { result in switch result { case let .success(data): if let data { handler(.success(data)) } else { handler(.failure(PhotosPickerItemImageDataProviderError.invalidImage)) } case let .failure(error): handler(.failure(PhotosPickerItemImageDataProviderError.pickerProviderError(error))) } }) } } #endif ================================================ FILE: Sources/General/ImageSource/Resource.swift ================================================ // // Resource.swift // Kingfisher // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents an image resource at a certain url and a given cache key. /// Kingfisher will use a ``Resource`` to download a resource from network and cache it with the cache key when /// using ``Source/network(_:)`` as its image setting source. public protocol Resource: Sendable { /// The key used in cache. var cacheKey: String { get } /// The target image URL. var downloadURL: URL { get } } extension Resource { /// Converts `self` to a valid ``Source`` based on the ``Resource/downloadURL`` scheme. A ``Source/provider(_:)`` /// with ``LocalFileImageDataProvider`` associated will be returned if the URL points to a local file. Otherwise, /// ``Source/network(_:)`` is returned. /// /// - Parameter overrideCacheKey: The key should be used to override the ``Resource/cacheKey`` when performing the /// conversion. `nil` if not overridden and ``Resource/cacheKey`` of `self` is used. /// - Returns: The converted source. /// public func convertToSource(overrideCacheKey: String? = nil) -> Source { let key = overrideCacheKey ?? cacheKey return downloadURL.isFileURL ? .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) : .network(KF.ImageResource(downloadURL: downloadURL, cacheKey: key)) } } @available(*, deprecated, message: "This type conflicts with `GeneratedAssetSymbols.ImageResource` in Swift 5.9. Renamed to avoid issues in the future.", renamed: "KF.ImageResource") public typealias ImageResource = KF.ImageResource extension KF { /// ``ImageResource`` is a simple combination of ``downloadURL`` and ``cacheKey``. /// When passed to image view set methods, Kingfisher will try to download the target /// image from the ``downloadURL``, and then store it with the ``cacheKey`` as the key in cache. public struct ImageResource: Resource { // MARK: - Initializers /// Creates an image resource. /// /// - Parameters: /// - downloadURL: The target image URL from where the image can be downloaded. /// - cacheKey: /// The cache key. If `nil`, Kingfisher will use the `absoluteString` of ``ImageResource/downloadURL`` as /// the key. Default is `nil`. /// public init(downloadURL: URL, cacheKey: String? = nil) { self.downloadURL = downloadURL self.cacheKey = cacheKey ?? downloadURL.cacheKey } // MARK: Protocol Conforming /// The key used in cache. public let cacheKey: String /// The target image URL. public let downloadURL: URL } } /// URL conforms to ``Resource`` in Kingfisher. /// The `absoluteString` of this URL is used as ``cacheKey``. And the URL itself will be used as `downloadURL`. /// If you need customize the url and/or cache key, use `ImageResource` instead. extension URL: Resource { public var cacheKey: String { return isFileURL ? localFileCacheKey : absoluteString } public var downloadURL: URL { return self } } extension URL { static let localFileCacheKeyPrefix = "kingfisher.local.cacheKey" // The special version of cache key for a local file on disk. Every time the app is reinstalled on the disk, // the system assigns a new container folder to hold the .app (and the extensions, .appex) folder. So the URL for // the same image in bundle might be different. // // This getter only uses the fixed part in the URL (until the bundle name folder) to provide a stable cache key // for the image under the same path inside the bundle. // // See #1825 (https://github.com/onevcat/Kingfisher/issues/1825) var localFileCacheKey: String { var validComponents: [String] = [] for part in pathComponents.reversed() { validComponents.append(part) if part.hasSuffix(".app") || part.hasSuffix(".appex") { break } } let fixedPath = "\(Self.localFileCacheKeyPrefix)/\(validComponents.reversed().joined(separator: "/"))" if let q = query { return "\(fixedPath)?\(q)" } else { return fixedPath } } } ================================================ FILE: Sources/General/ImageSource/Source.swift ================================================ // // Source.swift // Kingfisher // // Created by onevcat on 2018/11/17. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents an image source setting for Kingfisher methods. /// /// A ``Source`` value indicates the way in which the target image can be retrieved and cached. /// /// - `network`: The target image should be retrieved from the network remotely. The associated ``Resource`` /// value defines detailed information like image URL and cache key. /// - `provider`: The target image should be provided in a data format. Normally, it can be an image /// from local storage or in any other encoding format (like Base64). /// public enum Source: Sendable { /// Represents the source task identifier when setting an image to a view with extension methods. public enum Identifier { /// The underlying value type of source identifier. public typealias Value = UInt @MainActor static private(set) var current: Value = 0 // Not thread safe. Expected to be always called on the main thread. @MainActor static func next() -> Value { current += 1 return current } } // MARK: Member Cases /// The target image should be fetched from the network remotely. The associated `Resource` /// value defines detailed information such as the image URL and cache key. case network(any Resource) /// The target image should be provided in a data format, typically as an image /// from local storage or in any other encoding format, such as Base64. case provider(any ImageDataProvider) // MARK: Getting Properties /// The cache key defined for this source value. public var cacheKey: String { switch self { case .network(let resource): return resource.cacheKey case .provider(let provider): return provider.cacheKey } } /// The URL defined for this source value. /// /// For a ``Source/network(_:)`` source, it is the ``Resource/downloadURL`` of associated ``Resource`` instance. /// For a ``Source/provider(_:)`` value, it is always `nil`. public var url: URL? { switch self { case .network(let resource): return resource.downloadURL case .provider(let provider): return provider.contentURL } } } extension Source: Hashable { public static func == (lhs: Source, rhs: Source) -> Bool { switch (lhs, rhs) { case (.network(let r1), .network(let r2)): return r1.cacheKey == r2.cacheKey && r1.downloadURL == r2.downloadURL case (.provider(let p1), .provider(let p2)): return p1.cacheKey == p2.cacheKey && p1.contentURL == p2.contentURL case (.provider(_), .network(_)): return false case (.network(_), .provider(_)): return false } } public func hash(into hasher: inout Hasher) { switch self { case .network(let r): hasher.combine(r.cacheKey) hasher.combine(r.downloadURL) case .provider(let p): hasher.combine(p.cacheKey) hasher.combine(p.contentURL) } } } extension Source { var asResource: (any Resource)? { guard case .network(let resource) = self else { return nil } return resource } } ================================================ FILE: Sources/General/KF.swift ================================================ // // KF.swift // Kingfisher // // Created by onevcat on 2020/09/21. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(UIKit) import UIKit #endif #if canImport(CarPlay) && !targetEnvironment(macCatalyst) import CarPlay #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit #endif #if canImport(WatchKit) import WatchKit #endif #if canImport(TVUIKit) import TVUIKit #endif /// A helper type to create image setting tasks in a builder pattern. /// /// Use methods in this type to create a ``KF/Builder`` instance and configure image tasks there. public enum KF { /// Creates a builder for a given ``Source``. /// - Parameter source: The ``Source`` object defines data information from network or a data provider. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its /// `Builder/set(to:)` to start the image loading. public static func source(_ source: Source?) -> KF.Builder { Builder(source: source) } /// Creates a builder for a given ``Resource``. /// - Parameter resource: The ``Resource`` object defines data information like key or URL. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its /// `Builder/set(to:)` to start the image loading. public static func resource(_ resource: (any Resource)?) -> KF.Builder { source(resource?.convertToSource()) } /// Creates a builder for a given `URL` and an optional cache key. /// - Parameters: /// - url: The URL where the image should be downloaded. /// - cacheKey: The key used to store the downloaded image in cache. /// If `nil`, the `absoluteString` of `url` is used as the cache key. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its /// `Builder/set(to:)` to start the image loading. public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder { source(url?.convertToSource(overrideCacheKey: cacheKey)) } /// Creates a builder for a given ``ImageDataProvider``. /// - Parameter provider: The ``ImageDataProvider`` object contains information about the data. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its /// `Builder/set(to:)` to start the image loading. public static func dataProvider(_ provider: (any ImageDataProvider)?) -> KF.Builder { source(provider?.convertToSource()) } /// Creates a builder for some given raw data and a cache key. /// - Parameters: /// - data: The data object from which the image should be created. /// - cacheKey: The key used to store the downloaded image in cache. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its /// `Builder/set(to:)` to start the image loading. public static func data(_ data: Data?, cacheKey: String) -> KF.Builder { if let data = data { return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) } else { return dataProvider(nil) } } } extension KF { /// A builder class to configure an image retrieving task and set it to a holder view or component. public class Builder: @unchecked Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.KF.Builder.propertyQueue") private let source: Source? #if os(watchOS) private var _placeholder: KFCrossPlatformImage? private var placeholder: KFCrossPlatformImage? { get { propertyQueue.sync { _placeholder } } set { propertyQueue.sync { _placeholder = newValue } } } #else private var _placeholder: (any Placeholder)? private var placeholder: (any Placeholder)? { get { propertyQueue.sync { _placeholder } } set { propertyQueue.sync { _placeholder = newValue } } } #endif private var _options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions) public var options: KingfisherParsedOptionsInfo { get { propertyQueue.sync { _options } } set { propertyQueue.sync { _options = newValue } } } public let onFailureDelegate = Delegate() public let onSuccessDelegate = Delegate() public let onProgressDelegate = Delegate<(Int64, Int64), Void>() init(source: Source?) { self.source = source } private var resultHandler: (@Sendable (Result) -> Void)? { { switch $0 { case .success(let result): self.onSuccessDelegate(result) case .failure(let error): self.onFailureDelegate(error) } } } private var progressBlock: DownloadProgressBlock? { onProgressDelegate.isSet ? { self.onProgressDelegate(($0, $1)) } : nil } } } @MainActor extension KF.Builder { #if !os(watchOS) /// Builds the image task request and sets it to an image view. /// - Parameter imageView: The image view which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? { imageView.kf.setImage( with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } /// Builds the image task request and sets it to an `NSTextAttachment` object. /// - Parameters: /// - attachment: The text attachment object which loads the task and should be set with the image. /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set( to attachment: NSTextAttachment, attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return attachment.kf.setImage( with: source, attributedView: attributedView, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #if canImport(UIKit) /// Builds the image task request and sets it to a button. /// - Parameters: /// - button: The button which loads the task and should be set with the image. /// - state: The button state to which the image should be set. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return button.kf.setImage( with: source, for: state, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } /// Builds the image task request and sets it to the background image for a button. /// - Parameters: /// - button: The button which loads the task and should be set with the image. /// - state: The button state to which the image should be set. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return button.kf.setBackgroundImage( with: source, for: state, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif // end of canImport(UIKit) #if canImport(CarPlay) && !targetEnvironment(macCatalyst) /// Builds the image task request and sets it to the image for a list item. /// - Parameters: /// - listItem: The list item which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @available(iOS 14.0, *) @discardableResult public func set(to listItem: CPListItem) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return listItem.kf.setImage( with: source, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) /// Builds the image task request and sets it to a button. /// - Parameter button: The button which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to button: NSButton) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return button.kf.setImage( with: source, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } /// Builds the image task request and sets it to the alternative image for a button. /// - Parameter button: The button which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func setAlternative(to button: NSButton) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return button.kf.setAlternateImage( with: source, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif // end of canImport(AppKit) #endif // end of !os(watchOS) #if canImport(WatchKit) /// Builds the image task request and sets it to a `WKInterfaceImage` object. /// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? { return interfaceImage.kf.setImage( with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif // end of canImport(WatchKit) #if canImport(TVUIKit) /// Builds the image task request and sets it to a TV monogram view. /// - Parameter monogramView: The monogram view which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @available(tvOS 12.0, *) @discardableResult public func set(to monogramView: TVMonogramView) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return monogramView.kf.setImage( with: source, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif // end of canImport(TVUIKit) } #if !os(watchOS) extension KF.Builder { #if os(iOS) || os(tvOS) || os(visionOS) /// Sets a placeholder which is used while retrieving the image. /// - Parameter placeholder: A placeholder to show while retrieving the image from its source. /// - Returns: A ``KF/Builder`` with changes applied. public func placeholder(_ placeholder: (any Placeholder)?) -> Self { self.placeholder = placeholder return self } #endif /// Sets a placeholder image which is used while retrieving the image. /// - Parameters: /// - image: An image to show while retrieving the image from its source. /// - Returns: A ``KF/Builder`` with changes applied. public func placeholder(_ image: KFCrossPlatformImage?) -> Self { self.placeholder = image return self } } #endif extension KF.Builder { #if os(iOS) || os(tvOS) || os(visionOS) /// Sets the transition for the image task. /// - Parameter transition: The desired transition effect when setting the image to image view. /// - Returns: A ``KF/Builder`` with changes applied. /// /// Kingfisher will use the `transition` parameter to animate the image in if it is downloaded from web. /// The transition will not happen when the image is retrieved from either memory or disk cache by default. /// If you need to do the transition even when the image being retrieved from cache, also call /// ``KFOptionSetter/forceRefresh(_:)`` on the returned ``KF/Builder``. public func transition(_ transition: ImageTransition) -> Self { options.transition = transition return self } /// Sets a fade transition for the image task. /// - Parameter duration: The duration of the fade transition. /// - Returns: A ``KF/Builder`` with changes applied. /// /// Kingfisher will use the `transition` parameter to animate the image in if it is downloaded from web. /// The transition will not happen when the image is retrieved from either memory or disk cache by default. /// If you need to do the transition even when the image being retrieved from cache, also call /// ``KFOptionSetter/forceRefresh(_:)`` on the returned ``KF/Builder``. public func fade(duration: TimeInterval) -> Self { options.transition = .fade(duration) return self } #endif /// Sets whether keeping the existing image of image view while setting another image to it. /// - Parameter enabled: Whether the existing image should be kept. /// - Returns: A ``KF/Builder`` with changes applied. /// /// By setting this option, the placeholder image parameter of image view extension method /// will be ignored and the current image will be kept while loading or downloading the new image. /// public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self { options.keepCurrentImageWhileLoading = enabled return self } /// Sets whether only the first frame from an animated image file should be loaded as a single image. /// - Parameter enabled: Whether the only the first frame should be loaded. /// - Returns: A ``KF/Builder`` with changes applied. /// /// Loading an animated images may take too much memory. It will be useful when you want to display a /// static preview of the first frame from an animated image. /// /// This option will be ignored if the target image is not animated image data. /// public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self { options.onlyLoadFirstFrame = enabled return self } } // MARK: - Deprecated extension KF.Builder { /// Starts the loading process of `self` immediately. /// /// By default, a ``KFImage`` will not load its source until the `onAppear` is called. This is a lazily loading /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once /// could help avoiding the flickering, with some performance trade-off. /// /// - Returns: The `Self` value with changes applied. @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") public func loadImmediately(_ start: Bool = true) -> Self { return self } } // MARK: - Redirect Handler extension KF { /// Represents the detail information when a task redirect happens. It is wrapping necessary information for a /// ``ImageDownloadRedirectHandler``. See that protocol for more information. public struct RedirectPayload { /// The related session data task when the redirect happens. It is /// the current ``SessionDataTask`` which triggers this redirect. public let task: SessionDataTask /// The response received during redirection. public let response: HTTPURLResponse /// The request for redirection which can be modified. public let newRequest: URLRequest /// A closure for being called with modified request. public let completionHandler: (URLRequest?) -> Void } } ================================================ FILE: Sources/General/KFOptionsSetter.swift ================================================ // // KFOptionsSetter.swift // Kingfisher // // Created by onevcat on 2020/12/22. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A protocol that Kingfisher can use to perform chained setting in builder pattern. @MainActor public protocol KFOptionSetter { var options: KingfisherParsedOptionsInfo { get nonmutating set } var onFailureDelegate: Delegate { get } var onSuccessDelegate: Delegate { get } var onProgressDelegate: Delegate<(Int64, Int64), Void> { get } } extension KF.Builder: KFOptionSetter { } final actor KFDelegateObserver { static let `default` = KFDelegateObserver() } // MARK: - Life cycles extension KFOptionSetter { /// Sets the progress block to current builder. /// /// - Parameter block: /// Called when the image downloading progress gets updated. If the response does not contain an /// [`expectedContentLength`](https://developer.apple.com/documentation/foundation/urlresponse/1413507-expectedcontentlength) /// in the received `URLResponse`, this block will not be called. If `block` is `nil`, the callback will be reset. /// /// - Returns: A `Self` value with changes applied. /// public func onProgress(_ block: DownloadProgressBlock?) -> Self { onProgressDelegate.delegate(on: KFDelegateObserver.default) { (_, result) in block?(result.0, result.1) } return self } /// Sets the done block to current builder. /// - Parameter block: Called when the image task successfully completes and the image set is done. If `block` /// is `nil`, the callback will be reset. /// - Returns: A `Self` with changes applied. /// public func onSuccess(_ block: ((RetrieveImageResult) -> Void)?) -> Self { onSuccessDelegate.delegate(on: KFDelegateObserver.default) { (_, result) in block?(result) } return self } /// Sets the catch block to current builder. /// - Parameter block: Called when an error happens during the image task. If `block` /// is `nil`, the callback will be reset. /// - Returns: A `Self` with changes applied. /// public func onFailure(_ block: ((KingfisherError) -> Void)?) -> Self { onFailureDelegate.delegate(on: KFDelegateObserver.default) { (_, error) in block?(error) } return self } } // MARK: - Basic options settings. extension KFOptionSetter { /// Sets the target image cache for this task. /// /// - Parameter cache: The target cache to be used for the task. /// - Returns: A `Self` value with changes applied. /// /// Kingfisher will utilize the associated ``ImageCache`` object when performing related operations, /// such as attempting to retrieve cached images and storing downloaded images within it. /// public func targetCache(_ cache: ImageCache) -> Self { options.targetCache = cache return self } /// Sets the target image cache to store the original downloaded image for this task. /// /// - Parameter cache: The target cache is about to be used for storing the original downloaded image from the task. /// - Returns: A `Self` value with changes applied. /// /// The ``ImageCache`` for storing and retrieving original images. If ``KingfisherOptionsInfoItem/originalCache(_:)`` /// is contained in the options, it will be preferred for storing and retrieving original images. /// If there is no ``KingfisherOptionsInfoItem/originalCache(_:)`` in the options, /// ``KingfisherOptionsInfoItem/targetCache(_:)`` will be used to store original images. /// /// When using ``KingfisherManager`` to download and store an image, if /// ``KingfisherOptionsInfoItem/cacheOriginalImage`` is applied in the option, the original image will be stored to /// the `cache` you pass as parameter in this method. At the same time, if a requested final image (with processor /// applied) cannot be found in the cache defined by ``KingfisherOptionsInfoItem/targetCache(_:)``, Kingfisher /// will try to search the original image to check whether it is already there. If found, it will be used and /// applied with the given processor. It is an optimization for not downloading the same image for multiple times. /// public func originalCache(_ cache: ImageCache) -> Self { options.originalCache = cache return self } /// Sets the downloader to be used for the image download task. /// /// - Parameter downloader: The `ImageDownloader` instance to use for downloading. /// - Returns: A `Self` value with the changes applied. /// /// Kingfisher will utilize the specified ``ImageDownloader`` instance to download requested images. /// public func downloader(_ downloader: ImageDownloader) -> Self { options.downloader = downloader return self } /// Sets the download priority for the image task. /// /// - Parameter priority: The download priority of the image download task. /// - Returns: A `Self` value with changes applied. /// /// The `priority` value will be configured as the priority of the image download task. Valid values range between /// 0.0 and 1.0. You can select a value from `URLSessionTask.defaultPriority`, `URLSessionTask.lowPriority`, /// or `URLSessionTask.highPriority`. If this option is not set, the default value /// (`URLSessionTask.defaultPriority`) will be used. /// public func downloadPriority(_ priority: Float) -> Self { options.downloadPriority = priority return self } /// Sets whether Kingfisher should ignore the cache and attempt to initiate a download task for the image source. /// /// - Parameter enabled: Enable force refresh or not. /// - Returns: A `Self` value with the changes applied. /// public func forceRefresh(_ enabled: Bool = true) -> Self { options.forceRefresh = enabled return self } /// Sets whether Kingfisher should attempt to retrieve the image from the memory cache first. If the image is not /// found in the memory cache, it bypasses the disk cache and initiates a download task for the image source. /// /// - Parameter enabled: Enable memory-only cache searching or not. /// - Returns: A `Self` value with the changes applied. /// /// This option is useful when you want to display a changeable image with the same URL during the same app session /// while avoiding multiple downloads of the same image. /// public func fromMemoryCacheOrRefresh(_ enabled: Bool = true) -> Self { options.fromMemoryCacheOrRefresh = enabled return self } /// Sets whether the image should be cached only in memory and not on disk. /// /// - Parameter enabled: Enable memory-only caching for the image or not. /// - Returns: A `Self` value with the changes applied. /// public func cacheMemoryOnly(_ enabled: Bool = true) -> Self { options.cacheMemoryOnly = enabled return self } /// Sets whether Kingfisher should wait for caching operations to be completed before invoking the `onSuccess` /// or `onFailure` block. /// /// - Parameter enabled: Enable waiting for caching operations or not. /// - Returns: A `Self` value with the changes applied. /// public func waitForCache(_ enabled: Bool = true) -> Self { options.waitForCache = enabled return self } /// Sets whether Kingfisher should exclusively attempt to retrieve the image from the cache and not from the network. /// /// - Parameter enabled: Enable cache-only image retrieval or not. /// - Returns: A `Self` value with the changes applied. /// /// If the image is not found in the cache, the image retrieval will fail with a /// ``KingfisherError/CacheErrorReason/imageNotExisting(key:)`` error. /// public func onlyFromCache(_ enabled: Bool = true) -> Self { options.onlyFromCache = enabled return self } /// Sets whether the image should be decoded on a background thread before usage. /// /// - Parameter enabled: Enable background image decoding or not. /// - Returns: A `Self` value with the changes applied. /// /// When set to `true`, the downloaded image data will be decoded and undergo off-screen rendering to extract pixel /// information in the background. This can enhance display speed but may consume additional time and memory for /// image preparation before usage. /// public func backgroundDecode(_ enabled: Bool = true) -> Self { options.backgroundDecode = enabled return self } /// Sets the callback queue used as the target queue for dispatching callbacks when retrieving images from the /// cache. If not set, Kingfisher will use the main queue for callbacks. /// /// - Parameter queue: The target queue on which cache retrieval callbacks will be invoked. /// - Returns: A `Self` value with the changes applied. /// /// - Note: This option does not impact callbacks for UI-related extension methods or ``KFImage`` result handlers. /// Callbacks for those methods will always be executed on the main queue. /// public func callbackQueue(_ queue: CallbackQueue) -> Self { options.callbackQueue = queue return self } /// Sets the scale factor value used when converting retrieved data to an image. /// /// - Parameter factor: The scale factor value to use. /// - Returns: A `Self` value with the changes applied. /// /// Specify the image scale factor, which may differ from your screen's scale. This is particularly important when /// working with 2x or 3x retina images. Failure to set the correct scale factor may result in Kingfisher /// converting the data to an image object with a `scale` of 1.0. /// public func scaleFactor(_ factor: CGFloat) -> Self { options.scaleFactor = factor return self } /// Sets whether the original image should be cached, even when the original image has been processed by other ``ImageProcessor``s. /// /// - Parameter enabled: Whether to cache the original image. /// - Returns: A `Self` value with the changes applied. /// /// When this option is set, and an ``ImageProcessor`` is used, Kingfisher will attempt to cache both the final /// processed image and the original image. This ensures that the original image can be reused when another /// processor is applied to the same resource, without the need for redownloading. You can use /// ``KingfisherOptionsInfoItem/originalCache(_:)`` to specify a cache for the original images. /// /// - Note: The original image will be cached only in disk storage. /// public func cacheOriginalImage(_ enabled: Bool = true) -> Self { options.cacheOriginalImage = enabled return self } /// Sets writing options for an original image on its initial write to disk storage. /// /// - Parameter writingOptions: Options that control the data writing operation to disk storage. /// - Returns: A `Self` value with the changes applied. /// /// If these options are set, they will be applied to the storage operation for new files. This can be useful if /// you want to implement features such as file encryption on the initial write, for example, /// using `[.completeFileProtection]`. /// public func diskStoreWriteOptions(_ writingOptions: Data.WritingOptions) -> Self { options.diskStoreWriteOptions = writingOptions return self } /// Sets whether disk storage loading should occur in the same calling queue. /// /// - Parameter enabled: Whether disk storage loading should happen in the same calling queue. /// - Returns: A `Self` value with the changes applied. /// /// By default, disk storage file loading operates in its own queue with asynchronous dispatch behavior. While this /// provides better non-blocking disk loading performance, it can result in flickering when reloading an image /// from disk if the image view already has an image set. /// /// Enabling this option prevents flickering by performing all loading in the same queue (typically the UI queue if /// you are using Kingfisher's extension methods to set an image). However, this may come at the cost of loading /// performance. /// /// - Note: When using SwiftUI components (e.g., `KFImage`), this option is enabled by default to prevent /// flickering during view updates. This is essential for maintaining visual consistency in SwiftUI's declarative /// environment. For UIKit/AppKit usage, the default remains `false` for optimal performance. /// public func loadDiskFileSynchronously(_ enabled: Bool = true) -> Self { options.loadDiskFileSynchronously = enabled return self } /// Sets the queue on which image processing should occur. /// /// - Parameter queue: The queue on which image processing should take place. /// - Returns: A `Self` value with the changes applied. /// /// By default, Kingfisher employs a pre-defined serial queue for image processing. Use this option to modify this /// behavior. For example, specify `.mainCurrentOrAsync` to process the image on the main queue, which can prevent /// potential flickering but may lead to UI blocking if the processor requires substantial time to execute. /// public func processingQueue(_ queue: CallbackQueue?) -> Self { options.processingQueue = queue return self } /// Sets the alternative sources to be used when loading the original input `Source` fails. /// /// - Parameter sources: The alternative sources to be used. /// - Returns: A `Self` value with the changes applied. /// /// The values in the `sources` array will be employed to initiate a new image loading task if the previous task /// fails due to an error. The image source loading process will terminate as soon as one of the alternative /// sources is successfully loaded. If all `sources` are used but loading still fails, /// a ``KingfisherError/ImageSettingErrorReason/alternativeSourcesExhausted(_:)`` error will be thrown in the /// `catch` block. /// /// This feature is valuable when implementing a fallback solution for setting images. /// /// - Note: User cancellation or calling on ``DownloadTask/cancel()`` on ``DownloadTask`` will not trigger the /// loading of alternative sources. /// public func alternativeSources(_ sources: [Source]?) -> Self { options.alternativeSources = sources return self } /// Sets a retry strategy to be used when issues arise during image retrieval. /// /// - Parameter strategy: The provided strategy that defines how retry attempts should occur. /// - Returns: A `Self` value with the changes applied. /// public func retry(_ strategy: (any RetryStrategy)?) -> Self { options.retryStrategy = strategy return self } /// Sets a retry strategy with a maximum retry count and retry interval. /// /// - Parameters: /// - maxCount: The maximum number of retry attempts before the retry stops. /// - interval: The time interval between each retry attempt. /// - Returns: A `Self` value with the changes applied. /// /// This defines a straightforward retry strategy that retries a failing request for a specified number of times /// with a designated time interval between each attempt. For example, `.retry(maxCount: 3, interval: .second(3))` /// indicates a maximum of three retry attempts, with a 3-second pause between each retry if the previous attempt /// fails. /// public func retry(maxCount: Int, interval: DelayRetryStrategy.Interval = .seconds(3)) -> Self { let strategy = DelayRetryStrategy(maxRetryCount: maxCount, retryInterval: interval) options.retryStrategy = strategy return self } /// Sets the `Source` to be loaded when the user enables Low Data Mode and the original source fails with an /// `NSURLErrorNetworkUnavailableReason.constrained` error. /// /// - Parameter source: The `Source` to be loaded under low data mode. /// - Returns: A `Self` value with the changes applied. /// /// When this option is set, the `allowsConstrainedNetworkAccess` property of the request for the original source /// will be set to `false`, and the specified ``Source`` will be used to retrieve the image in low data mode. /// Typically, you can provide a low-resolution version of your image or a local image provider to display a /// placeholder. /// /// If this option is not set or the `source` is `nil`, the device's Low Data Mode setting will be disregarded, /// and the original source will be loaded following the system's default behavior in a regular manner. /// public func lowDataModeSource(_ source: Source?) -> Self { options.lowDataModeSource = source return self } /// Sets whether the image setting for an image view should include a transition even when the image is retrieved /// from the cache. /// /// - Parameter enabled: Enable the use of a transition or not. /// - Returns: A `Self` value with the changes applied. /// public func forceTransition(_ enabled: Bool = true) -> Self { options.forceTransition = enabled return self } /// Sets the image to be used in the event of a failure during image retrieval. /// /// - Parameter image: The image to be used when an error occurs. /// - Returns: A `Self` value with the changes applied. /// /// If this option is set and an image retrieval error occurs, Kingfisher will use the provided image (or an empty /// image) in place of the requested one. This is useful when you do not want to display a placeholder during the /// loading process but prefer to use a default image when requests fail. /// public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self { options.onFailureImage = .some(image) return self } } // MARK: - Request Modifier extension KFOptionSetter { /// Sets an ``ImageDownloadRequestModifier`` to alter the image download request before it is sent. /// /// - Parameter modifier: The modifier to be used for changing the request before it is sent. /// - Returns: A `Self` value with the changes applied. /// /// This is your last opportunity to modify the image download request. You can use this for customization /// purposes, such as adding an authentication token to the header, implementing basic HTTP authentication, /// or URL mapping. public func requestModifier(_ modifier: any AsyncImageDownloadRequestModifier) -> Self { options.requestModifier = modifier return self } /// Sets a block to modify the image download request before it is sent. /// /// - Parameter modifyBlock: The modifying block that will be called to change the request before it is sent. /// - Returns: A `Self` value with the changes applied. /// /// This is your last opportunity to modify the image download request. You can use this for customization purposes, /// such as adding an authentication token to the header, implementing basic HTTP authentication, or URL mapping. /// public func requestModifier(_ modifyBlock: @escaping @Sendable (inout URLRequest) -> Void) -> Self { options.requestModifier = AnyModifier { r -> URLRequest? in var request = r modifyBlock(&request) return request } return self } } // MARK: - Redirect Handler extension KFOptionSetter { /// Sets an `ImageDownloadRedirectHandler` to modify the image download request during redirection. /// /// - Parameter handler: The handler to be used for redirection. /// - Returns: A `Self` value with the changes applied. /// /// This provides an opportunity to modify the image download request during redirection. You can use this for /// customization purposes, such as adding an authentication token to the header, implementing basic HTTP /// authentication, or URL mapping. By default, the original redirection request will be sent without any /// modification. /// public func redirectHandler(_ handler: any ImageDownloadRedirectHandler) -> Self { options.redirectHandler = handler return self } /// Sets a block to modify the image download request during redirection. /// /// - Parameter block: The block to be used for redirection. /// - Returns: A `Self` value with the changes applied. /// /// This provides an opportunity to modify the image download request during redirection. You can use this for /// customization purposes, such as adding an authentication token to the header, implementing basic HTTP /// authentication, or URL mapping. By default, the original redirection request will be sent without any /// modification. /// public func redirectHandler(_ block: @escaping @Sendable (KF.RedirectPayload) -> Void) -> Self { let redirectHandler = AnyRedirectHandler { (task, response, request, handler) in let payload = KF.RedirectPayload( task: task, response: response, newRequest: request, completionHandler: handler ) block(payload) } options.redirectHandler = redirectHandler return self } } // MARK: - Processor extension KFOptionSetter { /// Sets an image processor for the image task, replacing the current image processor settings. /// /// - Parameter processor: The processor to use for processing the image after it is downloaded. /// - Returns: A `Self` value with the changes applied. /// /// - Note: To append a processor to the current ones instead of replacing them all, use ``appendProcessor(_:)``. /// public func setProcessor(_ processor: any ImageProcessor) -> Self { options.processor = processor return self } /// Enables progressive image loading with a specified `ImageProgressive` setting to process the /// progressive JPEG data and display it in a progressive way. /// - Parameter progressive: The progressive settings which is used while loading. /// - Returns: A ``KF/Builder`` with changes applied. public func progressiveJPEG(_ progressive: ImageProgressive? = .init()) -> Self { options.progressiveJPEG = progressive return self } /// Sets an array of image processors for the image task, replacing the current image processor settings. /// /// - Parameter processors: An array of processors. The processors in this array will be concatenated one by one to /// form a processor pipeline. /// - Returns: A `Self` value with the changes applied. /// /// - Note: To append processors to the current ones instead of replacing them all, concatenate them using the /// `|>` operator, and then use ``KFOptionSetter/appendProcessor(_:)``. /// public func setProcessors(_ processors: [any ImageProcessor]) -> Self { switch processors.count { case 0: options.processor = DefaultImageProcessor.default case 1...: options.processor = processors.dropFirst().reduce(processors[0]) { $0 |> $1 } default: assertionFailure("Never happen") } return self } /// Appends a processor to the current set of processors. /// /// - Parameter processor: The processor to append to the current processor settings. /// - Returns: A `Self` value with the changes applied. /// public func appendProcessor(_ processor: any ImageProcessor) -> Self { options.processor = options.processor |> processor return self } /// Appends a ``RoundCornerImageProcessor`` to the current set of processors. /// /// - Parameters: /// - radius: The radius to apply during processing. Specify a certain point value with `.point`, or a fraction /// of the target image with `.widthFraction` or `.heightFraction`. For example, with a square image where width /// and height are equal, `.widthFraction(0.5)` means using half of the length of the size to make the final /// image round. /// - targetSize: The target size for the output image. If `nil`, the image will retain its original size after /// processing. /// - corners: The target corners to round. /// - backgroundColor: The background color of the output image. If `nil`, a transparent background will be used. /// - Returns: A `Self` value with the changes applied. /// public func roundCorner( radius: Radius, targetSize: CGSize? = nil, roundingCorners corners: RectCorner = .all, backgroundColor: KFCrossPlatformColor? = nil ) -> Self { let processor = RoundCornerImageProcessor( radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor ) return appendProcessor(processor) } /// Appends a ``BlurImageProcessor`` to the current set of processors. /// /// - Parameter radius: The blur radius for simulating Gaussian blur. /// - Returns: A `Self` value with the changes applied. /// public func blur(radius: CGFloat) -> Self { appendProcessor( BlurImageProcessor(blurRadius: radius) ) } /// Appends an ``OverlayImageProcessor`` to the current set of processors. /// /// - Parameters: /// - color: The overlay color to be used when overlaying the input image. /// - fraction: The fraction to be used when overlaying the color onto the image. /// - Returns: A `Self` value with the changes applied. /// public func overlay(color: KFCrossPlatformColor, fraction: CGFloat = 0.5) -> Self { appendProcessor( OverlayImageProcessor(overlay: color, fraction: fraction) ) } /// Appends a ``TintImageProcessor`` to the current set of processors. /// /// - Parameter color: The tint color to be used for tinting the input image. /// - Returns: A `Self` value with the changes applied. /// public func tint(color: KFCrossPlatformColor) -> Self { appendProcessor( TintImageProcessor(tint: color) ) } /// Appends a ``BlackWhiteProcessor`` to the current set of processors. /// /// - Returns: A `Self` value with the changes applied. /// public func blackWhite() -> Self { appendProcessor( BlackWhiteProcessor() ) } /// Appends a ``CroppingImageProcessor`` to the current set of processors. /// /// - Parameters: /// - size: The target size for the output image. /// - anchor: The anchor point from which the output size should be calculated. The anchor point is represented /// by two values between 0.0 and 1.0, indicating a relative point in the current image. See /// ``CroppingImageProcessor/init(size:anchor:)`` for more details. /// - Returns: A `Self` value with the changes applied. /// public func cropping(size: CGSize, anchor: CGPoint = .init(x: 0.5, y: 0.5)) -> Self { appendProcessor( CroppingImageProcessor(size: size, anchor: anchor) ) } /// Appends a ``DownsamplingImageProcessor`` to the current set of processors. /// /// Compared to the ``ResizingImageProcessor``, the ``DownsamplingImageProcessor`` doesn't render the original /// images and then resize them. Instead, it directly downsamples the input data to a thumbnail image, making it /// more efficient than the ``ResizingImageProcessor``. It is recommended to use the ``DownsamplingImageProcessor`` /// whenever possible instead of the ``ResizingImageProcessor``. /// /// - 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. /// - Returns: A `Self` value with the changes applied. /// /// - Note: Only CG-based images are supported, and animated images (e.g., GIF) are not supported. /// public func downsampling(size: CGSize) -> Self { let processor = DownsamplingImageProcessor(size: size) if options.processor == DefaultImageProcessor.default { return setProcessor(processor) } else { return appendProcessor(processor) } } /// Appends a ``ResizingImageProcessor`` to the current set of processors. /// /// If you need to resize a data-represented image to a smaller size, it is recommended to use the /// ``DownsamplingImageProcessor`` instead, which is more efficient and uses less memory. /// /// - Parameters: /// - referenceSize: The reference size for the resizing operation in points. /// - mode: The target content mode for the output image. The default is `.none`. /// - Returns: A `Self` value with the changes applied. /// public func resizing(referenceSize: CGSize, mode: ContentMode = .none) -> Self { appendProcessor( ResizingImageProcessor(referenceSize: referenceSize, mode: mode) ) } } // MARK: - Cache Serializer extension KFOptionSetter { /// Uses a specified ``CacheSerializer`` to convert data to an image object for retrieval from the disk cache or /// vice versa for storage to the disk cache. /// /// - Parameter cacheSerializer: The ``CacheSerializer`` to be used. /// - Returns: A `Self` value with the changes applied. /// public func serialize(by cacheSerializer: any CacheSerializer) -> Self { options.cacheSerializer = cacheSerializer return self } /// Uses a specified format to serialize the image data to disk. It converts the image object to the given data /// format. /// /// - Parameters: /// - format: The desired data encoding format when storing the image on disk. /// - jpegCompressionQuality: If the format is ``ImageFormat/JPEG``, it specifies the compression quality when /// converting the image to JPEG data. Otherwise, it is ignored. /// - Returns: A `Self` value with the changes applied. /// public func serialize(as format: ImageFormat, jpegCompressionQuality: CGFloat? = nil) -> Self { let cacheSerializer: FormatIndicatedCacheSerializer switch format { case .JPEG: cacheSerializer = .jpeg(compressionQuality: jpegCompressionQuality ?? 1.0) case .PNG: cacheSerializer = .png case .GIF: cacheSerializer = .gif case .unknown: cacheSerializer = .png } options.cacheSerializer = cacheSerializer return self } } // MARK: - Image Modifier extension KFOptionSetter { /// Sets an ``ImageModifier`` for the image task. Use this to modify the fetched image object's properties if needed. /// /// If the image was fetched directly from the downloader, the modifier will run directly after the /// ``ImageProcessor``. If the image is being fetched from a cache, the modifier will run after the /// ``CacheSerializer``. /// /// - Parameter modifier: The ``ImageModifier`` to be used for modifying the image object. /// - Returns: A `Self` value with the changes applied. /// public func imageModifier(_ modifier: (any ImageModifier)?) -> Self { options.imageModifier = modifier return self } /// Sets a block to modify the image object. Use this to modify the fetched image object's properties if needed. /// /// If the image was fetched directly from the downloader, the modifier block will run directly after the /// ``ImageProcessor``. If the image is being fetched from a cache, the modifier will run after the /// ``CacheSerializer``. /// /// - Parameter block: The block used to modify the image object. /// - Returns: A `Self` value with the changes applied. /// public func imageModifier(_ block: @escaping @Sendable (inout KFCrossPlatformImage) throws -> Void) -> Self { let modifier = AnyImageModifier { image -> KFCrossPlatformImage in var image = image try block(&image) return image } options.imageModifier = modifier return self } } // MARK: - Cache Expiration extension KFOptionSetter { /// Sets the expiration setting for the memory cache of this image task. /// /// By default, the underlying ``MemoryStorage/Backend`` uses the expiration in its configuration for all items. /// If set, the ``MemoryStorage/Backend`` will use this value to overwrite the configuration setting for this /// caching item. /// /// - Parameter expiration: The expiration setting used in cache storage. /// - Returns: A `Self` value with the changes applied. /// public func memoryCacheExpiration(_ expiration: StorageExpiration?) -> Self { options.memoryCacheExpiration = expiration return self } /// Sets the expiration extending setting for the memory cache. The item expiration time will be incremented by this /// value after access. /// /// By default, the underlying ``MemoryStorage/Backend`` uses the initial cache expiration as the extending value: /// ``ExpirationExtending/cacheTime``. /// /// To disable the extending option entirely, set `.none` to it. /// /// - Parameter extending: The expiration extending setting used in cache storage. /// - Returns: A `Self` value with the changes applied. /// public func memoryCacheAccessExtending(_ extending: ExpirationExtending) -> Self { options.memoryCacheAccessExtendingExpiration = extending return self } /// Sets the expiration setting for the disk cache of this image task. /// /// By default, the underlying ``DiskStorage/Backend`` uses the expiration in its configuration for all items. /// If set, the ``DiskStorage/Backend`` will use this value to overwrite the configuration setting for this caching /// item. /// /// - Parameter expiration: The expiration setting used in cache storage. /// - Returns: A `Self` value with the changes applied. /// public func diskCacheExpiration(_ expiration: StorageExpiration?) -> Self { options.diskCacheExpiration = expiration return self } /// Sets the expiration extending setting for the disk cache. The item expiration time will be incremented by this /// value after access. /// /// By default, the underlying ``DiskStorage/Backend`` uses the initial cache expiration as the extending /// value: ``ExpirationExtending/cacheTime``. /// /// To disable the extending option entirely, set `.none` to it. /// /// - Parameter extending: The expiration extending setting used in cache storage. /// - Returns: A `Self` value with the changes applied. /// public func diskCacheAccessExtending(_ extending: ExpirationExtending) -> Self { options.diskCacheAccessExtendingExpiration = extending return self } } ================================================ FILE: Sources/General/Kingfisher.swift ================================================ // // Kingfisher.swift // Kingfisher // // Created by Wei Wang on 16/9/14. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import ImageIO #if os(macOS) import AppKit public typealias KFCrossPlatformImage = NSImage public typealias KFCrossPlatformView = NSView public typealias KFCrossPlatformColor = NSColor public typealias KFCrossPlatformImageView = NSImageView public typealias KFCrossPlatformButton = NSButton // `NSImage` is not yet Sendable. We have to assume it sendable to resolve warnings in Kingfisher. #if compiler(>=6) extension KFCrossPlatformImage: @retroactive @unchecked Sendable { } #else extension KFCrossPlatformImage: @unchecked Sendable { } #endif // compiler(>=6) #else // os(macOS) import UIKit public typealias KFCrossPlatformImage = UIImage public typealias KFCrossPlatformColor = UIColor #if !os(watchOS) public typealias KFCrossPlatformImageView = UIImageView public typealias KFCrossPlatformView = UIView public typealias KFCrossPlatformButton = UIButton #if canImport(TVUIKit) import TVUIKit #endif // canImport(TVUIKit) #if canImport(CarPlay) && !targetEnvironment(macCatalyst) import CarPlay #endif // canImport(CarPlay) && !targetEnvironment(macCatalyst) #else // !os(watchOS) import WatchKit #endif // !os(watchOS) #endif // os(macOS) /// Wrapper for Kingfisher compatible types. This type provides an extension point for /// convenience methods in Kingfisher. public struct KingfisherWrapper: @unchecked Sendable { public let base: Base public init(_ base: Base) { self.base = base } } /// Represents an object type that is compatible with Kingfisher. You can use ``kf`` property to get a /// value in the namespace of Kingfisher. /// /// In Kingfisher, most of related classes that contains an image (such as `UIImage`, `UIButton`, `NSImageView` and /// more) conform to this protocol, and provides the helper methods for setting an image easily. You can access the `kf` /// property and call its `setImage` method with a certain URL: /// /// ```swift /// let imageView: UIImageView /// let url = URL(string: "https://example.com/image.jpg") /// imageView.kf.setImage(with: url) /// ``` /// /// For more about basic usage of Kingfisher, check the documentation. public protocol KingfisherCompatible: AnyObject { } /// Represents a value type that is compatible with Kingfisher. You can use ``kf`` property to get a /// value in the namespace of Kingfisher. public protocol KingfisherCompatibleValue {} extension KingfisherCompatible { /// Gets a namespace holder for Kingfisher compatible types. public var kf: KingfisherWrapper { get { return KingfisherWrapper(self) } set { } } } extension KingfisherCompatibleValue { /// Gets a namespace holder for Kingfisher compatible types. public var kf: KingfisherWrapper { get { return KingfisherWrapper(self) } set { } } } extension KFCrossPlatformImage : KingfisherCompatible { } #if !os(watchOS) extension KFCrossPlatformImageView : KingfisherCompatible { } extension KFCrossPlatformButton : KingfisherCompatible { } extension NSTextAttachment : KingfisherCompatible { } #else extension WKInterfaceImage : KingfisherCompatible { } #endif #if canImport(PhotosUI) && !os(watchOS) import PhotosUI extension PHLivePhotoView : KingfisherCompatible { } #endif #if os(tvOS) && canImport(TVUIKit) @available(tvOS 12.0, *) extension TVMonogramView : KingfisherCompatible { } #endif #if canImport(CarPlay) && !targetEnvironment(macCatalyst) @available(iOS 14.0, *) extension CPListItem : KingfisherCompatible { } #endif ================================================ FILE: Sources/General/KingfisherError.swift ================================================ // // KingfisherError.swift // Kingfisher // // Created by onevcat on 2018/09/26. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation #if os(macOS) import AppKit #else import UIKit #endif extension Never {} /// Represents all the errors that can occur in the Kingfisher framework. /// /// Kingfisher-related methods always throw a ``KingfisherError`` or invoke the callback with ``KingfisherError`` /// as its error type. To handle errors from Kingfisher, you switch over the error to get a reason catalog, /// then switch over the reason to understand the error details. /// public enum KingfisherError: Error { // MARK: Error Reason Types /// Represents the error reasons during the networking request phase. public enum RequestErrorReason: Sendable { /// The request is empty. /// /// Error Code: 1001 case emptyRequest /// The URL of the request is invalid. /// /// - Parameter request: The request is intended to be sent, but its URL is invalid. /// /// Error Code: 1002 case invalidURL(request: URLRequest) /// The downloading task is canceled by the user. /// /// - Parameters: /// - task: The session data task which is canceled. /// - token: The cancel token which is used for canceling the task. /// /// Error Code: 1003 case taskCancelled(task: SessionDataTask, token: SessionDataTask.CancelToken) /// The live photo downloading task is canceled by the user. /// /// - Parameters: /// - source: The live phot source. /// /// Error Code: 1004 case livePhotoTaskCancelled(source: LivePhotoSource) case asyncTaskContextCancelled } /// Represents the error reason during networking response phase. public enum ResponseErrorReason: Sendable { /// The response is not a valid URL response. /// /// - Parameters: /// - response: The received invalid URL response. /// The response is expected to be an HTTP response, but it is not. /// /// Error Code: 2001 case invalidURLResponse(response: URLResponse) /// The response contains an invalid HTTP status code. /// /// - Parameters: /// - response: The received response. /// /// Error Code: 2002 /// /// - Note: By default, status code 200..<400 is recognized as valid. You can override /// this behavior by conforming to the `ImageDownloaderDelegate`. case invalidHTTPStatusCode(response: HTTPURLResponse) /// An error happens in the system URL session. /// /// - Parameters: /// - error: The underlying URLSession error object. /// /// Error Code: 2003 case URLSessionError(error: any Error) /// Data modifying fails on returning a valid data. /// /// - Parameters: /// - task: The failed task. /// /// Error Code: 2004 case dataModifyingFailed(task: SessionDataTask) /// The task is done but no URL response found. /// /// - Parameters: /// - task: The failed task. /// /// Error Code: 2005 case noURLResponse(task: SessionDataTask) /// The task is cancelled by ``ImageDownloaderDelegate`` due to the `.cancel` response disposition is /// specified by the delegate method. /// /// - Parameters: /// - task: The cancelled task. /// /// Error Code: 2006 case cancelledByDelegate(response: URLResponse) } /// Represents the error reason during Kingfisher caching. public enum CacheErrorReason: @unchecked Sendable { /// Cannot create a file enumerator for a certain disk URL. /// /// - Parameters: /// - url: The target disk URL from which the file enumerator should be created. /// /// Error Code: 3001 case fileEnumeratorCreationFailed(url: URL) /// Cannot get correct file contents from a file enumerator. /// /// - Parameters: /// - url: The target disk URL from which the content of a file enumerator should be obtained. /// /// Error Code: 3002 case invalidFileEnumeratorContent(url: URL) /// The file at the target URL exists, but its URL resource is unavailable. /// /// - Parameters: /// - error: The underlying error thrown by the file manager. /// - key: The key used to retrieve the resource from cache. /// - url: The disk URL where the target cached file exists. /// /// Error Code: 3003 case invalidURLResource(error: any Error, key: String, url: URL) /// The file at the target URL exists, but the data cannot be loaded from it. /// /// - Parameters: /// - url: The disk URL where the target cached file exists. /// - error: The underlying error that describes why this error occurs. /// /// Error Code: 3004 case cannotLoadDataFromDisk(url: URL, error: any Error) /// Cannot create a folder at a given path. /// /// - Parameters: /// - path: The disk path where the directory creation operation fails. /// - error: The underlying error that describes why this error occurs. /// /// Error Code: 3005 case cannotCreateDirectory(path: String, error: any Error) /// The requested image does not exist in the cache. /// /// - Parameters: /// - key: The key of the requested image in the cache. /// /// Error Code: 3006 case imageNotExisting(key: String) /// Unable to convert an object to data for storage. /// /// - Parameters: /// - object: The object that needs to be converted to data. /// /// Error Code: 3007 case cannotConvertToData(object: Any, error: any Error) /// Unable to serialize an image to data for storage. /// /// - Parameters: /// - image: The input image that needs to be serialized to cache. /// - original: The original image data, if it exists. /// - serializer: The ``CacheSerializer`` used for the image serialization. /// /// Error Code: 3008 case cannotSerializeImage(image: KFCrossPlatformImage?, original: Data?, serializer: any CacheSerializer) /// Unable to create the cache file at a specified `fileURL` under a given `key`. /// /// - Parameters: /// - fileURL: The URL where the cache file should be created. /// - key: The cache key used for the cache. When caching a file through ``KingfisherManager`` and Kingfisher's /// extension method, it is the resolved cache key based on your input ``Source`` and the image /// processors. /// - data: The data to be cached. /// - error: The underlying error originally thrown by Foundation when attempting to write the `data` to the disk file at /// `fileURL`. /// /// Error Code: 3009 case cannotCreateCacheFile(fileURL: URL, key: String, data: Data, error: any Error) /// Unable to set file attributes for a cached file. /// /// - Parameters: /// - filePath: The path of the target cache file. /// - attributes: The file attributes to be set for the target file. /// - error: The underlying error originally thrown by the Foundation framework when attempting to set the specified /// `attributes` for the disk file at `filePath`. /// /// Error Code: 3010 case cannotSetCacheFileAttribute(filePath: String, attributes: [FileAttributeKey : Any], error: any Error) /// The disk storage for caching is not ready. /// /// - Parameters: /// - cacheURL: The intended URL that should be the storage folder. /// /// This issue typically arises due to an extreme lack of space on the disk storage. Kingfisher fails to create /// the cache folder under these circumstances, rendering the disk storage unusable. In such cases, it is /// recommended to prompt the user to free up storage space and restart the app to restore functionality. /// /// Error Code: 3011 case diskStorageIsNotReady(cacheURL: URL) /// The resource is expected on the disk, but now missing for some reason. /// /// This happens when the expected resource is not on the disk for some reason during loading a live photo. /// /// Error Code: 3012 case missingLivePhotoResourceOnDisk(_ resource: LivePhotoResource) } /// Represents the error reason during image processing phase. public enum ProcessorErrorReason: Sendable { /// Image processing has failed, and there is no valid output image generated by the processor. /// /// - Parameters: /// - processor: The `ImageProcessor` responsible for processing the image or its data in `item`. /// - item: The image or its data content. /// /// Error Code: 4001 case processingFailed(processor: any ImageProcessor, item: ImageProcessItem) } /// Represents the error reason during image setting in a view related class. public enum ImageSettingErrorReason: Sendable { /// The input resource is empty or `nil`. /// /// Error Code: 5001 case emptySource /// The resource task is completed, but it is not the one that was expected. This typically occurs when you set /// another resource on the view without canceling the current ongoing task. The previous task will fail with the /// `.notCurrentSourceTask` error when a result is obtained, regardless of whether it was successful or not for /// that task. /// /// - Parameters: /// - result: The `RetrieveImageResult` if the source task is completed without any issues. `nil` if an error occurred. /// - error: The `Error` if there was a problem during the image setting task. `nil` if the task completed successfully. /// - source: The original source value of the task. /// /// Error Code: 5002 case notCurrentSourceTask(result: RetrieveImageResult?, error: (any Error)?, source: Source) /// An error occurs while retrieving data from an `ImageDataProvider`. /// /// - Parameters: /// - provider: The ``ImageDataProvider`` that encountered the error. /// - error: The underlying error that describes why this error occurred. /// /// Error Code: 5003 case dataProviderError(provider: any ImageDataProvider, error: any Error) /// No more alternative ``Source`` can be used in current loading process. It means that the /// ``KingfisherOptionsInfoItem/alternativeSources(_:)`` are set and Kingfisher tried to recovery from the original error, but still /// fails for all the given alternative sources. The associated value holds all the errors encountered during /// the load process, including the original source loading error and all the alternative sources errors. /// Code 5004. /// No more alternative `Source` can be used in the current loading process. /// /// - Parameters: /// - error : A ``PropagationError`` contains more information about the source and error. /// /// This means that the ``KingfisherOptionsInfoItem/alternativeSources(_:)`` option is set, and Kingfisher attempted to recover from the original error, /// but still failed for all the provided alternative sources. The associated value holds all the errors encountered during /// the loading process, including the original source loading error and all the alternative sources errors. /// /// Error Code: 5004 case alternativeSourcesExhausted([PropagationError]) /// The resource task is completed, but it is not the one that was expected. This typically occurs when you set /// another resource on the view without canceling the current ongoing task. The previous task will fail with the /// `.notCurrentLivePhotoSourceTask` error when a result is obtained, regardless of whether it was successful or /// not for that task. /// /// This error is the live photo version of the `.notCurrentSourceTask` error (error 5002). /// /// - Parameters: /// - result: The `RetrieveImageResult` if the source task is completed without any issues. `nil` if an error occurred. /// - error: The `Error` if there was a problem during the image setting task. `nil` if the task completed successfully. /// - source: The original source value of the task. /// /// Error Code: 5005 case notCurrentLivePhotoSourceTask( result: RetrieveLivePhotoResult?, error: (any Error)?, source: LivePhotoSource ) /// The error happens during processing the live photo. /// /// When creating the final `PHLivePhoto` object from the downloaded image files, the internal Photos framework /// method `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` /// invokes its `resultHandler`. If the `info` dictionary in `resultHandler` contains `PHLivePhotoInfoErrorKey`, /// Kingfisher raises this error reason to pass the information to outside. /// /// If the processing fails due to any error that is not a `KingfisherError` case, Kingfisher also reports it /// with this reason. /// /// - Parameters: /// - result: The `RetrieveLivePhotoResult` if the source task is completed and a result is already existing. /// - error: The `NSError` if `PHLivePhotoInfoErrorKey` is contained in the `resultHandler` info dictionary. /// - source: The original source value of the task. /// /// - Note: It is possible that both `result` and `error` are non-nil value. Check the /// ``RetrieveLivePhotoResult/info`` property for the raw values that are from the Photos framework. /// /// Error Code: 5006 case livePhotoResultError(result: RetrieveLivePhotoResult?, error: (any Error)?, source: LivePhotoSource) } // MARK: Member Cases /// Represents the error reasons that can occur during the networking request phase. case requestError(reason: RequestErrorReason) /// Represents the error reason that can occur during networking response phase. case responseError(reason: ResponseErrorReason) /// Represents the error reason that can occur during Kingfisher caching phase. case cacheError(reason: CacheErrorReason) /// Represents the error reason that can occur during image processing phase. case processorError(reason: ProcessorErrorReason) /// Represents the error reason that can occur during image setting in a view related class. case imageSettingError(reason: ImageSettingErrorReason) // MARK: Helper Properties & Methods /// A helper property to determine if this error is of type `RequestErrorReason.taskCancelled`. public var isTaskCancelled: Bool { if case .requestError(reason: .taskCancelled) = self { return true } return false } /// Helper method to check whether this error is a ``ResponseErrorReason/invalidHTTPStatusCode(response:)`` /// and the associated value is a given status code. /// /// - Parameter code: The given status code. /// - Returns: If `self` is a `ResponseErrorReason.invalidHTTPStatusCode` error /// and its status code equals to `code`, `true` is returned. Otherwise, `false`. /// /// A helper method for checking HTTP status code. /// /// Use this helper method to determine whether this error corresponds to a /// ``ResponseErrorReason/invalidHTTPStatusCode(response:)`` with a specific status code. /// /// - Parameter code: The desired HTTP status code for comparison. /// - Returns: `true` if the error is of type ``ResponseErrorReason/invalidHTTPStatusCode(response:)`` and its /// status code matches the provided `code`, otherwise `false`. public func isInvalidResponseStatusCode(_ code: Int) -> Bool { if case .responseError(reason: .invalidHTTPStatusCode(let response)) = self { return response.statusCode == code } return false } /// A helper method for checking the error is type of ``ResponseErrorReason/invalidHTTPStatusCode(response:)``. public var isInvalidResponseStatusCode: Bool { if case .responseError(reason: .invalidHTTPStatusCode) = self { return true } return false } /// A helper property that indicates whether this error is of type /// ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)`` or not. /// /// This property is used to check if a new image setting task starts while the old one is still running. /// In such a scenario, the identifier of the new task will overwrite the identifier of the old task. /// /// When the old task finishes, a ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)`` error will /// be raised to notify you that the setting process has completed with a certain result, but the image view or /// button has not been updated. /// /// - Returns: `true` if the error is of type ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)``, /// `false` otherwise. public var isNotCurrentTask: Bool { if case .imageSettingError(reason: .notCurrentSourceTask(_, _, _)) = self { return true } return false } var isLowDataModeConstrained: Bool { if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *), case .responseError(reason: .URLSessionError(let sessionError)) = self, let urlError = sessionError as? URLError, urlError.networkUnavailableReason == .constrained { return true } return false } } // MARK: - LocalizedError Conforming extension KingfisherError: LocalizedError { /// Provides a localized message describing the error that occurred. /// /// Use this property to obtain a human-readable description of the error for display to the user. public var errorDescription: String? { switch self { case .requestError(let reason): return reason.errorDescription case .responseError(let reason): return reason.errorDescription case .cacheError(let reason): return reason.errorDescription case .processorError(let reason): return reason.errorDescription case .imageSettingError(let reason): return reason.errorDescription } } } // MARK: - CustomNSError Conforming extension KingfisherError: CustomNSError { /// The error domain for ``KingfisherError``. All errors generated by Kingfisher are categorized under this domain. /// /// When handling errors from the Kingfisher library, you can use this domain to identify and distinguish them /// from other types of errors in your application. /// /// - Note: The error domain is a string identifier associated with each error. public static let domain = "com.onevcat.Kingfisher.Error" /// Represents the error code within the specified error domain. /// /// Use this property to retrieve the specific error code associated with a ``KingfisherError``. The error code /// provides additional context and information about the error, allowing you to handle and respond to different /// error scenarios. /// /// - Note: Error codes are numerical values associated with each error within a domain. Check the error code in the /// API reference of each error reason for the detail. /// /// - Returns: The error code as an integer. public var errorCode: Int { switch self { case .requestError(let reason): return reason.errorCode case .responseError(let reason): return reason.errorCode case .cacheError(let reason): return reason.errorCode case .processorError(let reason): return reason.errorCode case .imageSettingError(let reason): return reason.errorCode } } } extension KingfisherError.RequestErrorReason { var errorDescription: String? { switch self { case .emptyRequest: return "The request is empty or `nil`." case .invalidURL(let request): return "The request contains an invalid or empty URL. Request: \(request)." case .taskCancelled(let task, let token): return "The session task was cancelled. Task: \(task), cancel token: \(token)." case .livePhotoTaskCancelled(let source): return "The live photo download task was cancelled. Source: \(source)" case .asyncTaskContextCancelled: return "The async task context was cancelled. This usually happens when the task is cancelled before it starts." } } var errorCode: Int { switch self { case .emptyRequest: return 1001 case .invalidURL: return 1002 case .taskCancelled: return 1003 case .livePhotoTaskCancelled: return 1004 case .asyncTaskContextCancelled: return 1005 } } } extension KingfisherError.ResponseErrorReason { var errorDescription: String? { switch self { case .invalidURLResponse(let response): return "The URL response is invalid: \(response)" case .invalidHTTPStatusCode(let response): return "The HTTP status code in response is invalid. Code: \(response.statusCode), response: \(response)." case .URLSessionError(let error): return "A URL session error happened. The underlying error: \(error)" case .dataModifyingFailed(let task): return "The data modifying delegate returned `nil` for the downloaded data. Task: \(task)." case .noURLResponse(let task): return "No URL response received. Task: \(task)." case .cancelledByDelegate(let response): return "The downloading task is cancelled by the downloader delegate. Response: \(response)." } } var errorCode: Int { switch self { case .invalidURLResponse: return 2001 case .invalidHTTPStatusCode: return 2002 case .URLSessionError: return 2003 case .dataModifyingFailed: return 2004 case .noURLResponse: return 2005 case .cancelledByDelegate: return 2006 } } } extension KingfisherError.CacheErrorReason { var errorDescription: String? { switch self { case .fileEnumeratorCreationFailed(let url): return "Cannot create file enumerator for URL: \(url)." case .invalidFileEnumeratorContent(let url): return "Cannot get contents from the file enumerator at URL: \(url)." case .invalidURLResource(let error, let key, let url): return "Cannot get URL resource values or data for the given URL: \(url). " + "Cache key: \(key). Underlying error: \(error)" case .cannotLoadDataFromDisk(let url, let error): return "Cannot load data from disk at URL: \(url). Underlying error: \(error)" case .cannotCreateDirectory(let path, let error): return "Cannot create directory at given path: Path: \(path). Underlying error: \(error)" case .imageNotExisting(let key): return "The image is not in cache, but you requires it should only be " + "from cache by enabling the `.onlyFromCache` option. Key: \(key)." case .cannotConvertToData(let object, let error): return "Cannot convert the input object to a `Data` object when storing it to disk cache. " + "Object: \(object). Underlying error: \(error)" case .cannotSerializeImage(let image, let originalData, let serializer): return "Cannot serialize an image due to the cache serializer returning `nil`. " + "Image: \(String(describing:image)), original data: \(String(describing: originalData)), " + "serializer: \(serializer)." case .cannotCreateCacheFile(let fileURL, let key, let data, let error): return "Cannot create cache file at url: \(fileURL), key: \(key), data length: \(data.count). " + "Underlying foundation error: \(error)." case .cannotSetCacheFileAttribute(let filePath, let attributes, let error): return "Cannot set file attribute for the cache file at path: \(filePath), attributes: \(attributes)." + "Underlying foundation error: \(error)." case .diskStorageIsNotReady(let cacheURL): return "The disk storage is not ready to use yet at URL: '\(cacheURL)'. " + "This is usually caused by extremely lack of disk space. Ask users to free up some space and restart the app." case .missingLivePhotoResourceOnDisk(let resource): return "The live photo resource '\(resource)' is missing in the cache. Usually a re-download" + " can fix this issue." } } var errorCode: Int { switch self { case .fileEnumeratorCreationFailed: return 3001 case .invalidFileEnumeratorContent: return 3002 case .invalidURLResource: return 3003 case .cannotLoadDataFromDisk: return 3004 case .cannotCreateDirectory: return 3005 case .imageNotExisting: return 3006 case .cannotConvertToData: return 3007 case .cannotSerializeImage: return 3008 case .cannotCreateCacheFile: return 3009 case .cannotSetCacheFileAttribute: return 3010 case .diskStorageIsNotReady: return 3011 case .missingLivePhotoResourceOnDisk: return 3012 } } } extension KingfisherError.ProcessorErrorReason { var errorDescription: String? { switch self { case .processingFailed(let processor, let item): return "Processing image failed. Processor: \(processor). Processing item: \(item)." } } var errorCode: Int { switch self { case .processingFailed: return 4001 } } } extension KingfisherError.ImageSettingErrorReason { var errorDescription: String? { switch self { case .emptySource: return "The input resource is empty." case .notCurrentSourceTask(let result, let error, let resource): if let result = result { return "Retrieving resource succeeded, but this source is " + "not the one currently expected. Result: \(result). Resource: \(resource)." } else if let error = error { return "Retrieving resource failed, and this resource is " + "not the one currently expected. Error: \(error). Resource: \(resource)." } else { return nil } case .dataProviderError(let provider, let error): return "Image data provider fails to provide data. Provider: \(provider), error: \(error)" case .alternativeSourcesExhausted(let errors): return "Image setting from alternative sources failed: \(errors)" case .notCurrentLivePhotoSourceTask(let result, let error, let source): if let result = result { return "Retrieving live photo resource succeeded, but this source is " + "not the one currently expected. Result: \(result). Resource: \(source)." } else if let error = error { return "Retrieving live photo resource failed, and this resource is " + "not the one currently expected. Error: \(error). Resource: \(source)." } else { return nil } case .livePhotoResultError(let result, let error, let source): return "An error occurred while processing live photo. Source: \(source). " + "Result: \(String(describing: result)). Error: \(String(describing: error))" } } var errorCode: Int { switch self { case .emptySource: return 5001 case .notCurrentSourceTask: return 5002 case .dataProviderError: return 5003 case .alternativeSourcesExhausted: return 5004 case .notCurrentLivePhotoSourceTask: return 5005 case .livePhotoResultError: return 5006 } } } ================================================ FILE: Sources/General/KingfisherManager+LivePhoto.swift ================================================ // // KingfisherManager+LivePhoto.swift // Kingfisher // // Created by onevcat on 2024/10/01. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) @preconcurrency import Photos /// A structure that contains information about the result of loading a live photo. public struct LivePhotoLoadingInfoResult: Sendable { /// Retrieves the live photo disk URLs from this result. public let fileURLs: [URL] /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved. /// /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned. /// Otherwise, ``CacheType/disk`` will be returned for the live photo. ``CacheType/memory`` is not available for /// live photos since it may take too much memory. All cached live photos are loaded from disk only. public let cacheType: CacheType /// The ``LivePhotoSource`` to which this result is related. This indicates where the `livePhoto` referenced by /// `self` is located. public let source: LivePhotoSource /// The original ``LivePhotoSource`` from which the retrieval task begins. It may differ from the ``source`` property. /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process. public let originalSource: LivePhotoSource /// Retrieves the data associated with this result. /// /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache /// serializer from the loading options and returns the result. /// /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to /// use it multiple times and avoid frequent calls to this method. public let data: @Sendable () -> [Data] } extension KingfisherManager { /// Retrieves a live photo from the specified source. /// /// This method asynchronously loads a live photo from the given source, applying the specified options and /// reporting progress if a progress block is provided. /// /// - Parameters: /// - source: The ``LivePhotoSource`` from which to retrieve the live photo. /// - options: A dictionary of options to apply to the retrieval process. If `nil`, the default options will be /// used. /// - progressBlock: An optional closure to be called periodically during the download process. /// - referenceTaskIdentifierChecker: An optional closure that returns a Boolean value indicating whether the task /// should proceed. /// /// - Returns: A ``LivePhotoLoadingInfoResult`` containing information about the retrieved live photo. /// /// - Throws: An error if the retrieval process fails. /// /// - Note: This method uses `LivePhotoImageProcessor` by default. Custom processors are not supported for live photos. /// /// - Warning: Not all options are working for this method. And currently the `progressBlock` is not working. /// It will be implemented in the future. public func retrieveLivePhoto( with source: LivePhotoSource, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, referenceTaskIdentifierChecker: (() -> Bool)? = nil ) async throws -> LivePhotoLoadingInfoResult { let fullOptions = currentDefaultOptions + (options ?? .empty) var checkedOptions = KingfisherParsedOptionsInfo(fullOptions) if checkedOptions.processor == DefaultImageProcessor.default { // The default processor is a default behavior so we replace it silently. checkedOptions.processor = LivePhotoImageProcessor.default } else if checkedOptions.processor != LivePhotoImageProcessor.default { // Warn the framework user that the processor is not supported. assertionFailure("[Kingfisher] Using of custom processors during loading of live photo resource is not supported.") checkedOptions.processor = LivePhotoImageProcessor.default } if let checker = referenceTaskIdentifierChecker { checkedOptions.onDataReceived?.forEach { $0.onShouldApply = checker } } // TODO. We ignore the retry of live photo and the progress now to suppress the complexity. let missingResources = missingResources(source, options: checkedOptions) let resourcesResult = try await downloadAndCache(resources: missingResources, options: checkedOptions) let targetCache = checkedOptions.targetCache ?? cache var fileURLs = [URL]() for resource in source.resources { let url = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: checkedOptions) guard let url else { // This should not happen normally if the previous `downloadAndCache` done without issue, but in case. throw KingfisherError.cacheError(reason: .missingLivePhotoResourceOnDisk(resource)) } fileURLs.append(url) } return LivePhotoLoadingInfoResult( fileURLs: fileURLs, cacheType: missingResources.isEmpty ? .disk : .none, source: source, originalSource: source, data: { resourcesResult.map { $0.originalData } }) } // Returns the missing resources for the given source and options. If the resource is not in the cache, it will be // returned as a missing resource. func missingResources(_ source: LivePhotoSource, options: KingfisherParsedOptionsInfo) -> [LivePhotoResource] { let missingResources: [LivePhotoResource] if options.forceRefresh { missingResources = source.resources } else { let targetCache = options.targetCache ?? cache missingResources = source.resources.reduce([], { r, resource in // Check if the resource is in the cache. It includes a guess of the file extension. let cachedFileURL = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: options) if cachedFileURL == nil { return r + [resource] } else { return r } }) } return missingResources } // Download the resources and store them to the cache. // If the resource does not specify a file extension (from either the URL extension or the explicit // `referenceFileType`), we infer it from the file signature. func downloadAndCache( resources: [LivePhotoResource], options: KingfisherParsedOptionsInfo ) async throws -> [LivePhotoResourceDownloadingResult] { if resources.isEmpty { return [] } let downloader = options.downloader ?? downloader let cache = options.targetCache ?? cache // Download all resources concurrently. return try await withThrowingTaskGroup(of: LivePhotoResourceDownloadingResult.self) { group in for resource in resources { group.addTask { let downloadedResource: LivePhotoResourceDownloadingResult switch resource.dataSource { case .network(let urlResource): downloadedResource = try await downloader.downloadLivePhotoResource( with: urlResource.downloadURL, options: options ) case .provider(let provider): downloadedResource = try await LivePhotoResourceDownloadingResult( originalData: provider.data(), url: provider.contentURL ) } // We need to specify the extension so the file is saved correctly. Live photo loading requires // the file extension to be correct. Otherwise, a 3302 error will be thrown. // https://developer.apple.com/documentation/photokit/phphotoserror/code/invalidresource let fileExtension = resource.referenceFileType .determinedFileExtension(downloadedResource.originalData) try await cache.storeToDisk( downloadedResource.originalData, forKey: resource.cacheKey, processorIdentifier: options.processor.identifier, forcedExtension: fileExtension, expiration: options.diskCacheExpiration ) return downloadedResource } } var result: [LivePhotoResourceDownloadingResult] = [] for try await resource in group { result.append(resource) } return result } } } extension ImageCache { func possibleCacheFileURLIfOnDisk( resource: LivePhotoResource, options: KingfisherParsedOptionsInfo ) -> URL? { possibleCacheFileURLIfOnDisk( forKey: resource.cacheKey, processorIdentifier: options.processor.identifier, referenceFileType: resource.referenceFileType ) } // Returns the possible cache file URL for the given key and processor identifier. If the file is on disk, it will // return the URL. Otherwise, it will return `nil`. // // This method also tries to guess the file extension if it is not specified in the `referenceFileType`. // `PHLivePhoto`'s `request` method requires the file extension to be correct on the disk, and we also stored the // downloaded data with the correct extension (if it is not specified in the `referenceFileType`, we infer it from // the file signature. See `FileType.determinedFileExtension` for more). func possibleCacheFileURLIfOnDisk( forKey key: String, processorIdentifier identifier: String, referenceFileType: LivePhotoResource.FileType ) -> URL? { switch referenceFileType { case .heic, .mov: // The extension is specified and is what necessary to load a live photo, use it. return cacheFileURLIfOnDisk( forKey: key, processorIdentifier: identifier, forcedExtension: referenceFileType.fileExtension ) case .other(let ext): if ext.isEmpty { // The extension is not specified. Guess from the default set of values. let possibleFileTypes: [LivePhotoResource.FileType] = [.heic, .mov] for fileType in possibleFileTypes { let url = cacheFileURLIfOnDisk( forKey: key, processorIdentifier: identifier, forcedExtension: fileType.fileExtension ) if url != nil { // Found, early return. return url } } return nil } else { // The extension is specified but maybe not valid for live photo. Trust the user and use it to find the // file. return cacheFileURLIfOnDisk( forKey: key, processorIdentifier: identifier, forcedExtension: ext ) } } } } #endif ================================================ FILE: Sources/General/KingfisherManager.swift ================================================ // // KingfisherManager.swift // Kingfisher // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation #if os(macOS) import AppKit #else import UIKit #endif /// Represents the type for a downloading progress block. /// /// This block type is used to monitor the progress of data being downloaded. It takes two parameters: /// /// 1. `receivedSize`: The size of the data received in the current response. /// 2. `expectedSize`: The total expected data length from the response's "Content-Length" header. If the expected /// length is not available, this block will not be called. /// /// You can use this progress block to track the download progress and update user interfaces or perform additional /// actions based on the progress. /// /// - Parameters: /// - receivedSize: The size of the data received. /// - expectedSize: The expected total data length from the "Content-Length" header. public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void) /// Represents the result of a Kingfisher image retrieval task. /// /// This type encapsulates the outcome of an image retrieval operation performed by Kingfisher. /// It holds a successful result with the retrieved image. public struct RetrieveImageResult: Sendable { /// Retrieves the image object from this result. public let image: KFCrossPlatformImage /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved. /// /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned. /// Otherwise, either ``CacheType/memory`` or ``CacheType/disk`` will be returned, allowing you to determine whether /// the image was retrieved from memory or disk cache. public let cacheType: CacheType /// The ``Source`` to which this result is related. This indicates where the `image` referenced by `self` is located. public let source: Source /// The original ``Source`` from which the retrieval task begins. It may differ from the ``source`` property. /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process. public let originalSource: Source /// Retrieves the data associated with this result. /// /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache /// serializer from the loading options and returns the result. /// /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to /// use it multiple times and avoid frequent calls to this method. public let data: @Sendable () -> Data? /// The network metrics collected during the download process. /// /// This property contains network performance metrics when the image was downloaded from the network /// (`cacheType == .none`). For cached images (`cacheType == .memory` or `.disk`), this will be `nil`. public let metrics: NetworkMetrics? /// Creates a RetrieveImageResult. /// /// - Parameters: /// - image: The retrieved image. /// - cacheType: The cache source type. /// - source: The source of the image. /// - originalSource: The original source that initiated the retrieval. /// - data: A closure that provides the image data. /// - metrics: The network metrics collected during download. Defaults to nil for cached images. public init( image: KFCrossPlatformImage, cacheType: CacheType, source: Source, originalSource: Source, data: @escaping @Sendable () -> Data?, metrics: NetworkMetrics? = nil ) { self.image = image self.cacheType = cacheType self.source = source self.originalSource = originalSource self.data = data self.metrics = metrics } } /// A structure that stores related information about a ``KingfisherError``. It provides contextual information /// to facilitate the identification of the error. public struct PropagationError: Sendable { /// The ``Source`` to which current `error` is bound. public let source: Source /// The actual error happens in framework. public let error: KingfisherError } /// The block type used for handling updates during the downloading task. /// /// The `newTask` parameter represents the updated task for the image loading process. It is `nil` if the image loading /// doesn't involve a downloading process. When an image download is initiated, this value will contain the actual /// ``DownloadTask`` instance, allowing you to retain it or cancel it later if necessary. public typealias DownloadTaskUpdatedBlock = (@Sendable (_ newTask: DownloadTask?) -> Void) /// The main manager class of Kingfisher. It connects the Kingfisher downloader and cache to offer a set of convenient /// methods for working with Kingfisher tasks. /// /// You can utilize this class to retrieve an image via a specified URL from the web or cache. public class KingfisherManager: @unchecked Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.KingfisherManagerPropertyQueue") /// Represents a shared manager used across Kingfisher. /// Use this instance for getting or storing images with Kingfisher. public static let shared = KingfisherManager() // Mark: Public Properties private var _cache: ImageCache /// The ``ImageCache`` utilized by this manager, which defaults to ``ImageCache/default``. /// /// If a cache is specified in ``KingfisherManager/defaultOptions`` or ``KingfisherOptionsInfoItem/targetCache(_:)``, /// those specified values will take precedence when Kingfisher attempts to retrieve or store images in the cache. public var cache: ImageCache { get { propertyQueue.sync { _cache } } set { propertyQueue.sync { _cache = newValue } } } private var _downloader: ImageDownloader /// The ``ImageDownloader`` utilized by this manager, which defaults to ``ImageDownloader/default``. /// /// If a downloader is specified in ``KingfisherManager/defaultOptions`` or ``KingfisherOptionsInfoItem/downloader(_:)``, /// those specified values will take precedence when Kingfisher attempts to download the image data from a remote /// server. public var downloader: ImageDownloader { get { propertyQueue.sync { _downloader } } set { propertyQueue.sync { _downloader = newValue } } } /// The default options used by the ``KingfisherManager`` instance. /// /// These options are utilized in Kingfisher manager-related methods, as well as all view extension methods. /// You can also pass additional options for each image task by providing an `options` parameter to Kingfisher's APIs. /// /// Per-image options will override the default ones if there is a conflict. public var defaultOptions = KingfisherOptionsInfo.empty // Use `defaultOptions` to overwrite the `downloader` and `cache`. var currentDefaultOptions: KingfisherOptionsInfo { return [.downloader(downloader), .targetCache(cache)] + defaultOptions } private let processingQueue: CallbackQueue private convenience init() { self.init(downloader: .default, cache: .default) } /// Creates an image setting manager with the specified downloader and cache. /// /// - Parameters: /// - downloader: The image downloader used for image downloads. /// - cache: The image cache that stores images in memory and on disk. /// public init(downloader: ImageDownloader, cache: ImageCache) { _downloader = downloader _cache = cache let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)" processingQueue = .dispatch(DispatchQueue(label: processQueueName)) } // MARK: - Getting Images /// Retrieves an image from a specified resource. /// /// - Parameters: /// - resource: The ``Resource`` object defining data information, such as a key or URL. /// - options: Options to use when creating the image. /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response /// contains an `expectedContentLength` and always runs on the main queue. /// - downloadTaskUpdated: Called when a new image download task is created for the current image retrieval. This /// typically occurs when an alternative source is used to replace the original (failed) task. You can update your /// reference to the ``DownloadTask`` if you want to manually invoke ``DownloadTask/cancel()`` on the new task. /// - completionHandler: Called when the image retrieval and setting are completed. This completion handler is /// invoked from the `options.callbackQueue`. If not specified, the main queue is used. /// /// - Returns: A task representing the image download. If a download task is initiated for a ``Source/network(_:)`` resource, /// the started ``DownloadTask`` is returned; otherwise, `nil` is returned. /// /// - Note: This method first checks whether the requested `resource` is already in the cache. If it is cached, /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads /// the `resource`, stores it in the cache, and then calls the `completionHandler`. /// @discardableResult public func retrieveImage( with resource: any Resource, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? { return retrieveImage( with: resource.convertToSource(), options: options, progressBlock: progressBlock, downloadTaskUpdated: downloadTaskUpdated, completionHandler: completionHandler ) } /// Retrieves an image from a specified source. /// /// - Parameters: /// - source: The ``Source`` object defining data information, such as a key or URL. /// - options: Options to use when creating the image. /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response /// contains an `expectedContentLength` and always runs on the main queue. /// - downloadTaskUpdated: Called when a new image download task is created for the current image retrieval. This /// typically occurs when an alternative source is used to replace the original (failed) task. You can update your /// reference to the ``DownloadTask`` if you want to manually invoke ``DownloadTask/cancel()`` on the new task. /// - completionHandler: Called when the image retrieval and setting are completed. This completion handler is /// invoked from the `options.callbackQueue`. If not specified, the main queue is used. /// /// - Returns: A task representing the image download. If a download task is initiated for a ``Source/network(_:)`` resource, /// the started ``DownloadTask`` is returned; otherwise, `nil` is returned. /// /// - Note: This method first checks whether the requested `source` is already in the cache. If it is cached, /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads /// the `source`, stores it in the cache, and then calls the `completionHandler`. /// @discardableResult public func retrieveImage( with source: Source, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? { let options = currentDefaultOptions + (options ?? .empty) let info = KingfisherParsedOptionsInfo(options) return retrieveImage( with: source, options: info, progressBlock: progressBlock, downloadTaskUpdated: downloadTaskUpdated, completionHandler: completionHandler) } func retrieveImage( with source: Source, options: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock?, downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil, completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? { var info = options if let block = progressBlock { info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } return retrieveImage( with: source, options: info, downloadTaskUpdated: downloadTaskUpdated, progressiveImageSetter: progressiveImageSetter, completionHandler: completionHandler) } func retrieveImage( with source: Source, options: KingfisherParsedOptionsInfo, downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil, referenceTaskIdentifierChecker: (() -> Bool)? = nil, completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? { var options = options let retryStrategy = options.retryStrategy let progressiveJPEG = options.progressiveJPEG if let provider = ImageProgressiveProvider(options: options, refresh: { image in guard let setter = progressiveImageSetter else { return } guard let strategy = progressiveJPEG?.onImageUpdated(image) else { setter(image) return } switch strategy { case .default: setter(image) case .keepCurrent: break case .replace(let newImage): setter(newImage) } }) { options.onDataReceived = (options.onDataReceived ?? []) + [provider] } if let checker = referenceTaskIdentifierChecker { options.onDataReceived?.forEach { $0.onShouldApply = checker } } let retrievingContext = RetrievingContext(options: options, originalSource: source) @Sendable func startNewRetrieveTask( with source: Source, retryContext: RetryContext?, downloadTaskUpdated: DownloadTaskUpdatedBlock? ) { let newTask = self.retrieveImage( with: source, context: retrievingContext, downloadTaskUpdated: downloadTaskUpdated ) { result in handler(currentSource: source, retryContext: retryContext, result: result) } downloadTaskUpdated?(newTask) } @Sendable func failCurrentSource(_ source: Source, retryContext: RetryContext?, with error: KingfisherError) { // Skip alternative sources if the user cancelled it. guard !error.isTaskCancelled else { completionHandler?(.failure(error)) return } // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly. guard !error.isLowDataModeConstrained else { if let source = retrievingContext.options.lowDataModeSource { retrievingContext.options.lowDataModeSource = nil startNewRetrieveTask(with: source, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated) } else { // This should not happen. completionHandler?(.failure(error)) } return } if let nextSource = retrievingContext.popAlternativeSource() { retrievingContext.appendError(error, to: source) startNewRetrieveTask(with: nextSource, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated) } else { // No other alternative source. Finish with error. if retrievingContext.propagationErrors.isEmpty { completionHandler?(.failure(error)) } else { retrievingContext.appendError(error, to: source) let finalError = KingfisherError.imageSettingError( reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors) ) completionHandler?(.failure(finalError)) } } } @Sendable func handler( currentSource: Source, retryContext: RetryContext?, result: (Result) ) -> Void { switch result { case .success: completionHandler?(result) case .failure(let error): if let retryStrategy = retryStrategy { let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error) retryStrategy.retry(context: context) { decision in switch decision { case .retry(let userInfo): context.userInfo = userInfo startNewRetrieveTask(with: source, retryContext: context, downloadTaskUpdated: downloadTaskUpdated) case .stop: failCurrentSource(currentSource, retryContext: context, with: error) } } } else { failCurrentSource(currentSource, retryContext: retryContext, with: error) } } } return retrieveImage( with: source, context: retrievingContext, downloadTaskUpdated: downloadTaskUpdated) { result in handler(currentSource: source, retryContext: nil, result: result) } } private func retrieveImage( with source: Source, context: RetrievingContext, downloadTaskUpdated: DownloadTaskUpdatedBlock?, completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? { let options = context.options if options.forceRefresh { return loadAndCacheImage( source: source, context: context, completionHandler: completionHandler)?.value } else { let loadedFromCache = retrieveImageFromCache( source: source, context: context, downloadTaskUpdated: downloadTaskUpdated, completionHandler: completionHandler) if loadedFromCache { return nil } if options.onlyFromCache { let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey)) completionHandler?(.failure(error)) return nil } return loadAndCacheImage( source: source, context: context, completionHandler: completionHandler)?.value } } func provideImage( provider: any ImageDataProvider, options: KingfisherParsedOptionsInfo, completionHandler: (@Sendable (Result) -> Void)?) { guard let completionHandler = completionHandler else { return } provider.data { result in switch result { case .success(let data): (options.processingQueue ?? self.processingQueue).execute { let processor = options.processor let processingItem = ImageProcessItem.data(data) guard let image = processor.process(item: processingItem, options: options) else { options.callbackQueue.execute { let error = KingfisherError.processorError( reason: .processingFailed(processor: processor, item: processingItem)) completionHandler(.failure(error)) } return } options.callbackQueue.execute { let result = ImageLoadingResult(image: image, url: nil, originalData: data) completionHandler(.success(result)) } } case .failure(let error): options.callbackQueue.execute { let error = KingfisherError.imageSettingError( reason: .dataProviderError(provider: provider, error: error)) completionHandler(.failure(error)) } } } } private func cacheImage( source: Source, options: KingfisherParsedOptionsInfo, context: RetrievingContext, result: Result, completionHandler: (@Sendable (Result) -> Void)? ) { switch result { case .success(let value): let needToCacheOriginalImage = options.cacheOriginalImage && options.processor != DefaultImageProcessor.default let coordinator = CacheCallbackCoordinator( shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage) let result = RetrieveImageResult( image: options.imageModifier?.modify(value.image) ?? value.image, cacheType: .none, source: source, originalSource: context.originalSource, data: { value.originalData }, metrics: value.metrics ) // Add image to cache. let targetCache = options.targetCache ?? self.cache targetCache.store( value.image, original: value.originalData, forKey: source.cacheKey, options: options, toDisk: !options.cacheMemoryOnly) { _ in coordinator.apply(.cachingImage) { completionHandler?(.success(result)) } } // Add original image to cache if necessary. if needToCacheOriginalImage { let originalCache = options.originalCache ?? targetCache originalCache.storeToDisk( value.originalData, forKey: source.cacheKey, processorIdentifier: DefaultImageProcessor.default.identifier, expiration: options.diskCacheExpiration) { _ in coordinator.apply(.cachingOriginalImage) { completionHandler?(.success(result)) } } } coordinator.apply(.cacheInitiated) { completionHandler?(.success(result)) } case .failure(let error): completionHandler?(.failure(error)) } } @discardableResult func loadAndCacheImage( source: Source, context: RetrievingContext, completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask.WrappedTask? { let options = context.options @Sendable func _cacheImage(_ result: Result) { cacheImage( source: source, options: options, context: context, result: result, completionHandler: completionHandler ) } switch source { case .network(let resource): let downloader = options.downloader ?? self.downloader let task = downloader.downloadImage( with: resource.downloadURL, options: options, completionHandler: _cacheImage ) // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler. // Let's fallback to a traditional style before it can be fixed in Swift. // // https://github.com/onevcat/Kingfisher/issues/1436 // // return task.map(DownloadTask.WrappedTask.download) if task.isInitialized { return .download(task) } else { return nil } case .provider(let provider): provideImage(provider: provider, options: options, completionHandler: _cacheImage) return .dataProviding } } /// Retrieves an image from either memory or disk cache. /// /// - Parameters: /// - source: The target source from which to retrieve the image. /// - key: The key to use for caching the image. /// - url: The image request URL. This is not used when retrieving an image from the cache; it is solely used for /// compatibility with ``RetrieveImageResult`` callbacks. /// - options: Options on how to retrieve the image from the image cache. /// - completionHandler: Called when the image retrieval is complete, either with a successful /// ``RetrieveImageResult`` or an error. /// /// - Returns: `true` if the requested image or the original image before processing exists in the cache. Otherwise, this method returns `false`. /// /// - Note: Image retrieval can occur in either the memory cache or the disk cache. The /// ``KingfisherOptionsInfoItem/processor(_:)`` option in `options` is considered when searching the cache. If no /// processed image is found, Kingfisher attempts to determine whether an original version of the image exists. If /// an original exists, Kingfisher retrieves it from the cache and processes it. Subsequently, the processed image /// is stored back in the cache for future use. /// func retrieveImageFromCache( source: Source, context: RetrievingContext, downloadTaskUpdated: DownloadTaskUpdatedBlock?, completionHandler: (@Sendable (Result) -> Void)?) -> Bool { let options = context.options // 1. Check whether the image was already in target cache. If so, just get it. let targetCache = options.targetCache ?? cache let key = source.cacheKey let targetImageCached = targetCache.imageCachedType( forKey: key, processorIdentifier: options.processor.identifier) let validCache = targetImageCached.cached && (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory) if validCache { targetCache.retrieveImage(forKey: key, options: options) { result in guard let completionHandler = completionHandler else { return } // TODO: Optimize it when we can use async across all the project. @Sendable func checkResultImageAndCallback(_ inputImage: KFCrossPlatformImage) { var image = inputImage if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, options.imageCreatingOptions != image.kf.imageCreatingOptions, let data = image.kf.animatedImageData { // Recreate animated image representation when loaded in different options. // https://github.com/onevcat/Kingfisher/issues/1923 image = options.processor.process(item: .data(data), options: options) ?? .init() } if let modifier = options.imageModifier { image = modifier.modify(image) } let value = result.map { RetrieveImageResult( image: image, cacheType: $0.cacheType, source: source, originalSource: context.originalSource, data: { [image] in options.cacheSerializer.data(with: image, original: nil) } ) } completionHandler(value) } result.match { cacheResult in options.callbackQueue.execute { guard let image = cacheResult.image else { completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))) return } if options.cacheSerializer.originalDataUsed { let processor = options.processor (options.processingQueue ?? self.processingQueue).execute { let item = ImageProcessItem.image(image) guard let processedImage = processor.process(item: item, options: options) else { let error = KingfisherError.processorError( reason: .processingFailed(processor: processor, item: item)) options.callbackQueue.execute { completionHandler(.failure(error)) } return } options.callbackQueue.execute { checkResultImageAndCallback(processedImage) } } } else { checkResultImageAndCallback(image) } } } onFailure: { error in options.callbackQueue.execute { completionHandler(.failure(error)) } } } return true } // 2. Check whether the original image exists. If so, get it, process it, save to storage and return. let originalCache = options.originalCache ?? targetCache // No need to store the same file in the same cache again. if originalCache === targetCache && options.processor == DefaultImageProcessor.default { return false } // Check whether the unprocessed image existing or not. let originalImageCacheType = originalCache.imageCachedType( forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier) let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh let canUseOriginalImageCache = (canAcceptDiskCache && originalImageCacheType.cached) || (!canAcceptDiskCache && originalImageCacheType == .memory) if canUseOriginalImageCache { // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove // any processor from options first. var optionsWithoutProcessor = options optionsWithoutProcessor.processor = DefaultImageProcessor.default originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in result.match( onSuccess: { cacheResult in guard let image = cacheResult.image else { // The original cache type check is not a strong guarantee. When it happens, treat it as a cache miss. // In this case, fall back to download or provider loading. if options.onlyFromCache { let error = KingfisherError.cacheError(reason: .imageNotExisting(key: key)) options.callbackQueue.execute { completionHandler?(.failure(error)) } } else { let task = self.loadAndCacheImage( source: source, context: context, completionHandler: completionHandler ) downloadTaskUpdated?(task?.value) } return } let processor = options.processor (options.processingQueue ?? self.processingQueue).execute { let item = ImageProcessItem.image(image) guard let processedImage = processor.process(item: item, options: options) else { let error = KingfisherError.processorError( reason: .processingFailed(processor: processor, item: item)) options.callbackQueue.execute { completionHandler?(.failure(error)) } return } var cacheOptions = options cacheOptions.callbackQueue = .untouch let coordinator = CacheCallbackCoordinator( shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false) let image = options.imageModifier?.modify(processedImage) ?? processedImage let result = RetrieveImageResult( image: image, cacheType: .none, source: source, originalSource: context.originalSource, data: { options.cacheSerializer.data(with: processedImage, original: nil) } ) targetCache.store( processedImage, forKey: key, options: cacheOptions, toDisk: !options.cacheMemoryOnly) { _ in coordinator.apply(.cachingImage) { options.callbackQueue.execute { completionHandler?(.success(result)) } } } coordinator.apply(.cacheInitiated) { options.callbackQueue.execute { completionHandler?(.success(result)) } } } }, onFailure: { error in // This should not happen actually, since we already confirmed `originalImageCached` is `true`. // Just in case... if let completionHandler = completionHandler { options.callbackQueue.execute { completionHandler(.failure(error)) } } } ) } return true } return false } } // Concurrency extension KingfisherManager { /// Retrieves an image from a specified resource. /// /// - Parameters: /// - resource: The ``Resource`` object defining data information, such as a key or URL. /// - options: Options to use when creating the image. /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response /// contains an `expectedContentLength` and always runs on the main queue. /// /// - Returns: The ``RetrieveImageResult`` containing the retrieved image object and cache type. /// - Throws: A ``KingfisherError`` if any issue occurred during the image retrieving progress. /// /// - Note: This method first checks whether the requested `resource` is already in the cache. If it is cached, /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads /// the `resource`, stores it in the cache, and then calls the `completionHandler`. /// public func retrieveImage( with resource: any Resource, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil ) async throws -> RetrieveImageResult { try await retrieveImage( with: resource.convertToSource(), options: options, progressBlock: progressBlock ) } /// Retrieves an image from a specified source. /// /// - Parameters: /// - source: The ``Source`` object defining data information, such as a key or URL. /// - options: Options to use when creating the image. /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response /// contains an `expectedContentLength` and always runs on the main queue. /// /// - Returns: The ``RetrieveImageResult`` containing the retrieved image object and cache type. /// - Throws: A ``KingfisherError`` if any issue occurred during the image retrieving progress. /// /// - Note: This method first checks whether the requested `source` is already in the cache. If it is cached, /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads /// the `source`, stores it in the cache, and then calls the `completionHandler`. /// public func retrieveImage( with source: Source, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil ) async throws -> RetrieveImageResult { let options = currentDefaultOptions + (options ?? .empty) let info = KingfisherParsedOptionsInfo(options) return try await retrieveImage( with: source, options: info, progressBlock: progressBlock ) } func retrieveImage( with source: Source, options: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil ) async throws -> RetrieveImageResult { var info = options if let block = progressBlock { info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } return try await retrieveImage( with: source, options: info, progressiveImageSetter: nil ) } func retrieveImage( with source: Source, options: KingfisherParsedOptionsInfo, progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil, referenceTaskIdentifierChecker: (() -> Bool)? = nil ) async throws -> RetrieveImageResult { // Early cancellation check if Task.isCancelled { throw CancellationError() } let task = CancellationDownloadTask() return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in // Use an actor to ensure continuation is only resumed once in a Swift 6 compatible way actor ContinuationState { var isResumed = false func tryResume() -> Bool { if !isResumed { isResumed = true return true } return false } } let state = ContinuationState() @Sendable func safeResume(with result: Result) { Task { if await state.tryResume() { continuation.resume(with: result) } } } let downloadTask = retrieveImage( with: source, options: options, downloadTaskUpdated: { newTask in Task { await task.setTask(newTask) } }, progressiveImageSetter: progressiveImageSetter, referenceTaskIdentifierChecker: referenceTaskIdentifierChecker, completionHandler: { result in safeResume(with: result) } ) // Check for cancellation that may have occurred during setup if Task.isCancelled { downloadTask?.cancel() let error: KingfisherError if let sessionTask = downloadTask?.sessionTask, let cancelToken = downloadTask?.cancelToken { error = .requestError(reason: .taskCancelled(task: sessionTask, token: cancelToken)) } else { error = .requestError(reason: .asyncTaskContextCancelled) } safeResume(with: .failure(error)) } else { Task { await task.setTask(downloadTask) } } } } onCancel: { Task { await task.task?.cancel() } } } } class RetrievingContext: @unchecked Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.RetrievingContextPropertyQueue") private var _options: KingfisherParsedOptionsInfo var options: KingfisherParsedOptionsInfo { get { propertyQueue.sync { _options } } set { propertyQueue.sync { _options = newValue } } } let originalSource: SourceType var propagationErrors: [PropagationError] = [] init(options: KingfisherParsedOptionsInfo, originalSource: SourceType) { self.originalSource = originalSource _options = options } func popAlternativeSource() -> Source? { var localOptions = options guard var alternativeSources = localOptions.alternativeSources, !alternativeSources.isEmpty else { return nil } let nextSource = alternativeSources.removeFirst() localOptions.alternativeSources = alternativeSources options = localOptions return nextSource } @discardableResult func appendError(_ error: KingfisherError, to source: Source) -> [PropagationError] { let item = PropagationError(source: source, error: error) propagationErrors.append(item) return propagationErrors } } class CacheCallbackCoordinator: @unchecked Sendable { enum State { case idle case imageCached case originalImageCached case done } enum Action { case cacheInitiated case cachingImage case cachingOriginalImage } private let shouldWaitForCache: Bool private let shouldCacheOriginal: Bool private let stateQueue: DispatchQueue private var threadSafeState: State = .idle private(set) var state: State { set { stateQueue.sync { threadSafeState = newValue } } get { stateQueue.sync { threadSafeState } } } init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) { self.shouldWaitForCache = shouldWaitForCache self.shouldCacheOriginal = shouldCacheOriginal let stateQueueName = "com.onevcat.Kingfisher.CacheCallbackCoordinator.stateQueue.\(UUID().uuidString)" self.stateQueue = DispatchQueue(label: stateQueueName) } func apply(_ action: Action, trigger: () -> Void) { switch (state, action) { case (.done, _): break // From .idle case (.idle, .cacheInitiated): if !shouldWaitForCache { state = .done trigger() } case (.idle, .cachingImage): if shouldCacheOriginal { state = .imageCached } else { state = .done trigger() } case (.idle, .cachingOriginalImage): state = .originalImageCached // From .imageCached case (.imageCached, .cachingOriginalImage): state = .done trigger() // From .originalImageCached case (.originalImageCached, .cachingImage): state = .done trigger() default: assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)") } } } ================================================ FILE: Sources/General/KingfisherOptionsInfo.swift ================================================ // // KingfisherOptionsInfo.swift // Kingfisher // // Created by Wei Wang on 15/4/23. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(macOS) import AppKit #else import UIKit #endif /// `KingfisherOptionsInfo` is a typealias for `[KingfisherOptionsInfoItem]`. /// You can utilize the enum of option items with values to control certain behaviors of Kingfisher. public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem] extension Array where Element == KingfisherOptionsInfoItem { static let empty: KingfisherOptionsInfo = [] } /// Represents the available option items that can be used in ``KingfisherOptionsInfo``. public enum KingfisherOptionsInfoItem: Sendable { /// Kingfisher will utilize the associated ``ImageCache`` object when performing related operations, such as /// attempting to retrieve cached images and storing downloaded images in it. case targetCache(ImageCache) /// The ``ImageCache`` used for storing and retrieving original images. /// /// If ``originalCache(_:)`` is specified in the options, it will be given preference for storing and retrieving /// original images. If there is no ``originalCache(_:)`` option, ``targetCache(_:)`` will be used to /// store original images as well. /// /// When using ``KingfisherManager`` to download and store an image, if ``cacheOriginalImage`` is applied in the /// options, the original image will be stored in the associated ``ImageCache`` of this option. /// /// Simultaneously, if a requested final image (with a processor applied) cannot be found in the ``targetCache(_:)``, /// Kingfisher will attempt to search for the original image to see if it already exists. If found, it will be /// utilized and processed with the given processor. This optimization prevents downloading the same image multiple /// times. case originalCache(ImageCache) /// Kingfisher will utilize the associated ``ImageDownloader`` object to download the requested images. case downloader(ImageDownloader) /// This enum defines the transition effect to be applied when setting an image to an image view. /// /// Kingfisher uses the ``ImageTransition`` specified by this enum to animate the image in if it's downloaded from /// the web. /// /// By default, the transition does not occur when the image is retrieved from either memory or disk cache. To /// force the transition even when the image is retrieved from the cache, also set /// ``KingfisherOptionsInfoItem/forceTransition``. /// /// - Important: This option is designed for UIKit/AppKit transitions. For SwiftUI applications, use the /// ``KFImageProtocol/loadTransition(_:animation:)`` method instead, which provides native SwiftUI transition support. case transition(ImageTransition) /// The associated `Float` value to be set as the priority of the image download task. /// /// This value should fall within the range of 0.0 to 1.0. If this option is not set, the default value /// (`URLSessionTask.defaultPriority`) will be used. case downloadPriority(Float) /// When set, Kingfisher will disregard the cache and attempt to initiate a download task for the image source. case forceRefresh /// Sets whether Kingfisher should try to load from memory cache first, and then perform a refresh from network. /// /// When set, Kingfisher will attempt to retrieve the image from memory cache first. If the image is not found in /// the memory cache, it will skip the disk cache and download the image again from the network. This is useful /// when you want to display a changeable image with the same URL within the same app session, while avoiding /// multiple downloads. case fromMemoryCacheOrRefresh /// When set, applying a transition to set the image in an image view will occur even when the image is retrieved /// from the cache. Refer to the ``transition(_:)`` option for more details. case forceTransition /// When set, Kingfisher will cache the value only in memory and not on disk. case cacheMemoryOnly /// When set, Kingfisher will wait for the caching operation to be completed before invoking the completion block. case waitForCache /// When set, Kingfisher will attempt to retrieve the image solely from the cache and not from the network. /// /// If the image is not found in the cache, the image retrieval will fail with a /// ``KingfisherError/CacheErrorReason/imageNotExisting(key:)`` error. case onlyFromCache /// Decode the image on a background thread before usage. /// /// This process involves decoding the downloaded image data and performing off-screen rendering to extract pixel /// information in the background. While this can accelerate display performance, it may require additional time /// to prepare the image for use. case backgroundDecode /// The associated value will be used as the target queue of dispatch callbacks when retrieving images from /// cache. If not set, Kingfisher will use `.mainCurrentOrAsync` for callbacks. /// /// - Note: This option does not affect the callbacks for UI related extension methods. You will always get the /// callbacks called from main queue. /// The associated value will serve as the target queue for dispatch callbacks when retrieving images from the cache. /// /// If not set, Kingfisher will use ``CallbackQueue/mainCurrentOrAsync`` for callbacks. /// /// - Note: This option does not impact the callbacks for UI-related extension methods. Those callbacks will always /// occur on the main queue. case callbackQueue(CallbackQueue) /// The associated value will be used as the scale factor when converting retrieved image data to an image object. /// /// Specify the image scale rather than your screen scale. You should set the correct scale when dealing with 2x or /// 3x retina images. Otherwise, Kingfisher will convert the data to an image object with a scale of 1.0. case scaleFactor(CGFloat) /// Determines whether all the animated image data should be preloaded. /// /// The default value is `false`, which means only the following frames will be loaded on demand. If set to `true`, /// all the animated image data will be loaded and decoded into memory. /// /// This option is primarily used for internal backward compatibility. It should not be set directly. Instead, you /// should choose the appropriate image view class to control the GIF data loading. Kingfisher offers two classes /// for displaying GIF images: ``AnimatedImageView``, which does not preload all data, consumes less memory, but uses /// more CPU during display; and a regular image view (`UIImageView` or `NSImageView`), which loads all data at /// once, consumes more memory, but decodes image frames only once. case preloadAllAnimationData /// The contained ``ImageDownloadRequestModifier`` will be used to alter the request before it is sent. /// /// This is the final opportunity to modify the image download request. You can customize the request for various /// purposes, such as adding an authentication token to the header, performing basic HTTP authentication, or URL /// mapping. /// /// By default, the original request is sent without any modifications. case requestModifier(any AsyncImageDownloadRequestModifier) /// The contained ``ImageDownloadRedirectHandler`` will be used to alter the request during redirection. /// /// This provides an opportunity to customize the image download request during redirection. You can modify the /// request for various purposes, such as adding an authentication token to the header, performing basic HTTP /// authentication, or URL mapping. /// /// By default, the original redirection request is sent without any modifications. case redirectHandler(any ImageDownloadRedirectHandler) /// The processor used in the image retrieval task. /// /// After downloading is complete, a processor will convert the downloaded data into an image and/or apply various /// filters or transformations to it. /// /// If a cache is linked to the downloader (which occurs when you use ``KingfisherManager`` or any of the view /// extension methods), the converted image will also be stored in the cache. If not set, the /// ``DefaultImageProcessor/default`` will be used. case processor(any ImageProcessor) /// Offers a ``CacheSerializer`` to convert data into an image object for retrieval from disk cache, or vice versa /// for storage in the disk cache. /// /// If not set, the ``DefaultCacheSerializer/default`` will be used. case cacheSerializer(any CacheSerializer) /// An ``ImageModifier`` for making adjustments to an image right before it is used. /// /// If the image was directly fetched from the downloader, the modifier will be applied immediately after the /// ``ImageProcessor``. If the image is retrieved from a cache, the modifier will be applied after the /// ``CacheSerializer``. /// /// Use the ``ImageModifier`` when you need to set properties that do not persist when caching the image with a /// specific image type. Examples include setting the `renderingMode` or `alignmentInsets` of a `UIImage`. case imageModifier(any ImageModifier) /// Keep the existing image of image view while setting another image to it. /// By setting this option, the placeholder image parameter of image view extension method /// will be ignored and the current image will be kept while loading or downloading the new image. case keepCurrentImageWhileLoading /// When set, Kingfisher will load only the first frame from an animated image file as a single image. /// /// Loading animated images can consume a significant amount of memory. This option is useful when you want to /// display a static preview of the first frame from an animated image. It will be ignored if the target image is /// not animated image data. case onlyLoadFirstFrame /// When set and an non-default ``ImageProcessor`` is used, Kingfisher will attempt to cache both the final result /// and the original image. /// /// Kingfisher will have the opportunity to use the original image when another processor is applied to the same /// resource, instead of downloading it anew. You can use ``KingfisherOptionsInfoItem/originalCache(_:)`` to /// specify a cache for the original images if necessary. /// /// The original image will only be cached to disk storage. case cacheOriginalImage /// When set and an image retrieval error occurs, Kingfisher will replace the requested image with the provided /// image (or an empty image). /// /// This is useful when you prefer not to display a placeholder during loading but want to use a default image when /// requests fail. case onFailureImage(KFCrossPlatformImage?) /// When set and used in methods of ``ImagePrefetcher``, the prefetching operation will aggressively load the images /// into memory storage. /// /// By default, this option is not included in the options. This means that if the requested image is already in /// the disk cache, Kingfisher will not attempt to load it into memory. case alsoPrefetchToMemory /// When set, disk storage loading will occur in the same calling queue. /// /// By default, disk storage file loading operates on its own queue with asynchronous dispatch behavior. While this /// provides improved non-blocking disk loading performance, it can lead to flickering when you reload an image from /// disk if the image view already has an image set. /// /// Setting this option will eliminate that flickering by keeping all loading in the same queue (typically the UI /// queue if you are using Kingfisher's extension methods to set an image). However, this comes with a tradeoff in /// loading performance. case loadDiskFileSynchronously /// Options for controlling the data writing process to disk storage. /// /// When set, these options will be passed to the store operation for new files. case diskStoreWriteOptions(Data.WritingOptions) /// When set, use the associated ``StorageExpiration`` value for the memory cache to determine the expiration date. /// /// By default, the underlying ``MemoryStorage/Backend`` uses the expiration defined in its ``MemoryStorage/Config`` /// for all items. If this option is set, the ``MemoryStorage/Backend`` will utilize the associated value to /// override the configuration setting for this caching item. case memoryCacheExpiration(StorageExpiration) /// When set, use the associated ``ExpirationExtending`` value for the memory cache to determine the extending policy /// when setting the next expiration date. /// /// The item's expiration date will be extended after access to keep the "most recently accessed" items alive for a /// longer duration in the cache. /// /// By default, the underlying ``MemoryStorage/Backend`` uses the initial cache expiration as the extending value, /// which is ``ExpirationExtending/cacheTime``. /// /// - Note: To disable expiration extending entirely, use ``ExpirationExtending/none``. case memoryCacheAccessExtendingExpiration(ExpirationExtending) /// When set, use the associated ``StorageExpiration`` value for the disk cache to determine the expiration date. /// /// By default, the underlying ``DiskStorage/Backend`` uses the expiration defined in its ``DiskStorage/Config`` /// for all items. If this option is set, the ``DiskStorage/Backend`` will utilize the associated value to override /// the configuration setting for this caching item. case diskCacheExpiration(StorageExpiration) /// When set, use the associated ``ExpirationExtending`` value for the disk cache to determine the extending policy /// when setting the next expiration date. /// /// The item's expiration date will be extended after access to keep the "most recently accessed" items alive for a /// longer duration in the cache. /// /// By default, the underlying ``DiskStorage/Backend`` uses the initial cache expiration as the extending value, /// which is ``ExpirationExtending/cacheTime``. /// /// - Note: To disable expiration extending entirely, use ``ExpirationExtending/none``. case diskCacheAccessExtendingExpiration(ExpirationExtending) /// Determines the queue on which image processing should occur. /// /// By default, Kingfisher uses an internal pre-defined serial queue to process images. Use this option to modify /// this behavior. /// /// For instance, you can specify ``CallbackQueue/mainCurrentOrAsync`` to process the image on the main queue, /// preventing potential flickering (but with the risk of blocking the UI, especially if the processor is /// time-consuming). /// /// If you need more control over scheduling (such as limiting concurrency, changing priority, or using a LIFO /// strategy), you can provide an operation queue by using ``CallbackQueue/operationQueue(_:)``. /// /// ```swift /// let queue = OperationQueue() /// // Configure `queue` as needed. /// options = [.processingQueue(.operationQueue(queue))] /// ``` /// /// - Note: The execution order depends on the provided queue. case processingQueue(CallbackQueue) /// Enables progressive image loading. /// /// Kingfisher will use the associated ``ImageProgressive`` value to process progressive JPEG data and display /// it progressively, if the image supports it. case progressiveJPEG(ImageProgressive) /// Sets a set of alternative sources when the original input `Source` fails to load. /// /// The `Source`s in the associated /// array will be used to start a new image loading task if the previous task fails due to an error. The image /// source loading process will stop as soon as a source is loaded successfully. If all `[Source]`s are used but /// the loading is still failing, an `imageSettingError` with `alternativeSourcesExhausted` as its reason will be /// thrown out. /// /// This option is useful if you want to implement a fallback solution for setting image. /// /// User cancellation will not trigger the alternative source loading. /// /// Specifies a set of alternative sources to use when the original input ``Source`` fails to load. /// /// The ``Source``s in the associated array will be used to start a new image loading task if the previous task /// fails due to an error. The image source loading process will halt as soon as a source is loaded successfully. /// If all ``Source``s are used, but loading still fails, a /// ``KingfisherError/ImageSettingErrorReason/alternativeSourcesExhausted(_:)``will be used as the error in the /// result. /// /// This option is useful for implementing a fallback solution for image setting. /// /// - Note: User cancellation will not trigger the loading of alternative sources. case alternativeSources([Source]) /// Provides a retry strategy to use when something goes wrong during the image retrieval process from /// ``KingfisherManager``. /// /// You can define a strategy by creating a type that conforms to the ``RetryStrategy`` protocol. When Kingfisher /// encounters a loading failure, it follows the defined retry strategy and retries until a ``RetryDecision/stop`` /// is received. /// /// - Note: All extension methods of Kingfisher (the `kf` extensions on `UIImageView` or `UIButton`, for example) /// retrieve images through ``KingfisherManager``, so the retry strategy also applies when using them. However, /// this option does not apply when passed to an ``ImageDownloader`` or an ``ImageCache`` directly. case retryStrategy(any RetryStrategy) /// Specifies the `Source` to load when the user enables Low Data Mode and the original source fails due to the data /// constraint. /// /// When the user enables Low Data Mode in the system settings, and the original source fails with an /// `NSURLErrorNetworkUnavailableReason.constrained` error, Kingfisher uses this source instead to load an image /// for Low Data Mode. /// /// When this option is set, the `allowsConstrainedNetworkAccess` property of the request for the original source /// will be set to `false`, and the ``Source`` in the associated value will be used to retrieve the image for Low /// Data Mode. Typically, you can provide a low-resolution version of your image or a local image provider to /// display a placeholder to save data usage. /// /// If not set or if the associated optional ``Source`` value is `nil`, the device's Low Data Mode will be ignored, /// and the original source will be loaded following the system default behavior. case lowDataMode(Source?) case forcedCacheFileExtension(String?) } // MARK: - KingfisherParsedOptionsInfo // Improve performance by parsing the input `KingfisherOptionsInfo` (self) first. // So we can prevent the iterating over the options array again and again. /// Represents the parsed options info used throughout Kingfisher methods. /// /// Each property in this type corresponds to a case member in ``KingfisherOptionsInfoItem``. When a /// ``KingfisherOptionsInfo`` is sent to Kingfisher-related methods, it will be parsed and converted to a /// ``KingfisherParsedOptionsInfo`` first before passing through the internal methods. public struct KingfisherParsedOptionsInfo: Sendable { public var targetCache: ImageCache? = nil public var originalCache: ImageCache? = nil public var downloader: ImageDownloader? = nil public var transition: ImageTransition = .none public var downloadPriority: Float = URLSessionTask.defaultPriority public var forceRefresh = false public var fromMemoryCacheOrRefresh = false public var forceTransition = false public var cacheMemoryOnly = false public var waitForCache = false public var onlyFromCache = false public var backgroundDecode = false public var preloadAllAnimationData = false public var callbackQueue: CallbackQueue = .mainCurrentOrAsync public var scaleFactor: CGFloat = 1.0 public var requestModifier: (any AsyncImageDownloadRequestModifier)? = nil public var redirectHandler: (any ImageDownloadRedirectHandler)? = nil public var processor: any ImageProcessor = DefaultImageProcessor.default public var imageModifier: (any ImageModifier)? = nil public var cacheSerializer: any CacheSerializer = DefaultCacheSerializer.default public var keepCurrentImageWhileLoading = false public var onlyLoadFirstFrame = false public var cacheOriginalImage = false public var onFailureImage: Optional = .none public var alsoPrefetchToMemory = false public var loadDiskFileSynchronously = false public var diskStoreWriteOptions: Data.WritingOptions = [] public var memoryCacheExpiration: StorageExpiration? = nil public var memoryCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime public var diskCacheExpiration: StorageExpiration? = nil public var diskCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime public var processingQueue: CallbackQueue? = nil public var progressiveJPEG: ImageProgressive? = nil public var alternativeSources: [Source]? = nil public var retryStrategy: (any RetryStrategy)? = nil public var lowDataModeSource: Source? = nil public var forcedExtension: String? = nil var onDataReceived: [any DataReceivingSideEffect]? = nil public init(_ info: KingfisherOptionsInfo?) { guard let info = info else { return } for option in info { switch option { case .targetCache(let value): targetCache = value case .originalCache(let value): originalCache = value case .downloader(let value): downloader = value case .transition(let value): transition = value case .downloadPriority(let value): downloadPriority = value case .forceRefresh: forceRefresh = true case .fromMemoryCacheOrRefresh: fromMemoryCacheOrRefresh = true case .forceTransition: forceTransition = true case .cacheMemoryOnly: cacheMemoryOnly = true case .waitForCache: waitForCache = true case .onlyFromCache: onlyFromCache = true case .backgroundDecode: backgroundDecode = true case .preloadAllAnimationData: preloadAllAnimationData = true case .callbackQueue(let value): callbackQueue = value case .scaleFactor(let value): scaleFactor = value case .requestModifier(let value): requestModifier = value case .redirectHandler(let value): redirectHandler = value case .processor(let value): processor = value case .imageModifier(let value): imageModifier = value case .cacheSerializer(let value): cacheSerializer = value case .keepCurrentImageWhileLoading: keepCurrentImageWhileLoading = true case .onlyLoadFirstFrame: onlyLoadFirstFrame = true case .cacheOriginalImage: cacheOriginalImage = true case .onFailureImage(let value): onFailureImage = .some(value) case .alsoPrefetchToMemory: alsoPrefetchToMemory = true case .loadDiskFileSynchronously: loadDiskFileSynchronously = true case .diskStoreWriteOptions(let options): diskStoreWriteOptions = options case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration case .memoryCacheAccessExtendingExpiration(let expirationExtending): memoryCacheAccessExtendingExpiration = expirationExtending case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration case .diskCacheAccessExtendingExpiration(let expirationExtending): diskCacheAccessExtendingExpiration = expirationExtending case .processingQueue(let queue): processingQueue = queue case .progressiveJPEG(let value): progressiveJPEG = value case .alternativeSources(let sources): alternativeSources = sources case .retryStrategy(let strategy): retryStrategy = strategy case .lowDataMode(let source): lowDataModeSource = source case .forcedCacheFileExtension(let ext): forcedExtension = ext } } if originalCache == nil { originalCache = targetCache } } } extension KingfisherParsedOptionsInfo { var imageCreatingOptions: ImageCreatingOptions { return ImageCreatingOptions( scale: scaleFactor, duration: 0.0, preloadAll: preloadAllAnimationData, onlyFirstFrame: onlyLoadFirstFrame) } } protocol DataReceivingSideEffect: AnyObject, Sendable { var onShouldApply: () -> Bool { get set } func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) } class ImageLoadingProgressSideEffect: DataReceivingSideEffect, @unchecked Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageLoadingProgressSideEffectPropertyQueue") private var _onShouldApply: () -> Bool = { return true } var onShouldApply: () -> Bool { get { propertyQueue.sync { _onShouldApply } } set { propertyQueue.sync { _onShouldApply = newValue } } } let block: DownloadProgressBlock init(_ block: @escaping DownloadProgressBlock) { self.block = block } func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { DispatchQueue.main.async { guard self.onShouldApply() else { return } guard let expectedContentLength = task.task.response?.expectedContentLength, expectedContentLength != -1 else { return } let dataLength = Int64(task.mutableData.count) self.block(dataLength, expectedContentLength) } } } ================================================ FILE: Sources/Image/Filter.swift ================================================ // // Filter.swift // Kingfisher // // Created by Wei Wang on 2016/08/31. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) #if os(macOS) import AppKit #else import UIKit #endif import CoreImage // Reuses the same CI Context for all CI drawings. struct SendableBox: @unchecked Sendable { let value: T } private let ciContext = SendableBox(value: CIContext(options: nil)) /// Represents the type of transformer method, which will be used to provide a ``Filter``. public typealias Transformer = (CIImage) -> CIImage? /// Represents an ``ImageProcessor`` based on a ``Filter``, for images of `CIImage`. /// /// You can use any ``Filter``, or in other words, a ``Transformer`` to convert a `CIImage` to another, to create a /// ``ImageProcessor`` type easily. public protocol CIImageProcessor: ImageProcessor { var filter: Filter { get } } extension CIImageProcessor { public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.apply(filter) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// A wrapper struct for a `Transformer` of CIImage filters. /// /// A ``Filter`` value can be used to create an ``ImageProcessor`` for `CIImage`s. public struct Filter { let transform: Transformer /// Creates a ``Filter`` from a given ``Transformer``. /// /// - Parameter transform: The value defines how a `CIImage` can be converted to another one. public init(transform: @escaping Transformer) { self.transform = transform } /// Tint filter that applies a tint color to images. public static let tint: @Sendable (KFCrossPlatformColor) -> Filter = { color in Filter { input in let colorFilter = CIFilter(name: "CIConstantColorGenerator")! colorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey) let filter = CIFilter(name: "CISourceOverCompositing")! let colorImage = colorFilter.outputImage filter.setValue(colorImage, forKey: kCIInputImageKey) filter.setValue(input, forKey: kCIInputBackgroundImageKey) return filter.outputImage?.cropped(to: input.extent) } } /// Represents color control elements. /// /// It contains necessary variables which can be applied as a filter to `CIImage.applyingFilter` feature as /// "CIColorControls". public struct ColorElement { public let brightness: CGFloat public let contrast: CGFloat public let saturation: CGFloat public let inputEV: CGFloat /// Creates a ``ColorElement`` value with given parameters. /// - Parameters: /// - brightness: The brightness change applied to the image. /// - contrast: The contrast change applied to the image. /// - saturation: The saturation change applied to the image. /// - inputEV: The EV (F-stops brighter or darker) change applied to the image. public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) { self.brightness = brightness self.contrast = contrast self.saturation = saturation self.inputEV = inputEV } } /// Color control filter that applies color control changes to images. public static let colorControl: @Sendable (ColorElement) -> Filter = { arg -> Filter in return Filter { input in let paramsColor = [kCIInputBrightnessKey: arg.brightness, kCIInputContrastKey: arg.contrast, kCIInputSaturationKey: arg.saturation] let blackAndWhite = input.applyingFilter("CIColorControls", parameters: paramsColor) let paramsExposure = [kCIInputEVKey: arg.inputEV] return blackAndWhite.applyingFilter("CIExposureAdjust", parameters: paramsExposure) } } } extension KingfisherWrapper where Base: KFCrossPlatformImage { /// Applies a `Filter` containing a `CIImage` transformer to `self`. /// /// - Parameters: /// - filter: The filter used to transform `self`. /// - Returns: A transformed image by the input `Filter`. /// /// > Important: Only CG-based images are supported. If an error occurs during transformation, /// ``KingfisherWrapper/base`` will be returned. public func apply(_ filter: Filter) -> KFCrossPlatformImage { guard let cgImage = cgImage else { assertionFailure("[Kingfisher] Tint image only works for CG-based image.") return base } let inputImage = CIImage(cgImage: cgImage) guard let outputImage = filter.transform(inputImage) else { return base } guard let result = ciContext.value.createCGImage(outputImage, from: outputImage.extent) else { assertionFailure("[Kingfisher] Can not make an tint image within context.") return base } #if os(macOS) return fixedForRetinaPixel(cgImage: result, to: size) #else return KFCrossPlatformImage(cgImage: result, scale: base.scale, orientation: base.imageOrientation) #endif } } #endif ================================================ FILE: Sources/Image/GIFAnimatedImage.swift ================================================ // // AnimatedImage.swift // Kingfisher // // Created by onevcat on 2018/09/26. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import ImageIO /// Represents a set of image creation options used in Kingfisher. public struct ImageCreatingOptions: Equatable { /// The target scale of the image that needs to be created. public var scale: CGFloat /// The expected animation duration if an animated image is being created. public var duration: TimeInterval /// For an animated image, indicates whether or not all frames should be loaded before displaying. public var preloadAll: Bool /// For an animated image, indicates whether only the first image should be /// loaded as a static image. It is useful for previewing an animated image. public var onlyFirstFrame: Bool /// Creates an `ImageCreatingOptions` object. /// /// - Parameters: /// - scale: The target scale of the image that needs to be created. Default is `1.0`. /// - duration: The expected animation duration if an animated image is being created. /// A value less than or equal to `0.0` means the animated image duration will /// be determined by the frame data. Default is `0.0`. /// - preloadAll: For an animated image, whether or not all frames should be loaded before displaying. /// Default is `false`. /// - onlyFirstFrame: For an animated image, whether only the first image should be /// loaded as a static image. It is useful for previewing an animated image. /// Default is `false`. public init( scale: CGFloat = 1.0, duration: TimeInterval = 0.0, preloadAll: Bool = false, onlyFirstFrame: Bool = false ) { self.scale = scale self.duration = duration self.preloadAll = preloadAll self.onlyFirstFrame = onlyFirstFrame } } /// Represents the decoding for a GIF image. This class extracts frames from an ``ImageFrameSource``, and then /// holds the images for later use. public class GIFAnimatedImage { let images: [KFCrossPlatformImage] let duration: TimeInterval init?(from frameSource: any ImageFrameSource, options: ImageCreatingOptions) { let frameCount = frameSource.frameCount var images = [KFCrossPlatformImage]() var gifDuration = 0.0 for i in 0 ..< frameCount { guard let imageRef = frameSource.frame(at: i) else { return nil } if frameCount == 1 { gifDuration = .infinity } else { // Get current animated GIF frame duration gifDuration += frameSource.duration(at: i) } images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil)) if options.onlyFirstFrame { break } } self.images = images self.duration = gifDuration } convenience init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) { let frameSource = CGImageFrameSource(data: nil, imageSource: imageSource, options: info) self.init(from: frameSource, options: options) } /// Calculates the frame duration for a GIF frame out of the `kCGImagePropertyGIFDictionary` dictionary. public static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval { let defaultFrameDuration = 0.1 guard let gifInfo = gifInfo else { return defaultFrameDuration } let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber let duration = unclampedDelayTime ?? delayTime guard let frameDuration = duration else { return defaultFrameDuration } return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration } /// Calculates the frame duration at a specific index for a GIF from an `CGImageSource`. /// /// - Parameters: /// - imageSource: The image source where the animated image information should be extracted from. /// - index: The index of the target frame in the image. /// - Returns: The time duration of the frame at given index in the image. public static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval { guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil) as? [String: Any] else { return 0.0 } let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] return getFrameDuration(from: gifInfo) } } /// Represents a frame source for an animated image. public protocol ImageFrameSource { /// Source data associated with this frame source. var data: Data? { get } /// Count of the total frames in this frame source. var frameCount: Int { get } /// Retrieves the frame at a specific index. /// /// The resulting image is expected to be no larger than `maxSize`. If the index is invalid, /// implementors should return `nil`. func frame(at index: Int, maxSize: CGSize?) -> CGImage? /// Retrieves the duration at a specific index. If the index is invalid, implementors should return `0.0`. func duration(at index: Int) -> TimeInterval /// Creates a copy of the current `ImageFrameSource` instance. /// /// - Returns: A new instance of the same type as `self` with identical properties. /// If not overridden by conforming types, this default implementation /// simply returns `self`, which may not create an actual copy if the type is a reference type. func copy() -> Self } public extension ImageFrameSource { /// Retrieves the frame at a specific index. If the index is invalid, implementors should return `nil`. func frame(at index: Int) -> CGImage? { return frame(at: index, maxSize: nil) } func copy() -> Self { return self } } struct CGImageFrameSource: ImageFrameSource { let data: Data? let imageSource: CGImageSource let options: [String: Any]? var frameCount: Int { return CGImageSourceGetCount(imageSource) } func frame(at index: Int, maxSize: CGSize?) -> CGImage? { var options = self.options as? [CFString: Any] if let maxSize = maxSize, maxSize != .zero { options = (options ?? [:]).merging([ kCGImageSourceCreateThumbnailFromImageIfAbsent: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceThumbnailMaxPixelSize: max(maxSize.width, maxSize.height) ], uniquingKeysWith: { $1 }) } return CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?) } func duration(at index: Int) -> TimeInterval { return GIFAnimatedImage.getFrameDuration(from: imageSource, at: index) } func copy() -> Self { guard let data = data, let source = CGImageSourceCreateWithData(data as CFData, options as CFDictionary?) else { return self } return CGImageFrameSource(data: data, imageSource: source, options: options) } } ================================================ FILE: Sources/Image/GraphicsContext.swift ================================================ // // GraphicsContext.swift // Kingfisher // // Created by taras on 19/04/2021. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(macOS) || os(watchOS) #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit #endif #if canImport(UIKit) import UIKit #endif enum GraphicsContext { static func begin(size: CGSize, scale: CGFloat) { #if os(macOS) NSGraphicsContext.saveGraphicsState() #elseif os(watchOS) UIGraphicsBeginImageContextWithOptions(size, false, scale) #else assertionFailure("This method is deprecated on the current platform and should not be used.") #endif } static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> CGContext? { #if os(macOS) let descriptor = BitmapContextDescriptor(size: size, cgImage: cgImage) guard let context = descriptor.makeContext() else { assertionFailure("[Kingfisher] Image context cannot be created.") return nil } let graphicsContext = NSGraphicsContext(cgContext: context, flipped: false) graphicsContext.imageInterpolation = .high NSGraphicsContext.current = graphicsContext return graphicsContext.cgContext #elseif os(watchOS) guard let context = UIGraphicsGetCurrentContext() else { return nil } if inverting { // If drawing a CGImage, we need to make context flipped. context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: 0, y: -size.height) } return context #else assertionFailure("This method is deprecated on the current platform and should not be used.") return nil #endif } static func end() { #if os(macOS) NSGraphicsContext.restoreGraphicsState() #elseif os(watchOS) UIGraphicsEndImageContext() #else assertionFailure("This method is deprecated on the current platform and should not be used.") #endif } } #endif #if os(macOS) private struct BitmapContextDescriptor { let width: Int let height: Int let bitsPerComponent: Int let bytesPerRow: Int let colorSpace: CGColorSpace let bitmapInfo: CGBitmapInfo init(size: CGSize, cgImage: CGImage?) { width = max(Int(size.width.rounded(.down)), 1) height = max(Int(size.height.rounded(.down)), 1) colorSpace = BitmapContextDescriptor.resolveColorSpace(from: cgImage) bitsPerComponent = BitmapContextDescriptor.supportedBitsPerComponent(from: cgImage) let componentCount = colorSpace.numberOfComponents let hasAlpha = BitmapContextDescriptor.containsAlpha(from: cgImage) bitmapInfo = BitmapContextDescriptor.bitmapInfo(componentCount: componentCount, hasAlpha: hasAlpha) let channelsPerPixel = BitmapContextDescriptor.channelsPerPixel(componentCount: componentCount, hasAlpha: hasAlpha) let bitsPerPixel = channelsPerPixel * bitsPerComponent bytesPerRow = BitmapContextDescriptor.alignedBytesPerRow(bitsPerPixel: bitsPerPixel, width: width) } func makeContext() -> CGContext? { CGContext( data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) } private static func supportedBitsPerComponent(from cgImage: CGImage?) -> Int { guard let bits = cgImage?.bitsPerComponent, bits > 0 else { return 8 } if bits <= 8 { return 8 } return 16 } private static func resolveColorSpace(from cgImage: CGImage?) -> CGColorSpace { guard let cgColorSpace = cgImage?.colorSpace else { return CGColorSpaceCreateDeviceRGB() } let components = cgColorSpace.numberOfComponents if components == 1 || components == 3 { return cgColorSpace } return CGColorSpaceCreateDeviceRGB() } private static func containsAlpha(from cgImage: CGImage?) -> Bool { guard let alphaInfo = cgImage?.alphaInfo else { return true } switch alphaInfo { case .none, .noneSkipFirst, .noneSkipLast: return false default: return true } } private static func bitmapInfo(componentCount: Int, hasAlpha: Bool) -> CGBitmapInfo { let alphaInfo: CGImageAlphaInfo if componentCount == 1 { alphaInfo = hasAlpha ? .premultipliedLast : .none } else { alphaInfo = hasAlpha ? .premultipliedLast : .noneSkipLast } return CGBitmapInfo(rawValue: alphaInfo.rawValue) } private static func channelsPerPixel(componentCount: Int, hasAlpha: Bool) -> Int { if componentCount == 1 { return hasAlpha ? 2 : 1 } return hasAlpha ? componentCount + 1 : componentCount + 1 } private static func alignedBytesPerRow(bitsPerPixel: Int, width: Int) -> Int { let rawBytes = (bitsPerPixel * width + 7) / 8 return (rawBytes + 0x3F) & ~0x3F } } #endif ================================================ FILE: Sources/Image/Image.swift ================================================ // // Image.swift // Kingfisher // // Created by Wei Wang on 16/1/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(macOS) import AppKit #else // os(macOS) import UIKit import MobileCoreServices #endif // os(macOS) #if !os(watchOS) import CoreImage #endif import CoreGraphics import ImageIO #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif #if compiler(>=5.10) nonisolated(unsafe) private let animatedImageDataKey = malloc(1)! nonisolated(unsafe) private let imageFrameCountKey = malloc(1)! nonisolated(unsafe) private let imageSourceKey = malloc(1)! nonisolated(unsafe) private let imageCreatingOptionsKey = malloc(1)! #if os(macOS) nonisolated(unsafe) private let imagesKey = malloc(1)! nonisolated(unsafe) private let durationKey = malloc(1)! #endif // os(macOS) #else // compiler(>=5.10) private let animatedImageDataKey = malloc(1)! private let imageFrameCountKey = malloc(1)! private let imageSourceKey = malloc(1)! private let imageCreatingOptionsKey = malloc(1)! #if os(macOS) private let imagesKey = malloc(1)! private let durationKey = malloc(1)! #endif // os(macOS) #endif // compiler(>=5.10) // MARK: - Image Properties extension KingfisherWrapper where Base: KFCrossPlatformImage { private(set) var animatedImageData: Data? { get { return getAssociatedObject(base, animatedImageDataKey) } set { setRetainedAssociatedObject(base, animatedImageDataKey, newValue) } } private(set) var imageCreatingOptions: ImageCreatingOptions? { get { return getAssociatedObject(base, imageCreatingOptionsKey) } set { setRetainedAssociatedObject(base, imageCreatingOptionsKey, newValue) } } public var imageFrameCount: Int? { get { return getAssociatedObject(base, imageFrameCountKey) } set { setRetainedAssociatedObject(base, imageFrameCountKey, newValue) } } #if os(macOS) var cgImage: CGImage? { return base.cgImage(forProposedRect: nil, context: nil, hints: nil) } var scale: CGFloat { return 1.0 } private(set) var images: [KFCrossPlatformImage]? { get { return getAssociatedObject(base, imagesKey) } set { setRetainedAssociatedObject(base, imagesKey, newValue) } } private(set) var duration: TimeInterval { get { return getAssociatedObject(base, durationKey) ?? 0.0 } set { setRetainedAssociatedObject(base, durationKey, newValue) } } var size: CGSize { // Prefer to use pixel size of the image let pixelSize = base.representations.reduce(.zero) { size, rep in CGSize( width: max(size.width, CGFloat(rep.pixelsWide)), height: max(size.height, CGFloat(rep.pixelsHigh)) ) } // If the pixel size is zero (SVG or PDF, for example), use the size of the image. return pixelSize == .zero ? base.representations.reduce(.zero) { size, rep in CGSize( width: max(size.width, CGFloat(rep.size.width)), height: max(size.height, CGFloat(rep.size.height)) ) } : pixelSize } #else var cgImage: CGImage? { return base.cgImage } var scale: CGFloat { return base.scale } var images: [KFCrossPlatformImage]? { return base.images } var duration: TimeInterval { return base.duration } var size: CGSize { return base.size } /// The source reference for the current image. public var imageSource: CGImageSource? { get { guard let frameSource = frameSource as? CGImageFrameSource else { return nil } return frameSource.imageSource } } #endif /// The custom frame source for the current image. public private(set) var frameSource: (any ImageFrameSource)? { get { return getAssociatedObject(base, imageSourceKey) } set { setRetainedAssociatedObject(base, imageSourceKey, newValue) } } /// Copies Kingfisher internal image states from `base` to a `target` image. /// /// This includes the embedded animated image data and related metadata that are used by Kingfisher for caching and /// animated image rendering. It is useful when a custom processor creates and returns a new image instance from /// an animated image in `.image` branch. /// /// - Important: This method does not make the `target` image animated by itself. It only propagates Kingfisher's /// internal metadata so the cache can preserve the original animated bytes when possible. /// /// - Parameter target: The target image to which the internal states will be copied. public func copyKingfisherState(to target: KFCrossPlatformImage) { target.kf.animatedImageData = animatedImageData target.kf.imageFrameCount = imageFrameCount target.kf.frameSource = frameSource target.kf.imageCreatingOptions = imageCreatingOptions #if os(macOS) target.kf.images = images target.kf.duration = duration #endif } // Bitmap memory cost with bytes. var cost: Int { let pixel = Int(size.width * size.height * scale * scale) guard let cgImage = cgImage else { return pixel * 4 } let bytesPerPixel = cgImage.bitsPerPixel / 8 guard let imageCount = images?.count else { return pixel * bytesPerPixel } return pixel * bytesPerPixel * imageCount } } // MARK: - Image Conversion extension KingfisherWrapper where Base: KFCrossPlatformImage { #if os(macOS) static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { return KFCrossPlatformImage(cgImage: cgImage, size: .zero) } /// The normalized image. On macOS, this getter returns the image itself without performing any additional operations. public var normalized: KFCrossPlatformImage { return base } #else /// Create an image from a given `CGImage` with specified scale and orientation, tailored for `refImage`. This /// method signature is designed for compatibility with macOS versions. /// /// - Parameters: /// - cgImage: The `CGImage` which is used to create the `UIImage` object. /// - scale: The scale. /// - refImage: The ref image which is used to determine the image orientation. /// - Returns: The created image object. static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { return KFCrossPlatformImage(cgImage: cgImage, scale: scale, orientation: refImage?.imageOrientation ?? .up) } /// The normalized image for the current `base` image. /// /// This method attempts to redraw the image, taking orientation and scale into account. public var normalized: KFCrossPlatformImage { // prevent animated image (GIF) lose it's images guard images == nil else { return base.copy() as! KFCrossPlatformImage } // No need to do anything if already up guard base.imageOrientation != .up else { return base.copy() as! KFCrossPlatformImage } return draw(to: size, inverting: true, refImage: KFCrossPlatformImage()) { fixOrientation(in: $0) return true } } func fixOrientation(in context: CGContext) { guard let cgImage else { return } var transform = CGAffineTransform.identity let orientation = base.imageOrientation switch orientation { case .down, .downMirrored: transform = transform.translatedBy(x: size.width, y: size.height) transform = transform.rotated(by: .pi) case .left, .leftMirrored: transform = transform.translatedBy(x: size.width, y: 0) transform = transform.rotated(by: .pi / 2.0) case .right, .rightMirrored: transform = transform.translatedBy(x: 0, y: size.height) transform = transform.rotated(by: .pi / -2.0) case .up, .upMirrored: break @unknown default: break } // Flip image one more time if needed for mirrored images. This is to prevent the flipped image. switch orientation { case .upMirrored, .downMirrored: transform = transform.translatedBy(x: size.width, y: 0) transform = transform.scaledBy(x: -1, y: 1) case .leftMirrored, .rightMirrored: transform = transform.translatedBy(x: size.height, y: 0) transform = transform.scaledBy(x: -1, y: 1) case .up, .down, .left, .right: break @unknown default: break } context.concatenate(transform) switch orientation { case .left, .leftMirrored, .right, .rightMirrored: context.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) default: context.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) } } #endif } // MARK: - Image Representation extension KingfisherWrapper where Base: KFCrossPlatformImage { /// Returns a data object that contains the specified image in PNG format. /// /// - Returns: PNG data of image. public func pngRepresentation() -> Data? { #if os(macOS) guard let cgImage = cgImage else { return nil } let rep = NSBitmapImageRep(cgImage: cgImage) return rep.representation(using: .png, properties: [:]) #else return base.pngData() #endif } /// Returns a data object that contains the specified image in JPEG format. /// /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. /// - Returns: JPEG data of image. public func jpegRepresentation(compressionQuality: CGFloat) -> Data? { #if os(macOS) guard let cgImage = cgImage else { return nil } let rep = NSBitmapImageRep(cgImage: cgImage) return rep.representation(using:.jpeg, properties: [.compressionFactor: compressionQuality]) #else return base.jpegData(compressionQuality: compressionQuality) #endif } /// Returns GIF representation of `base` image. /// /// - Returns: Original GIF data of image. public func gifRepresentation() -> Data? { return animatedImageData } /// Returns a data representation for the `base` image with the specified `format`. /// /// - Parameters: /// - format: The desired format for the output data. If set to `unknown`, the `base` image will be /// converted to PNG representation. /// - compressionQuality: The compression quality when converting the image to a lossy format data. /// /// - Returns: The resulting data representation. public func data(format: ImageFormat, compressionQuality: CGFloat = 1.0) -> Data? { return autoreleasepool { () -> Data? in let data: Data? switch format { case .PNG: data = pngRepresentation() case .JPEG: data = jpegRepresentation(compressionQuality: compressionQuality) case .GIF: data = gifRepresentation() case .unknown: data = normalized.kf.pngRepresentation() } return data } } } // MARK: - Creating Images extension KingfisherWrapper where Base: KFCrossPlatformImage { /// Creates an animated image from provided data and options. /// /// - Parameters: /// - data: The data containing the animated image. /// - options: Options to be used when creating the animated image. /// - Returns: An `Image` object representing the animated image. It's structured as an array of image frames, /// each with a specific duration. Returns `nil` if any issues occur during animated image creation. /// /// - Note: Currently, only GIF data is supported. public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { #if os(visionOS) let info: [String: Any] = [ kCGImageSourceShouldCache as String: true, kCGImageSourceTypeIdentifierHint as String: UTType.gif.identifier ] #else let info: [String: Any] = [ kCGImageSourceShouldCache as String: true, kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF ] #endif guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else { return nil } let frameSource = CGImageFrameSource(data: data, imageSource: imageSource, options: info) #if os(macOS) let baseImage = KFCrossPlatformImage(data: data) #else let baseImage = KFCrossPlatformImage(data: data, scale: options.scale) #endif return animatedImage(source: frameSource, options: options, baseImage: baseImage) } /// Creates an animated image from a given frame source. /// /// - Parameters: /// - source: The frame source from which to create the animated image. /// - options: Options to be used during animated image creation. /// - baseImage: An optional image object to serve as the key frame of the animated image. If `nil`, the first /// frame of the `source` will be used. /// - Returns: An `Image` object representing the animated image. It consists of an array of image frames, each with a /// specific duration. Returns `nil` if any issues arise during animated image creation. public static func animatedImage(source: any ImageFrameSource, options: ImageCreatingOptions, baseImage: KFCrossPlatformImage? = nil) -> KFCrossPlatformImage? { #if os(macOS) guard let animatedImage = GIFAnimatedImage(from: source, options: options) else { return nil } var image: KFCrossPlatformImage? if options.onlyFirstFrame { image = animatedImage.images.first } else { if let baseImage = baseImage { image = baseImage } else { image = animatedImage.images.first } var kf = image?.kf kf?.images = animatedImage.images kf?.duration = animatedImage.duration } image?.kf.animatedImageData = source.data image?.kf.imageFrameCount = source.frameCount image?.kf.frameSource = source image?.kf.imageCreatingOptions = options return image #else var image: KFCrossPlatformImage? if options.preloadAll || options.onlyFirstFrame { // Use `images` image if you want to preload all animated data guard let animatedImage = GIFAnimatedImage(from: source, options: options) else { return nil } if options.onlyFirstFrame { image = animatedImage.images.first } else { let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration image = .animatedImage(with: animatedImage.images, duration: duration) } image?.kf.animatedImageData = source.data } else { if let baseImage = baseImage { image = baseImage } else { guard let firstFrame = source.frame(at: 0) else { return nil } image = KFCrossPlatformImage(cgImage: firstFrame, scale: options.scale, orientation: .up) } var kf = image?.kf kf?.frameSource = source kf?.animatedImageData = source.data } image?.kf.imageFrameCount = source.frameCount image?.kf.imageCreatingOptions = options return image #endif } /// Creates an image from provided data and options. Supported formats include `.JPEG`, `.PNG`, or `.GIF`. For /// other image formats, the system's image initializer will be used. If no image object can be created from the /// given `data`, `nil` will be returned. /// /// - Parameters: /// - data: The data representing the image. /// - options: Options to be used when creating the image. /// - Returns: An `Image` object representing the image if successfully created. If the `data` is invalid or /// unsupported, `nil` will be returned. public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { var image: KFCrossPlatformImage? switch data.kf.imageFormat { case .JPEG: image = KFCrossPlatformImage(data: data, scale: options.scale) case .PNG: image = KFCrossPlatformImage(data: data, scale: options.scale) case .GIF: image = KingfisherWrapper.animatedImage(data: data, options: options) case .unknown: image = KFCrossPlatformImage(data: data, scale: options.scale) } return image } /// Creates a downsampled image from the given data to a specified size and scale. /// /// - Parameters: /// - data: The image data containing a JPEG or PNG image. /// - pointSize: The target size in points to which the image should be downsampled. /// - scale: The scale of the resulting image. /// - Returns: A downsampled `Image` object adhering to the specified conditions. /// /// Unlike image `resize` methods, downsampling does not render the original input image in pixel format. /// Instead, it downsamples directly from the image data, making it more memory-efficient and friendly. Whenever /// possible, consider using downsampling. /// /// > Important: The `pointSize` should be smaller than the size of the input image. If it is larger than the original image /// > size, the resulting image will have the same dimensions as the input without downsampling. public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? { let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else { return nil } let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale let downsampleOptions: [CFString : Any] = [ kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels ] guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions as CFDictionary) else { return nil } return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil) } } ================================================ FILE: Sources/Image/ImageDrawing.swift ================================================ // // ImageDrawing.swift // Kingfisher // // Created by onevcat on 2018/09/28. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Accelerate #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit #endif #if canImport(UIKit) import UIKit #endif extension KingfisherWrapper where Base: KFCrossPlatformImage { // MARK: - Image Transforming // MARK: Blend Mode #if !os(macOS) /// Create an image from the `base` image and apply a blend mode. /// /// - Parameters: /// - blendMode: The blend mode to be applied to the image. /// - alpha: The alpha value to be used for the image. /// - backgroundColor: The background color for the output image. /// - Returns: An image with the specified blend mode applied. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. public func image(withBlendMode blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage { guard let _ = cgImage else { assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") return base } let rect = CGRect(origin: .zero, size: size) return draw(to: rect.size, inverting: false) { _ in if let backgroundColor = backgroundColor { backgroundColor.setFill() UIRectFill(rect) } base.draw(in: rect, blendMode: blendMode, alpha: alpha) return false } } #endif #if os(macOS) // MARK: Compositing /// Create an image from the `base` image and apply a compositing operation. /// /// - Parameters: /// - compositingOperation: The compositing operation to be applied to the image. /// - alpha: The alpha value to be used for the image. /// - backgroundColor: The background color for the output image. /// - Returns: An image with the specified compositing operation applied. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. public func image(withCompositingOperation compositingOperation: NSCompositingOperation, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage { guard let _ = cgImage else { assertionFailure("[Kingfisher] Compositing Operation image only works for CG-based image.") return base } let rect = CGRect(origin: .zero, size: size) return draw(to: rect.size, inverting: false) { _ in if let backgroundColor = backgroundColor { backgroundColor.setFill() rect.fill() } base.draw(in: rect, from: .zero, operation: compositingOperation, fraction: alpha) return false } } #endif // MARK: Round Corner /// Create a rounded corner image from the `base` image. /// /// - Parameters: /// - radius: The radius for rounding the corners of the image. /// - size: The target size of the resulting image. /// - corners: The corners to which rounding will be applied. /// - backgroundColor: The background color for the output image. /// - Returns: An image with rounded corners based on `self`. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. public func image( withRadius radius: Radius, fit size: CGSize, roundingCorners corners: RectCorner = .all, backgroundColor: KFCrossPlatformColor? = nil ) -> KFCrossPlatformImage { guard let _ = cgImage else { assertionFailure("[Kingfisher] Round corner image only works for CG-based image.") return base } let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) return draw(to: size, inverting: false) { _ in #if os(macOS) if let backgroundColor = backgroundColor { let rectPath = NSBezierPath(rect: rect) backgroundColor.setFill() rectPath.fill() } let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) path.addClip() base.draw(in: rect) #else guard let context = UIGraphicsGetCurrentContext() else { assertionFailure("[Kingfisher] Failed to create CG context for image.") return false } if let backgroundColor = backgroundColor { let rectPath = UIBezierPath(rect: rect) backgroundColor.setFill() rectPath.fill() } let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) context.addPath(path.cgPath) context.clip() base.draw(in: rect) #endif return false } } /// Create a round corner image from the `base` image. /// /// - Parameters: /// - radius: The radius for rounding the corners of the image. /// - size: The target size of the resulting image. /// - corners: The corners to which rounding will be applied. /// - backgroundColor: The background color for the output image. /// - Returns: An image with rounded corners based on `self`. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. public func image( withRoundRadius radius: CGFloat, fit size: CGSize, roundingCorners corners: RectCorner = .all, backgroundColor: KFCrossPlatformColor? = nil ) -> KFCrossPlatformImage { image(withRadius: .point(radius), fit: size, roundingCorners: corners, backgroundColor: backgroundColor) } #if os(macOS) func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> NSBezierPath { let cornerRadius = radius.compute(with: rect.size) let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: cornerRadius - offsetBase / 2) path.windingRule = .evenOdd return path } #else func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> UIBezierPath { let cornerRadius = radius.compute(with: rect.size) return UIBezierPath( roundedRect: rect, byRoundingCorners: corners.uiRectCorner, cornerRadii: CGSize( width: cornerRadius - offsetBase / 2, height: cornerRadius - offsetBase / 2 ) ) } #endif #if os(iOS) || os(tvOS) || os(visionOS) func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> KFCrossPlatformImage { switch contentMode { case .scaleAspectFit: return resize(to: size, for: .aspectFit) case .scaleAspectFill: return resize(to: size, for: .aspectFill) default: return resize(to: size) } } #endif // MARK: Resizing /// Resize the `base` image to a new size. /// /// - Parameter size: The target size in points. /// - Returns: An image with the new size. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. /// /// > Tip: This method resizes the `base` image to a specified size by drawing it into that size. If you require a /// smaller thumbnail of the image, consider using ``downsampledImage(data:to:scale:)`` instead, as it offers /// improved efficiency. public func resize(to size: CGSize) -> KFCrossPlatformImage { guard let _ = cgImage else { assertionFailure("[Kingfisher] Resize only works for CG-based image.") return base } let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) return draw(to: size, inverting: false) { _ in #if os(macOS) base.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) #else base.draw(in: rect) #endif return false } } /// Resize the `base` image to a new size while respecting the specified content mode. /// /// - Parameters: /// - targetSize: The target size in points. /// - contentMode: The desired content mode for the output image. /// - Returns: An image with the new size. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. /// /// > Tip: This method resizes the `base` image to a specified size by drawing it into that size. If you require a /// smaller thumbnail of the image, consider using ``downsampledImage(data:to:scale:)`` instead, as it offers /// improved efficiency. public func resize(to targetSize: CGSize, for contentMode: ContentMode) -> KFCrossPlatformImage { let newSize = size.kf.resize(to: targetSize, for: contentMode) return resize(to: newSize) } // MARK: Cropping /// Crop the `base` image to a new size with a specified anchor point. /// /// - Parameters: /// - size: The target size. /// - anchor: The anchor point from which the size should be calculated. /// - Returns: An image with the new size. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. public func crop(to size: CGSize, anchorOn anchor: CGPoint) -> KFCrossPlatformImage { guard let cgImage = cgImage else { assertionFailure("[Kingfisher] Crop only works for CG-based image.") return base } let rect = self.size.kf.constrainedRect(for: size, anchor: anchor) guard let image = cgImage.cropping(to: rect.scaled(scale)) else { assertionFailure("[Kingfisher] Cropping image failed.") return base } return KingfisherWrapper.image(cgImage: image, scale: scale, refImage: base) } // MARK: Blur /// Create an image with a blur effect based on the `base` image. /// /// - Parameter radius: The blur radius to be used when creating the blur effect. /// - Returns: An image with the blur effect applied. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. public func blurred(withRadius radius: CGFloat) -> KFCrossPlatformImage { guard let cgImage = cgImage else { assertionFailure("[Kingfisher] Blur only works for CG-based image.") return base } // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) // if d is odd, use three box-blurs of size 'd', centered on the output pixel. let s = max(radius, 2.0) // We will do blur on a resized image (*0.5), so the blur radius could be half as well. // Fix the slow compiling time for Swift 3. // See https://github.com/onevcat/Kingfisher/issues/611 let pi2 = 2 * CGFloat.pi let sqrtPi2 = sqrt(pi2) var targetRadius = floor(s * 3.0 * sqrtPi2 / 4.0 + 0.5) if targetRadius.isEven { targetRadius += 1 } // Determine necessary iteration count by blur radius. let iterations: Int if radius < 0.5 { iterations = 1 } else if radius < 1.5 { iterations = 2 } else { iterations = 3 } func createEffectBuffer(_ context: CGContext) -> vImage_Buffer { let data = context.data let width = vImagePixelCount(context.width) let height = vImagePixelCount(context.height) let rowBytes = context.bytesPerRow return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes) } guard let inputContext = CGContext.fresh(cgImage: cgImage) else { return base } inputContext.draw( cgImage, in: CGRect( x: 0, y: 0, width: size.width * scale, height: size.height * scale ) ) var inBuffer = createEffectBuffer(inputContext) guard let outContext = CGContext.fresh(cgImage: cgImage) else { return base } var outBuffer = createEffectBuffer(outContext) for _ in 0 ..< iterations { let flag = vImage_Flags(kvImageEdgeExtend) vImageBoxConvolve_ARGB8888( &inBuffer, &outBuffer, nil, 0, 0, UInt32(targetRadius), UInt32(targetRadius), nil, flag) // Next inBuffer should be the outButter of current iteration (inBuffer, outBuffer) = (outBuffer, inBuffer) } #if os(macOS) let result = outContext.makeImage().flatMap { fixedForRetinaPixel(cgImage: $0, to: size) } #else let result = outContext.makeImage().flatMap { KFCrossPlatformImage(cgImage: $0, scale: base.scale, orientation: base.imageOrientation) } #endif guard let blurredImage = result else { assertionFailure("[Kingfisher] Can not make an blurred image within this context.") return base } return blurredImage } public func addingBorder(_ border: Border) -> KFCrossPlatformImage { guard let _ = cgImage else { assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") return base } let rect = CGRect(origin: .zero, size: size) return draw(to: rect.size, inverting: false) { context in #if os(macOS) base.draw(in: rect) #else base.draw(in: rect, blendMode: .normal, alpha: 1.0) #endif let strokeRect = rect.insetBy(dx: border.lineWidth / 2, dy: border.lineWidth / 2) context.setStrokeColor(border.color.cgColor) context.setAlpha(border.color.rgba.a) let line = pathForRoundCorner( rect: strokeRect, radius: border.radius, corners: border.roundingCorners, offsetBase: border.lineWidth ) line.lineCapStyle = .square line.lineWidth = border.lineWidth line.stroke() return false } } // MARK: Overlay /// Create an image from the `base` image with a color overlay layer. /// /// - Parameters: /// - color: The color to be used for the overlay. /// - fraction: The fraction of the input color to apply, ranging from 0.0 (solid color) to 1.0 (transparent overlay). /// - Returns: An image with a color overlay applied. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. public func overlaying(with color: KFCrossPlatformColor, fraction: CGFloat) -> KFCrossPlatformImage { guard let _ = cgImage else { assertionFailure("[Kingfisher] Overlaying only works for CG-based image.") return base } let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) return draw(to: rect.size, inverting: false) { context in #if os(macOS) base.draw(in: rect) if fraction > 0 { color.withAlphaComponent(1 - fraction).set() rect.fill(using: .sourceAtop) } #else color.set() UIRectFill(rect) base.draw(in: rect, blendMode: .destinationIn, alpha: 1.0) if fraction > 0 { base.draw(in: rect, blendMode: .sourceAtop, alpha: fraction) } #endif return false } } // MARK: Tint /// Create an image from the `base` image with a color tint. /// /// - Parameter color: The color to be used for tinting the `base` image. /// - Returns: An image with a color tint applied. /// /// > Important: This method does not work on watchOS, where the original image is returned. public func tinted(with color: KFCrossPlatformColor) -> KFCrossPlatformImage { #if os(watchOS) return base #else return apply(.tint(color)) #endif } // MARK: Color Control /// Create an image from `self` with color control adjustments. /// /// - Parameters: /// - brightness: The degree of brightness adjustment to apply to the image. /// - contrast: The degree of contrast adjustment to apply to the image. /// - saturation: The degree of saturation adjustment to apply to the image. /// - inputEV: The exposure value (EV) adjustment to apply to the image. /// - Returns: An image with color control adjustments applied. /// /// > Important: This method does not work on watchOS, where the original image is returned. public func adjusted(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) -> KFCrossPlatformImage { #if os(watchOS) return base #else let colorElement = Filter.ColorElement( brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV ) return apply(.colorControl(colorElement)) #endif } /// Return an image with the specified scale. /// /// - Parameter scale: The target scale factor for the new image. /// - Returns: The image with the target scale. If the base image is already at the target scale, the `base` image /// will be returned. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image, the `base` image itself is returned. public func scaled(to scale: CGFloat) -> KFCrossPlatformImage { guard scale != self.scale else { return base } guard let cgImage = cgImage else { assertionFailure("[Kingfisher] Scaling only works for CG-based image.") return base } return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base) } } // MARK: - Decoding Image extension KingfisherWrapper where Base: KFCrossPlatformImage { /// Returns the decoded image of the `base` image. /// /// On iOS 15 or later, this is identical to the `UIImage.preparingForDisplay` method. /// /// In previous versions, this method draws the image in a plain context and returns the data from it. Using this /// method can improve drawing performance when an image is created from data but hasn't been displayed for the /// first time. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image or animated image, the `base` image itself is returned. public var decoded: KFCrossPlatformImage { return decoded(scale: scale) } /// Returns the decoded image of the `base` image at a given `scale`. /// /// On iOS 15 or later, this is identical to the `UIImage.preparingForDisplay` method. /// /// In previous versions, this method draws the image in a plain context and returns the data from it. Using this /// method can improve drawing performance when an image is created from data but hasn't been displayed for the /// first time. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image or animated image, the `base` image itself is returned. public func decoded(scale: CGFloat) -> KFCrossPlatformImage { // Prevent animated image (GIF) losing it's images #if os(iOS) || os(visionOS) if frameSource != nil { return base } #else if images != nil { return base } #endif // For older system versions, revert to the drawing for decoding. guard let imageRef = cgImage else { assertionFailure("[Kingfisher] Decoding only works for CG-based image.") return base } #if !os(watchOS) && !os(macOS) // In newer system versions, use `preparingForDisplay`. if #available(iOS 15.0, tvOS 15.0, visionOS 1.0, *) { if base.scale == scale, let image = base.preparingForDisplay() { return image } let scaledImage = KFCrossPlatformImage(cgImage: imageRef, scale: scale, orientation: base.imageOrientation) if let image = scaledImage.preparingForDisplay() { return image } } #endif let size = CGSize(width: CGFloat(imageRef.width) / scale, height: CGFloat(imageRef.height) / scale) return draw(to: size, inverting: true, scale: scale) { context in context.draw(imageRef, in: CGRect(origin: .zero, size: size)) return true } } /// Returns the decoded image of the `base` image on a given `context`. /// /// This method draws the image in the given context and returns the data from it. Using this /// method can improve drawing performance when an image is created from data but hasn't been displayed for the /// first time. /// /// > This method is only applicable to CG-based images. The current image scale is preserved. /// > For any non-CG-based image or animated image, the `base` image itself is returned. public func decoded(on context: CGContext) -> KFCrossPlatformImage { // Prevent animated image (GIF) losing it's images if frameSource != nil { return base } guard let refImage = cgImage, let decodedRefImage = refImage.decoded(on: context, scale: scale) else { assertionFailure("[Kingfisher] Decoding only works for CG-based image.") return base } return KingfisherWrapper.image(cgImage: decodedRefImage, scale: scale, refImage: base) } } extension CGImage { func decoded(on context: CGContext, scale: CGFloat) -> CGImage? { let size = CGSize(width: CGFloat(self.width) / scale, height: CGFloat(self.height) / scale) context.draw(self, in: CGRect(origin: .zero, size: size)) guard let decodedImageRef = context.makeImage() else { return nil } return decodedImageRef } static func create(ref: CGImage) -> CGImage? { guard let space = ref.colorSpace, let provider = ref.dataProvider else { return nil } return CGImage( width: ref.width, height: ref.height, bitsPerComponent: ref.bitsPerComponent, bitsPerPixel: ref.bitsPerPixel, bytesPerRow: ref.bytesPerRow, space: space, bitmapInfo: ref.bitmapInfo, provider: provider, decode: ref.decode, shouldInterpolate: ref.shouldInterpolate, intent: ref.renderingIntent ) } } extension KingfisherWrapper where Base: KFCrossPlatformImage { func draw( to size: CGSize, inverting: Bool, scale: CGFloat? = nil, refImage: KFCrossPlatformImage? = nil, draw: (CGContext) -> Bool // Whether use the refImage (`true`) or ignore image orientation (`false`) ) -> KFCrossPlatformImage { #if os(macOS) || os(watchOS) let targetScale = scale ?? self.scale GraphicsContext.begin(size: size, scale: targetScale) guard let context = GraphicsContext.current(size: size, scale: targetScale, inverting: inverting, cgImage: cgImage) else { assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") return base } defer { GraphicsContext.end() } let useRefImage = draw(context) guard let cgImage = context.makeImage() else { return base } let ref = useRefImage ? (refImage ?? base) : nil return KingfisherWrapper.image(cgImage: cgImage, scale: targetScale, refImage: ref) #else let format = UIGraphicsImageRendererFormat.preferred() format.scale = scale ?? self.scale let renderer = UIGraphicsImageRenderer(size: size, format: format) var useRefImage: Bool = false let image = renderer.image { rendererContext in let context = rendererContext.cgContext if inverting { // If drawing a CGImage, we need to make context flipped. context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: 0, y: -size.height) } useRefImage = draw(context) } if useRefImage { guard let cgImage = image.cgImage else { return base } let ref = refImage ?? base return KingfisherWrapper.image(cgImage: cgImage, scale: format.scale, refImage: ref) } else { return image } #endif } #if os(macOS) func fixedForRetinaPixel(cgImage: CGImage, to size: CGSize) -> KFCrossPlatformImage { let image = KFCrossPlatformImage(cgImage: cgImage, size: base.size) let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) return draw(to: self.size, inverting: false) { context in image.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) return false } } #endif } extension CGContext { fileprivate static func fresh(cgImage: CGImage) -> CGContext? { CGContext( data: nil, width: cgImage.width, height: cgImage.height, bitsPerComponent: 8, bytesPerRow: 4 * cgImage.width, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) } } ================================================ FILE: Sources/Image/ImageFormat.swift ================================================ // // ImageFormat.swift // Kingfisher // // Created by onevcat on 2018/09/28. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents the image format. public enum ImageFormat: Sendable { /// The format cannot be recognized or not supported yet. case unknown /// PNG image format. case PNG /// JPEG image format. case JPEG /// GIF image format. case GIF struct HeaderData { static let PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] static let JPEG_SOI: [UInt8] = [0xFF, 0xD8] static let JPEG_IF: [UInt8] = [0xFF] static let GIF: [UInt8] = [0x47, 0x49, 0x46] } /// JPEG marker of each sequence of segments. /// /// See also [here](https://www.digicamsoft.com/itu/itu-t81-36.html). public enum JPEGMarker { case SOF0 //baseline case SOF2 //progressive case DHT //Huffman Table case DQT //Quantization Table case DRI //Restart Interval case SOS //Start Of Scan case RSTn(UInt8) //Restart case APPn //Application-specific case COM //Comment case EOI //End Of Image var bytes: [UInt8] { switch self { case .SOF0: return [0xFF, 0xC0] case .SOF2: return [0xFF, 0xC2] case .DHT: return [0xFF, 0xC4] case .DQT: return [0xFF, 0xDB] case .DRI: return [0xFF, 0xDD] case .SOS: return [0xFF, 0xDA] case .RSTn(let n): return [0xFF, 0xD0 + n] case .APPn: return [0xFF, 0xE0] case .COM: return [0xFF, 0xFE] case .EOI: return [0xFF, 0xD9] } } } } extension Data: KingfisherCompatibleValue {} // MARK: - Misc Helpers extension KingfisherWrapper where Base == Data { /// Gets the image format corresponding to the data. public var imageFormat: ImageFormat { guard base.count > 8 else { return .unknown } var buffer = [UInt8](repeating: 0, count: 8) base.copyBytes(to: &buffer, count: 8) if buffer == ImageFormat.HeaderData.PNG { return .PNG } else if buffer[0] == ImageFormat.HeaderData.JPEG_SOI[0], buffer[1] == ImageFormat.HeaderData.JPEG_SOI[1], buffer[2] == ImageFormat.HeaderData.JPEG_IF[0] { return .JPEG } else if buffer[0] == ImageFormat.HeaderData.GIF[0], buffer[1] == ImageFormat.HeaderData.GIF[1], buffer[2] == ImageFormat.HeaderData.GIF[2] { return .GIF } return .unknown } public func contains(jpeg marker: ImageFormat.JPEGMarker) -> Bool { guard imageFormat == .JPEG else { return false } let bytes = [UInt8](base) let markerBytes = marker.bytes for (index, item) in bytes.enumerated() where bytes.count > index + 1 { guard item == markerBytes.first, bytes[index + 1] == markerBytes[1] else { continue } return true } return false } } ================================================ FILE: Sources/Image/ImageProcessor.swift ================================================ // // ImageProcessor.swift // Kingfisher // // Created by Wei Wang on 2016/08/26. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import CoreGraphics #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit #else import UIKit #endif /// Represents an item which could be processed by an `ImageProcessor`. public enum ImageProcessItem: Sendable { /// Input image. The processor should provide a method to apply /// processing to this `image` and return the resulting image. case image(KFCrossPlatformImage) /// Input data. The processor should provide a method to apply /// processing to this `data` and return the resulting image. case data(Data) } /// An `ImageProcessor` is used to convert downloaded data into an image. public protocol ImageProcessor: Sendable { /// Identifier for the processor. /// /// This identifier is used to distinguish the processor when caching and retrieving an image. Ensure that /// processors with the same properties or functionality share the same identifier so that processed images can be /// retrieved with the correct key. /// /// > Important: Avoid using an empty string for a custom processor, as it is already reserved by the /// > `DefaultImageProcessor`. It is recommended to use a reverse domain name notation string for your identifier. var identifier: String { get } /// Process the input `ImageProcessItem` using this processor. /// /// - Parameters: /// - item: The input item to be processed by `self`. /// - options: The parsed options for processing the item. /// - Returns: The processed image. /// /// You should return `nil` if processing fails when converting an input item to an image. If the processing /// caller receives `nil`, an error will be reported, and the processing flow will stop. If processing flow is not /// critical for your use case, and the input item is already an image (`.image` case), you can also choose to /// return the input image itself to continue the processing pipeline. /// /// > Important: Most processors only support CG-based images. The watchOS is not supported for processors /// > containing a filter, and the input image will be returned directly on watchOS. func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? } extension ImageProcessor { /// Append an `ImageProcessor` to another. The identifier of the new `ImageProcessor` will /// be `"\(self.identifier)|>\(another.identifier)"`. /// /// - Parameter another: An `ImageProcessor` to be appended to `self`. /// - Returns: The new `ImageProcessor` that will process the image in the order of the two processors concatenated. public func append(another: any ImageProcessor) -> any ImageProcessor { let newIdentifier = identifier.appending("|>\(another.identifier)") return GeneralProcessor(identifier: newIdentifier) { item, options in if let image = self.process(item: item, options: options) { return another.process(item: .image(image), options: options) } else { return nil } } } } func ==(left: any ImageProcessor, right: any ImageProcessor) -> Bool { return left.identifier == right.identifier } func !=(left: any ImageProcessor, right: any ImageProcessor) -> Bool { return !(left == right) } typealias ProcessorImp = (@Sendable (ImageProcessItem, KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?) struct GeneralProcessor: ImageProcessor { let identifier: String let p: ProcessorImp func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { return p(item, options) } } /// The default processor. It converts the input data into a valid image. /// /// Supported image formats include .PNG, .JPEG, and .GIF. If an image item is provided as the /// ``ImageProcessItem/image(_:)`` case, ``DefaultImageProcessor`` will leave it unchanged and return the associated /// image. public struct DefaultImageProcessor: ImageProcessor { /// A default instance of ``DefaultImageProcessor`` can be used across the framework. public static let `default` = DefaultImageProcessor() public let identifier = "" /// Create a ``DefaultImageProcessor``. /// /// Use ``DefaultImageProcessor/default`` to obtain an instance if you have no specific reason to create your own /// ``DefaultImageProcessor``. public init() {} public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.scaled(to: options.scaleFactor) case .data(let data): return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) } } } /// Represents the rect corner setting when processing a round corner image. public struct RectCorner: OptionSet, Sendable { /// Raw value for the corner radius. public let rawValue: Int /// Represents the top left corner. public static let topLeft = RectCorner(rawValue: 1 << 0) /// Represents the top right corner. public static let topRight = RectCorner(rawValue: 1 << 1) /// Represents the bottom left corner. public static let bottomLeft = RectCorner(rawValue: 1 << 2) /// Represents the bottom right corner. public static let bottomRight = RectCorner(rawValue: 1 << 3) /// Represents all corners. public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight] /// Create a `RectCorner` option set with a specified value. /// /// - Parameter rawValue: The value representing a specific corner option. public init(rawValue: Int) { self.rawValue = rawValue } var cornerIdentifier: String { if self == .all { return "" } return "_corner(\(rawValue))" } } #if !os(macOS) /// Processor for applying a blend mode to images. /// /// Supported for CG-based images only. public struct BlendImageProcessor: ImageProcessor { public let identifier: String /// The blend mode used to blend the input image. public let blendMode: CGBlendMode /// The alpha value used when blending the image. public let alpha: CGFloat /// The background color of the output image. /// /// If `nil`, the background will remain transparent. public let backgroundColor: KFCrossPlatformColor? /// Create a `BlendImageProcessor`. /// /// - Parameters: /// - blendMode: The blend mode to be used for blending the input image. /// - alpha: The alpha value to be used when blending the image, ranging from 0.0 (completely transparent) to /// 1.0 (completely solid). Default is 1.0. /// - backgroundColor: The background color to apply to the output image. Default is `nil`. public init(blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) { self.blendMode = blendMode self.alpha = alpha self.backgroundColor = backgroundColor var identifier = "com.onevcat.Kingfisher.BlendImageProcessor(\(blendMode.rawValue),\(alpha))" if let color = backgroundColor { identifier.append("_\(color.rgbaDescription)") } self.identifier = identifier } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.scaled(to: options.scaleFactor) .kf.image(withBlendMode: blendMode, alpha: alpha, backgroundColor: backgroundColor) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } #endif #if os(macOS) /// Processor for applying a compositing operation to images. /// /// Supported for CG-based images on macOS. public struct CompositingImageProcessor: ImageProcessor { public let identifier: String /// The compositing operation applied to the input image. public let compositingOperation: NSCompositingOperation /// The alpha value used when compositing the image. public let alpha: CGFloat /// The background color of the output image. If `nil`, the background will remain transparent. public let backgroundColor: KFCrossPlatformColor? /// Create a `CompositingImageProcessor`. /// /// - Parameters: /// - compositingOperation: The compositing operation to be applied to the input image. /// - alpha: The alpha value to be used when compositing the image, ranging from 0.0 (completely transparent) to /// 1.0 (completely solid). Default is 1.0. /// - backgroundColor: The background color to apply to the output image. Default is `nil`. public init(compositingOperation: NSCompositingOperation, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) { self.compositingOperation = compositingOperation self.alpha = alpha self.backgroundColor = backgroundColor var identifier = "com.onevcat.Kingfisher.CompositingImageProcessor(\(compositingOperation.rawValue),\(alpha))" if let color = backgroundColor { identifier.append("_\(color.rgbaDescription)") } self.identifier = identifier } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.scaled(to: options.scaleFactor) .kf.image( withCompositingOperation: compositingOperation, alpha: alpha, backgroundColor: backgroundColor) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } #endif /// Represents a radius specified in a ``RoundCornerImageProcessor``. public enum Radius: Sendable { /// The radius should be calculated as a fraction of the image width. Typically, the associated value should be /// between 0 and 0.5, where 0 represents no radius, and 0.5 represents using half of the image width. case widthFraction(CGFloat) /// The radius should be calculated as a fraction of the image height. Typically, the associated value should be /// between 0 and 0.5, where 0 represents no radius, and 0.5 represents using half of the image height. case heightFraction(CGFloat) /// Use a fixed point value as the round corner radius. case point(CGFloat) var radiusIdentifier: String { switch self { case .widthFraction(let f): return "w_frac_\(f)" case .heightFraction(let f): return "h_frac_\(f)" case .point(let p): return p.description } } public func compute(with size: CGSize) -> CGFloat { let cornerRadius: CGFloat switch self { case .point(let point): cornerRadius = point case .widthFraction(let widthFraction): cornerRadius = size.width * widthFraction case .heightFraction(let heightFraction): cornerRadius = size.height * heightFraction } return cornerRadius } } /// Processor for creating round corner images. /// /// Supported for CG-based images on macOS. If a non-CG image is passed in, the processor will have no effect. /// /// > Tip: The input image will be rendered with round corner pixels removed. If the image itself does not contain an /// > alpha channel (for example, a JPEG image), the processed image will contain an alpha channel in memory for /// > correct rendering. However, when cached to disk, Kingfisher defaults to preserving the original image format. /// > This means the alpha channel will be removed for these images. If you load the processed image from the cache /// > again, you will lose the transparent corners. /// > /// > You can use ``FormatIndicatedCacheSerializer/png`` to force Kingfisher to serialize the image to PNG format in this /// > case. public struct RoundCornerImageProcessor: ImageProcessor { public let identifier: String /// The radius to be applied during processing. /// /// Specify a specific point value with ``Radius/point(_:)``, or a fraction of the target image with /// ``Radius/widthFraction(_:)`` or ``Radius/heightFraction(_:)``. For example, if you have a square image with /// equal width and height, `.widthFraction(0.5)` means using half of the width of the size to create a round image. public let radius: Radius /// The target corners to round. public let roundingCorners: RectCorner /// The target size for the output image. If `nil`, the image will retain its original size after processing. public let targetSize: CGSize? /// The background color for the output image. If `nil`, it will use a transparent background. public let backgroundColor: KFCrossPlatformColor? /// Create a ``RoundCornerImageProcessor`` with given parameters. /// /// - Parameters: /// - cornerRadius: The corner radius in points to be applied during processing. /// - targetSize: The target size for the output image. If `nil`, the image will retain its original size after /// processing. Default is `nil`. /// - corners: The target corners to round. Default is ``RectCorner/all``. /// - backgroundColor: The background color to apply to the output image. Default is `nil`. /// /// This initializer accepts a specific point value for `cornerRadius`. If you don't know the image size but still /// want to apply a full round corner (making the final image round), or specify the corner radius as a fraction of /// one dimension of the target image, use the ``init(radius:targetSize:roundingCorners:backgroundColor:)`` /// instead. public init( cornerRadius: CGFloat, targetSize: CGSize? = nil, roundingCorners corners: RectCorner = .all, backgroundColor: KFCrossPlatformColor? = nil ) { let radius = Radius.point(cornerRadius) self.init(radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor) } /// Create a `RoundCornerImageProcessor`. /// /// - Parameters: /// - radius: The radius to be applied during processing. /// - targetSize: The target size for the output image. If `nil`, the image will retain its original size after /// processing. Default is `nil`. /// - corners: The target corners to round. Default is ``RectCorner/all``. /// - backgroundColor: The background color to apply to the output image. Default is `nil`. public init( radius: Radius, targetSize: CGSize? = nil, roundingCorners corners: RectCorner = .all, backgroundColor: KFCrossPlatformColor? = nil ) { self.radius = radius self.targetSize = targetSize self.roundingCorners = corners self.backgroundColor = backgroundColor self.identifier = { var identifier = "" if let size = targetSize { identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + "(\(radius.radiusIdentifier)_\(size)\(corners.cornerIdentifier))" } else { identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + "(\(radius.radiusIdentifier)\(corners.cornerIdentifier))" } if let backgroundColor = backgroundColor { identifier += "_\(backgroundColor)" } return identifier }() } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): let size = targetSize ?? image.kf.size return image.kf.scaled(to: options.scaleFactor) .kf.image( withRadius: radius, fit: size, roundingCorners: roundingCorners, backgroundColor: backgroundColor) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// Represents a border to be added to the image. /// /// Typically used with ``BorderImageProcessor``, which adds the border to the image. public struct Border: Sendable { /// The color of the border to create. public var color: KFCrossPlatformColor /// The line width of the border to create. public var lineWidth: CGFloat /// The radius to be applied during processing. /// /// Specify a specific point value with ``Radius/point(_:)``, or a fraction of the target image with /// ``Radius/widthFraction(_:)`` or ``Radius/heightFraction(_:)``. For example, if you have a square image with /// equal width and height, `.widthFraction(0.5)` means using half of the width of the size to create a round image. public var radius: Radius /// The target corners which will be applied rounding. public var roundingCorners: RectCorner /// Creates a border. /// - Parameters: /// - color: The color will be used to render the border. /// - lineWidth: The line width of the border. /// - radius: The radius of the border corner. /// - roundingCorners: The target corners type. public init( color: KFCrossPlatformColor = .black, lineWidth: CGFloat = 4, radius: Radius = .point(0), roundingCorners: RectCorner = .all ) { self.color = color self.lineWidth = lineWidth self.radius = radius self.roundingCorners = roundingCorners } var identifier: String { "\(color.rgbaDescription)_\(lineWidth)_\(radius.radiusIdentifier)_\(roundingCorners.cornerIdentifier)" } } /// Processor for creating bordered images. public struct BorderImageProcessor: ImageProcessor { public var identifier: String { "com.onevcat.Kingfisher.BorderImageProcessor(\(border)" } /// The border to be added to the image. public let border: Border /// Create a `BorderImageProcessor` with a given `Border`. /// /// - Parameter border: The border to be added to the image. public init(border: Border) { self.border = border } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.addingBorder(border) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// Represents how a size of content adjusts itself to fit a target size. public enum ContentMode: Sendable { /// Does not scale the content. case none /// Scales the content to fit the size of the view while maintaining the aspect ratio. case aspectFit /// Scales the content to fill the size of the view. case aspectFill } /// Processor for resizing images. /// /// If you need to resize an image represented by data to a smaller size, use ``DownsamplingImageProcessor`` instead, /// which is more efficient and uses less memory. public struct ResizingImageProcessor: ImageProcessor { public let identifier: String /// The reference size for the resizing operation in points. public let referenceSize: CGSize /// The target content mode of the output image. public let targetContentMode: ContentMode /// Create a ``ResizingImageProcessor``. /// /// - Parameters: /// - referenceSize: The reference size for the resizing operation in points. /// - mode: The target content mode of the output image. /// /// The instance of ``ResizingImageProcessor`` will follow the `mode` argument and attempt to resize the input /// images to fit or fill the `referenceSize`. This means if you are using a `mode` besides `.none`, you may get an /// image with a size that is not the same as the `referenceSize`. /// /// For example, with an input image size of {100, 200}, `referenceSize` of {100, 100}, and `mode` of `.aspectFit`, /// you will get an output image with a size of {50, 100} that "fits" the `referenceSize`. /// /// > If you need an output image to be exactly a specified size, append or use a ``CroppingImageProcessor``. public init(referenceSize: CGSize, mode: ContentMode = .none) { self.referenceSize = referenceSize self.targetContentMode = mode if mode == .none { self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize))" } else { self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize), \(mode))" } } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.scaled(to: options.scaleFactor) .kf.resize(to: referenceSize, for: targetContentMode) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// Processor for adding a blur effect to images. /// /// Uses `Accelerate.framework` under the hood for better performance. Applies a simulated Gaussian blur with the /// specified blur radius. public struct BlurImageProcessor: ImageProcessor { public let identifier: String /// The blur radius for the simulated Gaussian blur. public let blurRadius: CGFloat /// Create a `BlurImageProcessor`. /// /// - Parameter blurRadius: The blur radius for the simulated Gaussian blur. public init(blurRadius: CGFloat) { self.blurRadius = blurRadius self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))" } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): let radius = blurRadius * options.scaleFactor return image.kf.scaled(to: options.scaleFactor) .kf.blurred(withRadius: radius) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// Processor for adding an overlay to images. /// /// > Only CG-based images are supported. public struct OverlayImageProcessor: ImageProcessor { public let identifier: String /// The overlay color used to overlay the input image. public let overlay: KFCrossPlatformColor /// The fraction used when overlaying the color to the image. public let fraction: CGFloat /// Create an ``OverlayImageProcessor``. /// /// - Parameters: /// - overlay: The overlay color used to overlay the input image. /// - fraction: The fraction used when overlaying the color to the image. /// Ranges from 0.0 to 1.0. 0.0 means a solid color, and 1.0 means a transparent overlay. public init(overlay: KFCrossPlatformColor, fraction: CGFloat = 0.5) { self.overlay = overlay self.fraction = fraction self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.rgbaDescription)_\(fraction))" } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.scaled(to: options.scaleFactor) .kf.overlaying(with: overlay, fraction: fraction) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// Processor for tinting images with color. /// /// > Only CG-based images are supported. /// /// > Important: On watchOS, there is no tint support and the original image will be returned. public struct TintImageProcessor: ImageProcessor { public let identifier: String /// The tint color used to tint the input image. public let tint: KFCrossPlatformColor /// Create a ``TintImageProcessor``. /// /// - Parameter tint: The tint color used to tint the input image. public init(tint: KFCrossPlatformColor) { self.tint = tint self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.rgbaDescription))" } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.scaled(to: options.scaleFactor) .kf.tinted(with: tint) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// Processor for applying color control to images. /// /// > Only CG-based images are supported. /// /// > Important: On watchOS, there is no color control support and the original image will be returned. public struct ColorControlsProcessor: ImageProcessor { public let identifier: String /// The brightness change applied to the image. public let brightness: CGFloat /// The contrast change applied to the image. public let contrast: CGFloat /// The saturation change applied to the image. public let saturation: CGFloat /// The EV (F-stops brighter or darker) change applied to the image. public let inputEV: CGFloat /// Create a ``ColorControlsProcessor``. /// /// - Parameters: /// - brightness: The brightness change applied to the image. /// - contrast: The contrast change applied to the image. /// - saturation: The saturation change applied to the image. /// - inputEV: The EV (F-stops brighter or darker) change applied to the image. public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) { self.brightness = brightness self.contrast = contrast self.saturation = saturation self.inputEV = inputEV self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))" } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.scaled(to: options.scaleFactor) .kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// Processor for applying black and white effect to images. Only CG-based images are supported. /// /// > Only CG-based images are supported. /// /// > Important: On watchOS, there is no color control support and the original image will be returned. public struct BlackWhiteProcessor: ImageProcessor { public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor" /// Creates a ``BlackWhiteProcessor`` public init() {} public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7) .process(item: item, options: options) } } /// Processor for cropping an image. public struct CroppingImageProcessor: ImageProcessor { public let identifier: String /// The target size of the output image. public let size: CGSize /// Anchor point from which the output size should be calculate. /// /// The anchor point is consisted by two values between 0.0 and 1.0. /// It indicates a related point in current image. /// /// See ``CroppingImageProcessor/init(size:anchor:)`` for more. public let anchor: CGPoint /// Create a ``CroppingImageProcessor``. /// /// - Parameters: /// - size: The target size of the output image. /// - anchor: The anchor point from which the size should be calculated. Default is `CGPoint(x: 0.5, y: 0.5)`, /// which represents the center of the input image. /// /// The anchor point is composed of two values between 0.0 and 1.0. It indicates a relative point in the current /// image, e.g: /// - (0.0, 0.0) for the top-left corner /// - (0.5, 0.5) for the center /// - (1.0, 1.0) for the bottom-right corner /// /// The ``CroppingImageProcessor/size`` property will be used along with ``CroppingImageProcessor/anchor`` to /// calculate a target rectangle in the image size. /// /// > The target size will be automatically calculated with a reasonable behavior. For example, when you have an /// > image size of `CGSize(width: 100, height: 100)` and a target size of `CGSize(width: 20, height: 20)`: /// > /// > - with a (0.0, 0.0) anchor (top-left), the crop rect will be `{0, 0, 20, 20}`; /// > - with a (0.5, 0.5) anchor (center), it will be `{40, 40, 20, 20}`; /// > - while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}`. public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) { self.size = size self.anchor = anchor self.identifier = "com.onevcat.Kingfisher.CroppingImageProcessor(\(size)_\(anchor))" } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image.kf.scaled(to: options.scaleFactor) .kf.crop(to: size, anchorOn: anchor) case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) } } } /// Processor for downsampling an image. /// /// Compared to ``ResizingImageProcessor``, this processor does not render the images to resize. Instead, it /// downsamples the input data directly to an image. It is more efficient than ``ResizingImageProcessor``. /// /// > Tip: It is preferable to use ``DownsamplingImageProcessor`` whenever possible rather than the /// > ``ResizingImageProcessor``. /// /// > Important: Only CG-based images are supported. Animated images (such as GIFs) are not supported. public struct DownsamplingImageProcessor: ImageProcessor { /// The target size of 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. public let size: CGSize public let identifier: String /// Creates a `DownsamplingImageProcessor`. /// /// - Parameters: /// - size: The target size of the downsampling operation. /// /// > Important: The size 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. public init(size: CGSize) { self.size = size self.identifier = "com.onevcat.Kingfisher.DownsamplingImageProcessor(\(size))" } public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): guard let data = image.kf.data(format: .unknown) else { return nil } return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) case .data(let data): return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) } } } // This is an internal processor to provide the same interface for Live Photos. // It is not intended to be open and used from external. struct LivePhotoImageProcessor: ImageProcessor { public static let `default` = LivePhotoImageProcessor() private init() { } public let identifier = "com.onevcat.Kingfisher.LivePhotoImageProcessor" public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image case .data: return KFCrossPlatformImage() } } } infix operator |>: AdditionPrecedence /// Concatenates two `ImageProcessor`s to create a new one, in which the `left` and `right` are combined in order to /// process the image. /// /// - Parameters: /// - left: The first processor. /// - right: The second processor that follows the `left`. /// - Returns: The new processor that processes the image or the image data in left-to-right order. public func |>(left: any ImageProcessor, right: any ImageProcessor) -> any ImageProcessor { return left.append(another: right) } extension KFCrossPlatformColor { var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { var r: CGFloat = 0 var g: CGFloat = 0 var b: CGFloat = 0 var a: CGFloat = 0 #if os(macOS) (usingColorSpace(.extendedSRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a) #else getRed(&r, green: &g, blue: &b, alpha: &a) #endif return (r, g, b, a) } var rgbaDescription: String { let components = self.rgba return String(format: "(%.2f,%.2f,%.2f,%.2f)", components.r, components.g, components.b, components.a) } } ================================================ FILE: Sources/Image/ImageProgressive.swift ================================================ // // ImageProgressive.swift // Kingfisher // // Created by lixiang on 2019/5/10. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif private let sharedProcessingQueue: CallbackQueue = .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) /// Represents a progressive loading for images which supports this feature. public struct ImageProgressive: Sendable { /// The updating strategy when an intermediate progressive image is generated and about to be set to the hosting view. public enum UpdatingStrategy { /// Use the progressive image as it is. /// /// > It is the standard behavior when handling the progressive image. case `default` /// Discard this progressive image and keep the current displayed one. case keepCurrent /// Replace the image to a new one. /// /// If the progressive loading is initialized by a view extension in Kingfisher, the replacing image will be /// used to update the view. case replace(KFCrossPlatformImage?) } /// A default `ImageProgressive` could be used across. It blurs the progressive loading with the fastest /// scan enabled and scan interval as 0. @available(*, deprecated, message: "Getting a default `ImageProgressive` is deprecated due to its syntax semantic is not clear. Use `ImageProgressive.init` instead.", renamed: "init()") public static let `default` = ImageProgressive( isBlur: true, isFastestScan: true, scanInterval: 0 ) /// Indicates whether to enable blur effect processing. public var isBlur: Bool /// Indicates whether to enable the fastest scan. public var isFastestScan: Bool /// The minimum time interval for each scan. public var scanInterval: TimeInterval /// Called when an intermediate image is prepared and about to be set to the image view. /// /// If implemented, you should return an ``UpdatingStrategy`` value from this delegate. This value will be used to /// update the hosting view, if any. Otherwise, if there is no hosting view (i.e., the image retrieval is not /// happening from a view extension method), the returned ``UpdatingStrategy`` is ignored. public let onImageUpdated = Delegate() /// Creates an `ImageProgressive` value with default settings. /// /// It enables progressive loading with the fastest scan enabled and a scan interval of 0, resulting in a blurred /// effect. public init() { self.init(isBlur: true, isFastestScan: true, scanInterval: 0) } /// Creates an `ImageProgressive` value with the given values. /// /// - Parameters: /// - isBlur: Indicates whether to enable blur effect processing. /// - isFastestScan: Indicates whether to enable the fastest scan. /// - scanInterval: The minimum time interval for each scan. public init( isBlur: Bool, isFastestScan: Bool, scanInterval: TimeInterval ) { self.isBlur = isBlur self.isFastestScan = isFastestScan self.scanInterval = scanInterval } } // A data receiving provider to update the image. Working with an `ImageProgressive`, it helps to implement the image // progressive effect. final class ImageProgressiveProvider: DataReceivingSideEffect, @unchecked Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageProgressiveProviderPropertyQueue") private var _onShouldApply: () -> Bool = { return true } var onShouldApply: () -> Bool { get { propertyQueue.sync { _onShouldApply } } set { propertyQueue.sync { _onShouldApply = newValue } } } func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { DispatchQueue.main.async { guard self.onShouldApply() else { return } self.update(data: task.mutableData, with: task.callbacks) } } private let progressive: ImageProgressive private let refresh: (KFCrossPlatformImage) -> Void private let decoder: ImageProgressiveDecoder private let queue = ImageProgressiveSerialQueue() init?( options: KingfisherParsedOptionsInfo, refresh: @escaping (KFCrossPlatformImage) -> Void ) { guard let progressive = options.progressiveJPEG else { return nil } self.progressive = progressive self.refresh = refresh self.decoder = ImageProgressiveDecoder( progressive, processingQueue: options.processingQueue ?? sharedProcessingQueue, creatingOptions: options.imageCreatingOptions ) } func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) { guard !data.isEmpty else { return } queue.add(minimum: progressive.scanInterval) { completion in @Sendable func decode(_ data: Data) { self.decoder.decode(data, with: callbacks) { image in defer { completion() } guard self.onShouldApply() else { return } guard let image = image else { return } self.refresh(image) } } Task { @MainActor in let applyFlag = self.onShouldApply() guard applyFlag else { self.queue.clean() completion() return } if self.progressive.isFastestScan { decode(self.decoder.scanning(data) ?? Data()) } else { self.decoder.scanning(data).forEach { decode($0) } } } } } } private final class ImageProgressiveDecoder: @unchecked Sendable { private let option: ImageProgressive private let processingQueue: CallbackQueue private let creatingOptions: ImageCreatingOptions private(set) var scannedCount = 0 private(set) var scannedIndex = -1 init(_ option: ImageProgressive, processingQueue: CallbackQueue, creatingOptions: ImageCreatingOptions) { self.option = option self.processingQueue = processingQueue self.creatingOptions = creatingOptions } func scanning(_ data: Data) -> [Data] { guard data.kf.contains(jpeg: .SOF2) else { return [] } guard scannedIndex + 1 < data.count else { return [] } var datas: [Data] = [] var index = scannedIndex + 1 var count = scannedCount while index < data.count - 1 { scannedIndex = index // 0xFF, 0xDA - Start Of Scan let SOS = ImageFormat.JPEGMarker.SOS.bytes if data[index] == SOS[0], data[index + 1] == SOS[1] { if count > 0 { datas.append(data[0 ..< index]) } count += 1 } index += 1 } // Found more scans this the previous time guard count > scannedCount else { return [] } scannedCount = count // `> 1` checks that we've received a first scan (SOS) and then received // and also received a second scan (SOS). This way we know that we have // at least one full scan available. guard count > 1 else { return [] } return datas } func scanning(_ data: Data) -> Data? { guard data.kf.contains(jpeg: .SOF2) else { return nil } guard scannedIndex + 1 < data.count else { return nil } var index = scannedIndex + 1 var count = scannedCount var lastSOSIndex = 0 while index < data.count - 1 { scannedIndex = index // 0xFF, 0xDA - Start Of Scan let SOS = ImageFormat.JPEGMarker.SOS.bytes if data[index] == SOS[0], data[index + 1] == SOS[1] { lastSOSIndex = index count += 1 } index += 1 } // Found more scans this the previous time guard count > scannedCount else { return nil } scannedCount = count // `> 1` checks that we've received a first scan (SOS) and then received // and also received a second scan (SOS). This way we know that we have // at least one full scan available. guard count > 1 && lastSOSIndex > 0 else { return nil } return data[0 ..< lastSOSIndex] } func decode(_ data: Data, with callbacks: [SessionDataTask.TaskCallback], completion: @escaping @Sendable (KFCrossPlatformImage?) -> Void) { guard data.kf.contains(jpeg: .SOF2) else { CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } return } @Sendable func processing(_ data: Data) { let processor = ImageDataProcessor( data: data, callbacks: callbacks, processingQueue: processingQueue ) processor.onImageProcessed.delegate(on: self) { (self, result) in guard let image = try? result.0.get() else { CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } return } CallbackQueue.mainCurrentOrAsync.execute { completion(image) } } processor.process() } // Blur partial images. let count = scannedCount if option.isBlur, count < 6 { processingQueue.execute { // Progressively reduce blur as we load more scans. let image = KingfisherWrapper.image( data: data, options: self.creatingOptions ) let radius = max(2, 14 - count * 4) let temp = image?.kf.blurred(withRadius: CGFloat(radius)) processing(temp?.kf.data(format: .JPEG) ?? data) } } else { processing(data) } } } private final class ImageProgressiveSerialQueue: @unchecked Sendable { typealias ClosureCallback = @Sendable ((@escaping @Sendable () -> Void)) -> Void private let queue: DispatchQueue private var items: [DispatchWorkItem] = [] private var notify: (() -> Void)? private var lastTime: TimeInterval? init() { self.queue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue") } func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) { let completion = { @Sendable [weak self] in guard let self = self else { return } self.queue.async { [weak self] in guard let self = self else { return } guard !self.items.isEmpty else { return } self.items.removeFirst() if let next = self.items.first { self.queue.asyncAfter( deadline: .now() + interval, execute: next ) } else { self.lastTime = Date().timeIntervalSince1970 self.notify?() self.notify = nil } } } queue.async { [weak self] in guard let self = self else { return } let item = DispatchWorkItem { closure(completion) } if self.items.isEmpty { let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0) let delay = difference < interval ? interval - difference : 0 self.queue.asyncAfter(deadline: .now() + delay, execute: item) } self.items.append(item) } } func clean() { queue.async { [weak self] in guard let self = self else { return } self.items.forEach { $0.cancel() } self.items.removeAll() } } } ================================================ FILE: Sources/Image/ImageTransition.swift ================================================ // // ImageTransition.swift // Kingfisher // // Created by Wei Wang on 15/9/18. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation #if os(iOS) || os(tvOS) || os(visionOS) import UIKit /// Transition effect to be used when an image is downloaded and set using the `UIImageView` extension API in Kingfisher. /// /// You can assign an enum value with a transition duration as an item in `KingfisherOptionsInfo` to enable the animation /// transition. Apple's `UIViewAnimationOptions` are used under the hood. /// /// For custom transitions, you should specify your own transition options, animations, and completion handler as well. public enum ImageTransition: Sendable { /// No animation transition. case none /// Fade effect to the loaded image over a specified duration. case fade(TimeInterval) /// Flip from left transition. case flipFromLeft(TimeInterval) /// Flip from right transition. case flipFromRight(TimeInterval) /// Flip from top transition. case flipFromTop(TimeInterval) /// Flip from bottom transition. case flipFromBottom(TimeInterval) /// Custom transition defined by a general animation block. /// /// - Parameters: /// - duration: The duration of this custom transition. /// - options: The `UIView.AnimationOptions` to use in the transition. /// - animations: The animation block to apply when setting the image. /// - completion: A block called when the transition animation finishes. case custom(duration: TimeInterval, options: UIView.AnimationOptions, animations: (@Sendable @MainActor (UIImageView, UIImage) -> Void)?, completion: (@Sendable (Bool) -> Void)?) var duration: TimeInterval { switch self { case .none: return 0 case .fade(let duration): return duration case .flipFromLeft(let duration): return duration case .flipFromRight(let duration): return duration case .flipFromTop(let duration): return duration case .flipFromBottom(let duration): return duration case .custom(let duration, _, _, _): return duration } } var animationOptions: UIView.AnimationOptions { switch self { case .none: return [] case .fade: return .transitionCrossDissolve case .flipFromLeft: return .transitionFlipFromLeft case .flipFromRight: return .transitionFlipFromRight case .flipFromTop: return .transitionFlipFromTop case .flipFromBottom: return .transitionFlipFromBottom case .custom(_, let options, _, _): return options } } @MainActor var animations: ((UIImageView, UIImage) -> Void)? { switch self { case .custom(_, _, let animations, _): return animations default: return { $0.image = $1 } } } var completion: ((Bool) -> Void)? { switch self { case .custom(_, _, _, let completion): return completion default: return nil } } } #else // Just a placeholder for compiling on macOS. public enum ImageTransition: Sendable { case none /// This is a placeholder on macOS now. It is for SwiftUI (KFImage) to identify the fade option only. case fade(TimeInterval) } #endif ================================================ FILE: Sources/Image/Placeholder.swift ================================================ // // Placeholder.swift // Kingfisher // // Created by Tieme van Veen on 28/08/2017. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit #endif #if canImport(UIKit) import UIKit #endif /// Represents a placeholder type that could be set during loading as well as when loading is finished without /// getting an image. public protocol Placeholder { /// Called when the placeholder needs to be added to a given image view. /// /// To conform to ``Placeholder``, you implement this method and add your own placeholder view to the /// given `imageView`. /// /// - Parameter imageView: The image view where the placeholder should be added to. @MainActor func add(to imageView: KFCrossPlatformImageView) /// Called when the placeholder needs to be removed from a given image view. /// /// To conform to ``Placeholder``, you implement this method and remove your own placeholder view from the /// given `imageView`. /// /// - Parameter imageView: The image view where the placeholder is already added to and now should be removed from. @MainActor func remove(from imageView: KFCrossPlatformImageView) } @MainActor extension KFCrossPlatformImage: Placeholder { public func add(to imageView: KFCrossPlatformImageView) { imageView.image = self } public func remove(from imageView: KFCrossPlatformImageView) { imageView.image = nil } public func add(to base: any KingfisherHasImageComponent) { base.image = self } public func remove(from base: any KingfisherHasImageComponent) { base.image = nil } } /// Default implementation of an arbitrary view as a placeholder. The view will be /// added as a subview when adding and removed from its superview when removing. /// /// To use your customized View type as a placeholder, simply have it conform to /// `Placeholder` using an extension: `extension MyView: Placeholder {}`. @MainActor extension Placeholder where Self: KFCrossPlatformView { public func add(to imageView: KFCrossPlatformImageView) { imageView.addSubview(self) translatesAutoresizingMaskIntoConstraints = false centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true } public func remove(from imageView: KFCrossPlatformImageView) { removeFromSuperview() } } #endif ================================================ FILE: Sources/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 8.8.0 CFBundleSignature ???? CFBundleVersion 3260 NSPrincipalClass ================================================ FILE: Sources/Networking/AuthenticationChallengeResponsable.swift ================================================ // // AuthenticationChallengeResponsable.swift // Kingfisher // // Created by Wei Wang on 2018/10/11. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(*, deprecated, message: "Typo. Use `AuthenticationChallengeResponsible` instead", renamed: "AuthenticationChallengeResponsible") public typealias AuthenticationChallengeResponsable = AuthenticationChallengeResponsible /// Protocol indicates that an authentication challenge could be handled. public protocol AuthenticationChallengeResponsible: AnyObject { /// Called when a session level authentication challenge is received. /// /// This method provides a chance to handle and respond to the authentication challenge before the downloading can /// start. /// /// - Parameters: /// - downloader: The downloader that receives this challenge. /// - challenge: An object that contains the request for authentication. /// - Returns: The challenge disposition on how the challenge should be handled, and the credential if the /// disposition is `.useCredential`. /// /// > This method is a forward from `URLSessionDelegate.urlSession(_:didReceive:completionHandler:)`. /// > Please refer to the documentation of it in `URLSessionDelegate`. func downloader( _ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) /// Called when a task level authentication challenge is received. /// /// This method provides a chance to handle and respond to the authentication challenge before the downloading can /// start. /// /// - Parameters: /// - downloader: The downloader that receives this challenge. /// - task: The task whose request requires authentication. /// - challenge: An object that contains the request for authentication. /// - Returns: The challenge disposition on how the challenge should be handled, and the credential if the /// disposition is `.useCredential`. /// /// > This method is a forward from `URLSessionDataDelegate.urlSession(_:dataTask:didReceive:completionHandler:)`. /// > Please refer to the documentation of it in `URLSessionDataDelegate`. func downloader( _ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) } extension AuthenticationChallengeResponsible { public func downloader( _ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) { let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!) return (.useCredential, credential) } } return (.performDefaultHandling, nil) } public func downloader( _ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { (.performDefaultHandling, nil) } } ================================================ FILE: Sources/Networking/ImageDataProcessor.swift ================================================ // // ImageDataProcessor.swift // Kingfisher // // Created by Wei Wang on 2018/10/11. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation private let sharedProcessingQueue: CallbackQueue = .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) // Handles image processing work on an own process queue. final class ImageDataProcessor: Sendable { let data: Data let callbacks: [SessionDataTask.TaskCallback] let queue: CallbackQueue // Note: We have an optimization choice there, to reduce queue dispatch by checking callback // queue settings in each option... let onImageProcessed = Delegate<(Result, SessionDataTask.TaskCallback), Void>() init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) { self.data = data self.callbacks = callbacks self.queue = processingQueue ?? sharedProcessingQueue } func process() { queue.execute { self.doProcess() } } private func doProcess() { var processedImages = [String: KFCrossPlatformImage]() for callback in callbacks { let processor = callback.options.processor var image = processedImages[processor.identifier] if image == nil { image = processor.process(item: .data(data), options: callback.options) processedImages[processor.identifier] = image } let result: Result if let image = image { let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image result = .success(finalImage) } else { let error = KingfisherError.processorError( reason: .processingFailed(processor: processor, item: .data(data))) result = .failure(error) } onImageProcessed.call((result, callback)) } } } ================================================ FILE: Sources/Networking/ImageDownloader+LivePhoto.swift ================================================ // // ImageDownloader+LivePhoto.swift // Kingfisher // // Created by onevcat on 2024/10/01. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(macOS) import AppKit #else import UIKit #endif public struct LivePhotoResourceDownloadingResult: Sendable { /// The original URL of the image request. public let url: URL? /// The raw data received from the downloader. public let originalData: Data /// Creates an `ImageDownloadResult` object. /// /// - Parameters: /// - url: The URL from which the image was downloaded. /// - originalData: The binary data of the image. public init(originalData: Data, url: URL? = nil) { self.url = url self.originalData = originalData } } extension ImageDownloader { public func downloadLivePhotoResource( with url: URL, options: KingfisherParsedOptionsInfo ) async throws -> LivePhotoResourceDownloadingResult { let task = CancellationDownloadTask() return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in let downloadTask = downloadLivePhotoResource(with: url, options: options) { result in continuation.resume(with: result) } if Task.isCancelled { downloadTask.cancel() } else { Task { await task.setTask(downloadTask) } } } } onCancel: { Task { await task.task?.cancel() } } } @discardableResult public func downloadLivePhotoResource( with url: URL, options: KingfisherParsedOptionsInfo, completionHandler: (@Sendable (Result) -> Void)? = nil ) -> DownloadTask { var checkedOptions = options if options.processor == DefaultImageProcessor.default { // The default processor is a default behavior so we replace it silently. checkedOptions.processor = LivePhotoImageProcessor.default } else if options.processor != LivePhotoImageProcessor.default { assertionFailure("[Kingfisher] Using of custom processors during loading of live photo resource is not supported.") checkedOptions.processor = LivePhotoImageProcessor.default } return downloadImage(with: url, options: checkedOptions) { result in guard let completionHandler else { return } let newResult = result.map { LivePhotoResourceDownloadingResult(originalData: $0.originalData, url: $0.url) } completionHandler(newResult) } } } ================================================ FILE: Sources/Networking/ImageDownloader.swift ================================================ // // ImageDownloader.swift // Kingfisher // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(macOS) import AppKit #else import UIKit #endif typealias DownloadResult = Result /// Represents a successful result of an image downloading process. public struct ImageLoadingResult: Sendable { /// The downloaded image. public let image: KFCrossPlatformImage /// The original URL of the image request. public let url: URL? /// The raw data received from the downloader. public let originalData: Data /// The network metrics collected during the download process. public let metrics: NetworkMetrics? /// Creates an `ImageDownloadResult` object. /// /// - Parameters: /// - image: The image of the download result. /// - url: The URL from which the image was downloaded. /// - originalData: The binary data of the image. /// - metrics: The network metrics collected during the download. public init(image: KFCrossPlatformImage, url: URL? = nil, originalData: Data, metrics: NetworkMetrics? = nil) { self.image = image self.url = url self.originalData = originalData self.metrics = metrics } } /// Represents a task in the image downloading process. /// /// When a download starts in Kingfisher, the involved methods always return you an instance of ``DownloadTask``. If you /// need to cancel the task during the download process, you can keep a reference to the instance and call ``cancel()`` /// on it. public final class DownloadTask: @unchecked Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.DownloadTaskPropertyQueue") init(sessionTask: SessionDataTask, cancelToken: SessionDataTask.CancelToken) { _sessionTask = sessionTask _cancelToken = cancelToken } init() { } private var _sessionTask: SessionDataTask? = nil /// The ``SessionDataTask`` object associated with this download task. Multiple `DownloadTask`s could refer to the /// same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading tasks for the same /// URL resource simultaneously. /// /// When you call ``DownloadTask/cancel()``, this ``SessionDataTask`` and its cancellation token will be passed /// along. You can use them to identify the cancelled task. public private(set) var sessionTask: SessionDataTask? { get { propertyQueue.sync { _sessionTask } } set { propertyQueue.sync { _sessionTask = newValue } } } private var _cancelToken: SessionDataTask.CancelToken? = nil /// The cancellation token used to cancel the task. /// /// This is solely for identifying the task when it is cancelled. To cancel a ``DownloadTask``, call /// ``DownloadTask/cancelToken``. public private(set) var cancelToken: SessionDataTask.CancelToken? { get { propertyQueue.sync { _cancelToken } } set { propertyQueue.sync { _cancelToken = newValue } } } /// Cancel this single download task if it is running. /// /// This method will do nothing if this task is not running. /// /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is currently /// being downloaded. However, even when internally no new session task is created, a ``DownloadTask`` will still /// be created and returned when you call related methods. It will share the session downloading task with a /// previous task. /// /// In this case, if multiple ``DownloadTask``s share a single session download task, calling this method /// does not cancel the actual download process, since there are other `DownloadTask`s need it. It only removes /// `self` from the download list. /// /// > Tip: If you need to cancel all on-going ``DownloadTask``s of a certain URL, use /// ``ImageDownloader/cancel(url:)``. If you need to cancel all downloading tasks of an ``ImageDownloader``, /// use ``ImageDownloader/cancelAll()``. public func cancel() { guard let sessionTask, let cancelToken else { return } sessionTask.cancel(token: cancelToken) } public var isInitialized: Bool { propertyQueue.sync { _sessionTask != nil && _cancelToken != nil } } func linkToTask(_ task: DownloadTask) { self.sessionTask = task.sessionTask self.cancelToken = task.cancelToken } } actor CancellationDownloadTask { var task: DownloadTask? func setTask(_ task: DownloadTask?) { self.task = task } } extension DownloadTask { enum WrappedTask { case download(DownloadTask) case dataProviding func cancel() { switch self { case .download(let task): task.cancel() case .dataProviding: break } } var value: DownloadTask? { switch self { case .download(let task): return task case .dataProviding: return nil } } } } /// Represents a download manager for requesting an image with a URL from the server. open class ImageDownloader: @unchecked Sendable { // MARK: Singleton /// The default downloader. public static let `default` = ImageDownloader(name: "default") private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloaderPropertyQueue") // MARK: Public Properties private var _downloadTimeout: TimeInterval = 15.0 /// The duration before the download times out. /// /// If the download does not complete before this duration, the URL session will raise a timeout error, which /// Kingfisher wraps and forwards as a ``KingfisherError/ResponseErrorReason/URLSessionError(error:)``. /// /// The default timeout is set to 15 seconds. open var downloadTimeout: TimeInterval { get { propertyQueue.sync { _downloadTimeout } } set { propertyQueue.sync { _downloadTimeout = newValue } } } /// A set of trusted hosts when receiving server trust challenges. /// /// A challenge with host name contained in this set will be ignored. You can use this set to specify the /// self-signed site. It only will be used if you don't specify the /// ``ImageDownloader/authenticationChallengeResponder``. /// /// > If ``ImageDownloader/authenticationChallengeResponder`` is set, this property will be ignored and the /// implementation of ``ImageDownloader/authenticationChallengeResponder`` will be used instead. open var trustedHosts: Set? /// Use this to supply a configuration for the downloader. /// /// By default, `URLSessionConfiguration.ephemeral` will be used. /// /// You can modify the configuration before a downloading task begins. A configuration without persistent storage /// for caches is necessary for the downloader to function correctly. /// /// > Setting a new session delegate to the downloader will invalidate the existing session and create a new one /// > with the new value and the ``sessionDelegate``. open var sessionConfiguration = URLSessionConfiguration.ephemeral { didSet { session.invalidateAndCancel() session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) } } /// The session delegate which is used to handle the session related tasks. /// /// > Setting a new session delegate to the downloader will invalidate the existing session and create a new one /// > with the new value and the ``sessionConfiguration``. open var sessionDelegate: SessionDelegate { didSet { session.invalidateAndCancel() session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) setupSessionHandler() } } /// Whether the download requests should use pipeline or not. /// /// It sets the `httpShouldUsePipelining` of the `URLRequest` for the download task. Default is false. open var requestsUsePipelining = false /// The delegate of this `ImageDownloader` object. /// /// See the ``ImageDownloaderDelegate`` protocol for more information. open weak var delegate: (any ImageDownloaderDelegate)? /// A responder for authentication challenges. /// /// The downloader forwards the received authentication challenge for the downloading session to this responder. /// See ``AuthenticationChallengeResponsible`` for more. open weak var authenticationChallengeResponder: (any AuthenticationChallengeResponsible)? // The downloader name. private let name: String // The session bound to the downloader. private var session: URLSession private let lock = NSLock() // MARK: Initializers /// Creates a downloader with a given name. /// /// - Parameter name: The name for the downloader. It should not be empty. public init(name: String) { if name.isEmpty { fatalError("[Kingfisher] You should specify a name for the downloader. " + "A downloader with empty name is not permitted.") } self.name = name sessionDelegate = SessionDelegate() session = URLSession( configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) authenticationChallengeResponder = self setupSessionHandler() } deinit { session.invalidateAndCancel() } private func setupSessionHandler() { sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in await (self.authenticationChallengeResponder ?? self).downloader(self, didReceive: invoke.1) } sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in await (self.authenticationChallengeResponder ?? self).downloader(self, task: invoke.1, didReceive: invoke.2) } sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in (self.delegate ?? self).isValidStatusCode(code, for: self) } sessionDelegate.onResponseReceived.delegate(on: self) { (self, response) in await (self.delegate ?? self).imageDownloader(self, didReceive: response) } sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in let (url, result) = value do { let value = try result.get() self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil) } catch { self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error) } } sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task) } } // Wraps `completionHandler` to `onCompleted` respectively. private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate? { completionHandler.map { block -> Delegate in let delegate = Delegate, Void>() delegate.delegate(on: self) { (self, callback) in block(callback) } return delegate } } private func createTaskCallback( _ completionHandler: ((DownloadResult) -> Void)?, options: KingfisherParsedOptionsInfo ) -> SessionDataTask.TaskCallback { SessionDataTask.TaskCallback( onCompleted: createCompletionCallBack(completionHandler), options: options ) } private func createDownloadContext( with url: URL, options: KingfisherParsedOptionsInfo, done: @escaping (@Sendable (Result) -> Void) ) { @Sendable func checkRequestAndDone(r: URLRequest) { // There is a possibility that request modifier changed the url to `nil` or empty. // In this case, throw an error. guard let url = r.url, !url.absoluteString.isEmpty else { done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r)))) return } done(.success(DownloadingContext(url: url, request: r, options: options))) } // Creates default request. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout) request.httpShouldUsePipelining = requestsUsePipelining if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil { request.allowsConstrainedNetworkAccess = false } guard let requestModifier = options.requestModifier else { checkRequestAndDone(r: request) return } // Modifies request before sending. // FIXME: A temporary solution for keep the sync `ImageDownloadRequestModifier` behavior as before. // We should be able to combine two cases once the full async support can be introduced to Kingfisher. if let m = requestModifier as? any ImageDownloadRequestModifier { guard let result = m.modified(for: request) else { done(.failure(KingfisherError.requestError(reason: .emptyRequest))) return } checkRequestAndDone(r: result) } else { Task { [request] in guard let result = await requestModifier.modified(for: request) else { done(.failure(KingfisherError.requestError(reason: .emptyRequest))) return } checkRequestAndDone(r: result) } } } private func addDownloadTask( context: DownloadingContext, callback: SessionDataTask.TaskCallback ) -> DownloadTask { lock.lock() defer { lock.unlock() } // Ready to start download. Add it to session task manager (`sessionHandler`) let downloadTask: DownloadTask if let existingTask = sessionDelegate.task(for: context.url) { downloadTask = sessionDelegate.append(existingTask, callback: callback) } else { let sessionDataTask = session.dataTask(with: context.request) sessionDataTask.priority = context.options.downloadPriority downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback) } return downloadTask } private func reportWillDownloadImage(url: URL, request: URLRequest) { delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) } private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) { var response: URLResponse? var err: (any Error)? do { response = try result.get().1 } catch { err = error } self.delegate?.imageDownloader( self, didFinishDownloadingImageForURL: url, with: response, error: err ) } private func reportDidProcessImage( result: Result, url: URL, response: URLResponse? ) { if let image = try? result.get() { self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response) } } private func startDownloadTask( context: DownloadingContext, callback: SessionDataTask.TaskCallback, beforeTaskResume: ((DownloadTask) -> Void)? = nil ) -> DownloadTask { let downloadTask = addDownloadTask(context: context, callback: callback) guard let sessionTask = downloadTask.sessionTask, !sessionTask.started else { beforeTaskResume?(downloadTask) return downloadTask } sessionTask.onTaskDone.delegate(on: self) { [weak sessionTask] (self, done) in // Underlying downloading finishes. // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback] let (result, callbacks) = done // Before processing the downloaded data. self.reportDidDownloadImageData(result: result, url: context.url) switch result { // Download finished. Now process the data to an image. case .success(let (data, response)): let processor = ImageDataProcessor( data: data, callbacks: callbacks, processingQueue: context.options.processingQueue ) processor.onImageProcessed.delegate(on: self) { (self, done) in // `onImageProcessed` will be called for `callbacks.count` times, with each // `SessionDataTask.TaskCallback` as the input parameter. // result: Result, callback: SessionDataTask.TaskCallback let (result, callback) = done self.reportDidProcessImage(result: result, url: context.url, response: response) let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data, metrics: sessionTask?.metrics) } let queue = callback.options.callbackQueue queue.execute { callback.onCompleted?.call(imageResult) } } processor.process() case .failure(let error): callbacks.forEach { callback in let queue = callback.options.callbackQueue queue.execute { callback.onCompleted?.call(.failure(error)) } } } } // Ensure `beforeTaskResume` runs before `resume()`. Some stubbing layers may complete the request // synchronously during `resume()`, so any "task started" callback should be invoked before that. beforeTaskResume?(downloadTask) reportWillDownloadImage(url: context.url, request: context.request) sessionTask.resume() return downloadTask } // MARK: Downloading Task /// Downloads an image with a URL and options. /// /// - Parameters: /// - url: The target URL. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter. /// /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task. @discardableResult open func downloadImage( with url: URL, options: KingfisherParsedOptionsInfo, completionHandler: (@Sendable (Result) -> Void)? = nil) -> DownloadTask { let downloadTask = DownloadTask() createDownloadContext(with: url, options: options) { result in switch result { case .success(let context): let taskCallback = self.createTaskCallback(completionHandler, options: options) if let modifier = options.requestModifier { _ = self.startDownloadTask(context: context, callback: taskCallback, beforeTaskResume: { actualDownloadTask in downloadTask.linkToTask(actualDownloadTask) modifier.onDownloadTaskStarted?(downloadTask) }) } else { let actualDownloadTask = self.startDownloadTask(context: context, callback: taskCallback) downloadTask.linkToTask(actualDownloadTask) } case .failure(let error): options.callbackQueue.execute { completionHandler?(.failure(error)) } } } return downloadTask } /// Downloads an image with a URL and options. /// /// - Parameters: /// - url: The target URL. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. /// - progressBlock: Called when the download progress is updated. This block will always be called on the main /// queue. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter. /// /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task. @discardableResult open func downloadImage( with url: URL, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@Sendable (Result) -> Void)? = nil) -> DownloadTask { var info = KingfisherParsedOptionsInfo(options) if let block = progressBlock { info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } return downloadImage( with: url, options: info, completionHandler: completionHandler) } /// Downloads an image with a URL and options. /// /// - Parameters: /// - url: The target URL. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter. /// /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task. @discardableResult open func downloadImage( with url: URL, options: KingfisherOptionsInfo? = nil, completionHandler: (@Sendable (Result) -> Void)? = nil) -> DownloadTask { downloadImage( with: url, options: KingfisherParsedOptionsInfo(options), completionHandler: completionHandler ) } } // Concurrency extension ImageDownloader { /// Downloads an image with a URL and option. /// /// - Parameters: /// - url: Target URL. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. /// - Returns: The image loading result. /// /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in. public func downloadImage( with url: URL, options: KingfisherParsedOptionsInfo ) async throws -> ImageLoadingResult { let task = CancellationDownloadTask() return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in let downloadTask = downloadImage(with: url, options: options) { result in continuation.resume(with: result) } if Task.isCancelled { downloadTask.cancel() } else { Task { await task.setTask(downloadTask) } } } } onCancel: { Task { await task.task?.cancel() } } } /// Downloads an image with a URL and option. /// /// - Parameters: /// - url: Target URL. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue. /// - Returns: The image loading result. /// /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in. public func downloadImage( with url: URL, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil ) async throws -> ImageLoadingResult { var info = KingfisherParsedOptionsInfo(options) if let block = progressBlock { info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } return try await downloadImage(with: url, options: info) } /// Downloads an image with a URL and option. /// /// - Parameters: /// - url: Target URL. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. /// - Returns: The image loading result. /// /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in. public func downloadImage( with url: URL, options: KingfisherOptionsInfo? = nil ) async throws -> ImageLoadingResult { try await downloadImage(with: url, options: KingfisherParsedOptionsInfo(options)) } } // MARK: Cancelling Task extension ImageDownloader { /// Cancel all downloading tasks for this ``ImageDownloader``. /// /// It will trigger the completion handlers for all not-yet-finished downloading tasks with a cancellation error. /// /// If you need to only cancel a certain task, call ``DownloadTask/cancel()`` on the task returned by the /// downloading methods. If you need to cancel all ``DownloadTask``s of a certain URL, use /// ``ImageDownloader/cancel(url:)``. public func cancelAll() { sessionDelegate.cancelAll() } /// Cancel all downloading tasks for a given URL. /// /// It will trigger the completion handlers for all not-yet-finished downloading tasks for the URL with a /// cancellation error. /// /// - Parameter url: The URL for which you want to cancel downloading. public func cancel(url: URL) { sessionDelegate.cancel(url: url) } } // Use the default implementation from extension of `AuthenticationChallengeResponsible`. extension ImageDownloader: AuthenticationChallengeResponsible {} // Use the default implementation from extension of `ImageDownloaderDelegate`. extension ImageDownloader: ImageDownloaderDelegate {} extension ImageDownloader { struct DownloadingContext { let url: URL let request: URLRequest let options: KingfisherParsedOptionsInfo } } ================================================ FILE: Sources/Networking/ImageDownloaderDelegate.swift ================================================ // // ImageDownloaderDelegate.swift // Kingfisher // // Created by Wei Wang on 2018/10/11. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation #if os(macOS) import AppKit #else import UIKit #endif /// Protocol for handling events for ``ImageDownloader``. /// /// This delegate protocol provides a set of methods related to the stages and rules of the image downloader. You use /// the provided methods to inspect the downloader working phases or respond to some events to make decisions. public protocol ImageDownloaderDelegate: AnyObject { /// Called when the ``ImageDownloader`` object is about to start downloading an image from a specified URL. /// /// - Parameters: /// - downloader: The ``ImageDownloader`` object used for the downloading operation. /// - url: The URL of the starting request. /// - request: The request object for the download process. func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) /// Called when the ``ImageDownloader`` completes a downloading request with success or failure. /// /// - Parameters: /// - downloader: The ``ImageDownloader`` object used for the downloading operation. /// - url: The URL of the original request. /// - response: The response object of the downloading process. /// - error: The error in case of failure. func imageDownloader( _ downloader: ImageDownloader, didFinishDownloadingImageForURL url: URL, with response: URLResponse?, error: (any Error)?) /// Called when the ``ImageDownloader`` object successfully downloads image data with a specified task. /// /// This is your last chance to verify or modify the downloaded data before Kingfisher attempts to perform /// additional processing on the image data. /// /// - Parameters: /// - downloader: The ``ImageDownloader`` object used for the downloading operation. /// - data: The original downloaded data. /// - task: The data task containing request and response information for the download. /// - Returns: The data that Kingfisher should use to create an image. You need to provide valid data that is in /// one of the supported image file formats. Kingfisher will process this data and attempt to convert it into an /// image object. func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? /// Called when the ``ImageDownloader`` object successfully downloads image data from a specified URL. /// /// This is your last chance to verify or modify the downloaded data before Kingfisher attempts to perform /// additional processing on the image data. /// /// - Parameters: /// - downloader: The ``ImageDownloader`` object used for the downloading operation. /// - data: The original downloaded data. /// - url: The URL of the original request. /// /// - Returns: The data that Kingfisher should use to create an image. You need to provide valid data that is in /// one of the supported image file formats. Kingfisher will process this data and attempt to convert it into an /// image object. /// /// This method can be used to preprocess raw image data before the creation of the `Image` instance (e.g., /// decrypting or verification). If `nil` is returned, the processing is interrupted and a /// ``KingfisherError/ResponseErrorReason/dataModifyingFailed(task:)`` error will be raised. You can use this fact /// to stop the image processing flow if you find that the data is corrupted or malformed. /// /// > If the ``SessionDataTask`` version of `imageDownloader(_:didDownload:with:)` is implemented, this method will /// > not be called anymore. func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? /// Called when the ``ImageDownloader`` object successfully downloads and processes an image from a specified URL. /// /// - Parameters: /// - downloader: The ``ImageDownloader`` object used for the downloading operation. /// - image: The downloaded and processed image. /// - url: The URL of the original request. /// - response: The original response object of the downloading process. func imageDownloader( _ downloader: ImageDownloader, didDownload image: KFCrossPlatformImage, for url: URL, with response: URLResponse?) /// Checks if a received HTTP status code is valid or not. /// /// By default, a status code in the range `200..<400` is considered as valid. If an invalid code is received, /// the downloader will raise a ``KingfisherError/ResponseErrorReason/invalidHTTPStatusCode(response:)`` error. /// /// - Parameters: /// - code: The received HTTP status code. /// - downloader: The ``ImageDownloader`` object requesting validation of the status code. /// - Returns: A value indicating whether this HTTP status code is valid or not. /// /// > If the default range of `200..<400` as valid codes does not suit your needs, you can implement this method to /// change that behavior. func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool /// Called when the task has received a valid HTTP response after passing other checks such as the status code. /// You can perform additional checks or verifications on the response to determine if the download should be /// allowed or cancelled. /// /// For example, this is useful if you want to verify some header values in the response before actually starting /// the download. /// /// If implemented, you have to return a proper response disposition, such as `.allow` to start the actual /// downloading or `.cancel` to cancel the task. If `.cancel` is used as the disposition, the downloader will raise /// a ``KingfisherError/ResponseErrorReason/cancelledByDelegate(response:)`` error. If not implemented, any response /// that passes other checks will be allowed, and the download will start. /// /// - Parameters: /// - downloader: The `ImageDownloader` object used for the downloading operation. /// - response: The original response object of the downloading process. /// /// - Returns: The disposition for the download task. You have to return either `.allow` or `.cancel`. func imageDownloader( _ downloader: ImageDownloader, didReceive response: URLResponse ) async -> URLSession.ResponseDisposition } // Default implementation for `ImageDownloaderDelegate`. extension ImageDownloaderDelegate { public func imageDownloader( _ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) {} public func imageDownloader( _ downloader: ImageDownloader, didFinishDownloadingImageForURL url: URL, with response: URLResponse?, error: (any Error)?) {} public func imageDownloader( _ downloader: ImageDownloader, didDownload image: KFCrossPlatformImage, for url: URL, with response: URLResponse?) {} public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool { return (200..<400).contains(code) } public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? { guard let url = task.originalURL else { return data } return imageDownloader(downloader, didDownload: data, for: url) } public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? { return data } public func imageDownloader( _ downloader: ImageDownloader, didReceive response: URLResponse ) async -> URLSession.ResponseDisposition { .allow } } ================================================ FILE: Sources/Networking/ImageModifier.swift ================================================ // // ImageModifier.swift // Kingfisher // // Created by Ethan Gill on 2017/11/28. // // Copyright (c) 2019 Ethan Gill // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(macOS) import AppKit #else import UIKit #endif /// An ``ImageModifier`` can be used to change properties on an image between cache serialization and the actual use of /// the image. /// /// The ``ImageModifier/modify(_:)`` method will be called after the image is retrieved from its source and before it /// is returned to the caller. This modified image is expected to be used only for rendering purposes; any changes /// applied by the ``ImageModifier`` will not be serialized or cached. public protocol ImageModifier: Sendable { /// Modify an input `Image`. /// /// - Parameter image: The image which will be modified by `self`. /// /// - Returns: The modified image. /// /// > Important: The return value will be unmodified if modification is not possible on the current platform. func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage } /// A wrapper that simplifies the creation of an ``ImageModifier``. /// /// This type conforms to ``ImageModifier`` and encapsulates an image modification block. If the `block` throws an /// error, the original image will be used. public struct AnyImageModifier: ImageModifier { /// A block that modifies images, or returns the original image if modification cannot be performed, along with an /// error. let block: @Sendable (KFCrossPlatformImage) throws -> KFCrossPlatformImage /// Creates an ``AnyImageModifier`` with a given `modify` block. /// - Parameter modify: A block which is used to modify the input image. public init(modify: @escaping @Sendable (KFCrossPlatformImage) throws -> KFCrossPlatformImage) { block = modify } public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { return (try? block(image)) ?? image } } #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) import UIKit /// Modifier for setting the rendering mode of images. public struct RenderingModeImageModifier: ImageModifier { /// The rendering mode to apply to the image. public let renderingMode: UIImage.RenderingMode /// Creates a ``RenderingModeImageModifier``. /// /// - Parameter renderingMode: The rendering mode to apply to the image. The default is `.automatic`. public init(renderingMode: UIImage.RenderingMode = .automatic) { self.renderingMode = renderingMode } public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { return image.withRenderingMode(renderingMode) } } /// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images. public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier { /// Creates a ``FlipsForRightToLeftLayoutDirectionImageModifier``. public init() {} public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { return image.imageFlippedForRightToLeftLayoutDirection() } } /// Modifier for setting the `alignmentRectInsets` property of images. public struct AlignmentRectInsetsImageModifier: ImageModifier { /// The alignment insets to apply to the image. public let alignmentInsets: UIEdgeInsets /// Creates a ``AlignmentRectInsetsImageModifier``. /// - Parameter alignmentInsets: The alignment insets to apply to the image. public init(alignmentInsets: UIEdgeInsets) { self.alignmentInsets = alignmentInsets } public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { return image.withAlignmentRectInsets(alignmentInsets) } } #endif ================================================ FILE: Sources/Networking/ImagePrefetcher.swift ================================================ // // ImagePrefetcher.swift // Kingfisher // // Created by Claire Knight on 24/02/2016 // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if os(macOS) import AppKit #else import UIKit #endif /// Progress update block of prefetcher when initialized with a list of resources. /// /// - Parameters: /// - skippedResources: An array of resources that are already cached before the prefetching begins. /// - 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. /// - completedResources: An array of resources that are downloaded and cached successfully. public typealias PrefetcherProgressBlock = @Sendable (_ skippedResources: [any Resource], _ failedResources: [any Resource], _ completedResources: [any Resource]) -> Void /// Progress update block of prefetcher when initialized with a list of resources. /// /// - Parameters: /// - skippedSources: An array of sources that are already cached before the prefetching begins. /// - failedSources: An array of sources that fail to be fetched. /// - completedResources: An array of sources that are fetched and cached successfully. public typealias PrefetcherSourceProgressBlock = @Sendable (_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void /// Completion block of prefetcher when initialized with a list of sources. /// /// - Parameters: /// - skippedResources: An array of resources that are already cached before the prefetching begins. /// - 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. /// - completedResources: An array of resources that are downloaded and cached successfully. public typealias PrefetcherCompletionHandler = @Sendable (_ skippedResources: [any Resource], _ failedResources: [any Resource], _ completedResources: [any Resource]) -> Void /// Completion block of prefetcher when initialized with a list of sources. /// /// - Parameters: /// - skippedSources: An array of sources that are already cached before the prefetching begins. /// - failedSources: An array of sources that fail to be fetched. /// - completedSources: An array of sources that are fetched and cached successfully. public typealias PrefetcherSourceCompletionHandler = @Sendable (_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void /// ``ImagePrefetcher`` represents a downloading manager for requesting many images via URLs and then caching them. /// /// Use this class when you know a list of image resources and want to download them before showing. It also works with /// some Cocoa prefetching mechanisms like table view or collection view `prefetchDataSource` to start image downloading /// and caching before they are displayed on screen. public class ImagePrefetcher: CustomStringConvertible, @unchecked Sendable { public var description: String { return "\(Unmanaged.passUnretained(self).toOpaque())" } /// The maximum concurrent downloads to use when prefetching images. /// /// The default is 5. public var maxConcurrentDownloads = 5 private let prefetchSources: [Source] private let optionsInfo: KingfisherParsedOptionsInfo private var progressBlock: PrefetcherProgressBlock? private var completionHandler: PrefetcherCompletionHandler? private var progressSourceBlock: PrefetcherSourceProgressBlock? private var completionSourceHandler: PrefetcherSourceCompletionHandler? private var tasks = [String: DownloadTask.WrappedTask]() private var pendingSources: ArraySlice private var skippedSources = [Source]() private var completedSources = [Source]() private var failedSources = [Source]() private var stopped = false // A manager used for prefetching. We will use the helper methods in manager. private let manager: KingfisherManager private let prefetchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.prefetchQueue") private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue") private var finished: Bool { let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count return totalFinished == prefetchSources.count && tasks.isEmpty } /// Creates an image prefetcher with an array of URLs. /// /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the /// prefetching process. The images that are already cached will be skipped without being downloaded again. /// /// - Parameters: /// - urls: The URLs to be prefetched. /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information. /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled. /// - completionHandler: Called when the whole prefetching process is finished. /// /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache /// targets, respectively. You can specify other downloaders or caches by using a customized /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method. public convenience init( urls: [URL], options: KingfisherOptionsInfo? = nil, progressBlock: PrefetcherProgressBlock? = nil, completionHandler: PrefetcherCompletionHandler? = nil) { let resources: [any Resource] = urls.map { $0 } self.init( resources: resources, options: options, progressBlock: progressBlock, completionHandler: completionHandler) } /// Creates an image prefetcher with an array of ``Resource``s. /// /// The prefetcher should be initiated with a list of prefetching targets. The resource list is immutable. /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the /// prefetching process. The images that are already cached will be skipped without being downloaded again. /// /// - Parameters: /// - resources: An array of resource to be prefetched. See ``ImageResource``. /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information. /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled. /// - completionHandler: Called when the whole prefetching process is finished. /// /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache /// targets, respectively. You can specify other downloaders or caches by using a customized /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method. public convenience init( resources: [any Resource], options: KingfisherOptionsInfo? = nil, progressBlock: PrefetcherProgressBlock? = nil, completionHandler: PrefetcherCompletionHandler? = nil) { self.init(sources: resources.map { $0.convertToSource() }, options: options) self.progressBlock = progressBlock self.completionHandler = completionHandler } /// Creates an image prefetcher with an array of ``Source``s. /// /// The prefetcher should be initiated with a list of prefetching targets. The source list is immutable. /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the /// prefetching process. The images that are already cached will be skipped without being downloaded again. /// /// - Parameters: /// - sources: An array of resource to be prefetched. See ``Source``. /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information. /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled. /// - completionHandler: Called when the whole prefetching process is finished. /// /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache /// targets, respectively. You can specify other downloaders or caches by using a customized /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method. public convenience init(sources: [Source], options: KingfisherOptionsInfo? = nil, progressBlock: PrefetcherSourceProgressBlock? = nil, completionHandler: PrefetcherSourceCompletionHandler? = nil) { self.init(sources: sources, options: options) self.progressSourceBlock = progressBlock self.completionSourceHandler = completionHandler } init(sources: [Source], options: KingfisherOptionsInfo?) { var options = KingfisherParsedOptionsInfo(options) prefetchSources = sources pendingSources = ArraySlice(sources) // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options. // Add our own callback dispatch queue to make sure all internal callbacks are // coming back in our expected queue. options.callbackQueue = .dispatch(prefetchQueue) optionsInfo = options let cache = optionsInfo.targetCache ?? .default let downloader = optionsInfo.downloader ?? .default manager = KingfisherManager(downloader: downloader, cache: cache) } /// Starts downloading the resources and caching them. /// /// This can be useful for the background downloading of assets that are required for later use in an app. This /// code will not try to update any UI with the results of the process. public func start() { prefetchQueue.async { guard !self.stopped else { assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.") self.handleComplete() return } guard self.maxConcurrentDownloads > 0 else { assertionFailure("There should be concurrent downloads value should be at least 1.") self.handleComplete() return } // Empty case. guard self.prefetchSources.count > 0 else { self.handleComplete() return } let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads) for _ in 0 ..< initialConcurrentDownloads { if let resource = self.pendingSources.popFirst() { self.startPrefetching(resource) } } } } /// Stops the current downloading progress and cancels any future prefetching activity that might be occurring. public func stop() { prefetchQueue.async { if self.finished { return } self.stopped = true self.tasks.values.forEach { $0.cancel() } } } private func downloadAndCache(_ source: Source, retryContext: RetryContext? = nil) { let retryStrategy = optionsInfo.retryStrategy @Sendable func completeWithSuccess() { self.completedSources.append(source) self.reportProgress() if self.stopped { if self.tasks.isEmpty { self.failedSources.append(contentsOf: self.pendingSources) self.handleComplete() } } else { self.reportCompletionOrStartNext() } } @Sendable func completeWithFailure() { self.failedSources.append(source) self.reportProgress() if self.stopped { if self.tasks.isEmpty { self.failedSources.append(contentsOf: self.pendingSources) self.handleComplete() } } else { self.reportCompletionOrStartNext() } } let downloadTaskCompletionHandler: (@Sendable (Result) -> Void) = { result in self.tasks.removeValue(forKey: source.cacheKey) switch result { case .success: completeWithSuccess() case .failure(let error): guard let retryStrategy else { completeWithFailure() return } let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error) retryStrategy.retry(context: context) { decision in switch decision { case .retry(let userInfo): context.userInfo = userInfo self.prefetchQueue.async { guard !self.stopped else { completeWithFailure() return } self.downloadAndCache(source, retryContext: context) } case .stop: self.prefetchQueue.async { guard !self.stopped else { completeWithFailure() return } completeWithFailure() } } } } } var downloadTask: DownloadTask.WrappedTask? ImagePrefetcher.requestingQueue.sync { let context = RetrievingContext( options: optionsInfo, originalSource: source ) downloadTask = manager.loadAndCacheImage( source: source, context: context, completionHandler: downloadTaskCompletionHandler) } if let downloadTask = downloadTask { tasks[source.cacheKey] = downloadTask } } private func append(cached source: Source) { skippedSources.append(source) reportProgress() reportCompletionOrStartNext() } private func startPrefetching(_ source: Source) { if optionsInfo.forceRefresh { downloadAndCache(source) return } let cacheType = manager.cache.imageCachedType( forKey: source.cacheKey, processorIdentifier: optionsInfo.processor.identifier ) switch cacheType { case .memory: append(cached: source) case .disk: if optionsInfo.alsoPrefetchToMemory { let context = RetrievingContext(options: optionsInfo, originalSource: source) _ = manager.retrieveImageFromCache( source: source, context: context, downloadTaskUpdated: nil) { _ in self.append(cached: source) } } else { append(cached: source) } case .none: downloadAndCache(source) } } private func reportProgress() { if progressBlock == nil && progressSourceBlock == nil { return } let skipped = self.skippedSources let failed = self.failedSources let completed = self.completedSources CallbackQueue.mainCurrentOrAsync.execute { self.progressSourceBlock?(skipped, failed, completed) self.progressBlock?( skipped.compactMap { $0.asResource }, failed.compactMap { $0.asResource }, completed.compactMap { $0.asResource } ) } } private func reportCompletionOrStartNext() { if let resource = self.pendingSources.popFirst() { // Loose call stack for huge amount of sources. prefetchQueue.async { self.startPrefetching(resource) } } else { guard allFinished else { return } self.handleComplete() } } var allFinished: Bool { return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count } private func handleComplete() { if completionHandler == nil && completionSourceHandler == nil { return } // Snapshot arrays/handlers before switching threads to avoid concurrent mutation crashes. let skipped = self.skippedSources let failed = self.failedSources let completed = self.completedSources let completionSourceHandler = self.completionSourceHandler let completionHandler = self.completionHandler // The completion handler should be called on the main thread CallbackQueue.mainCurrentOrAsync.execute { completionSourceHandler?(skipped, failed, completed) completionHandler?( skipped.compactMap { $0.asResource }, failed.compactMap { $0.asResource }, completed.compactMap { $0.asResource } ) self.completionHandler = nil self.progressBlock = nil } } } ================================================ FILE: Sources/Networking/NetworkMetrics.swift ================================================ // // NetworkMetrics.swift // Kingfisher // // Created by FunnyValentine on 2025/07/25. // // Copyright (c) 2025 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents the network performance metrics collected during an image download task. public struct NetworkMetrics: Sendable { /// The original URLSessionTaskMetrics for advanced use cases. public let rawMetrics: URLSessionTaskMetrics /// The duration of the actual image retrieval (excluding redirects). public let retrieveImageDuration: TimeInterval? /// The total time from request start to completion (including redirects). public let totalRequestDuration: TimeInterval /// The time it took to perform DNS lookup. public let domainLookupDuration: TimeInterval? /// The time it took to establish the TCP connection. public let connectDuration: TimeInterval? /// The time it took to perform TLS handshake. public let secureConnectionDuration: TimeInterval? /// The number of bytes sent in the request body. public let requestBodyBytesSent: Int64 /// The number of bytes received in the response body. public let responseBodyBytesReceived: Int64 /// The HTTP response status code, if available. public let httpStatusCode: Int? /// The number of redirects that occurred during the request. public let redirectCount: Int /// Creates a NetworkMetrics instance from URLSessionTaskMetrics init?(from urlMetrics: URLSessionTaskMetrics) { // Find the first successful transaction (200-299 status) ignoring redirects // We need to ensure we get metrics from an actual successful download, not from // intermediate redirects (301/302) which don't represent real download performance var successfulTransaction: URLSessionTaskTransactionMetrics? for transaction in urlMetrics.transactionMetrics { if let httpResponse = transaction.response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) { successfulTransaction = transaction break } } // make sure we have a valid successful transaction guard let successfulTransaction else { return nil } // Store raw metrics for advanced use cases self.rawMetrics = urlMetrics // Calculate the image retrieval duration from the successful transaction self.retrieveImageDuration = Self.calculateRetrieveImageDuration(from: successfulTransaction) // Calculate the total request duration from the task interval self.totalRequestDuration = urlMetrics.taskInterval.duration // Calculate timing metrics from the successful transaction self.domainLookupDuration = Self.calculateDomainLookupDuration(from: successfulTransaction) self.connectDuration = Self.calculateConnectDuration(from: successfulTransaction) self.secureConnectionDuration = Self.calculateSecureConnectionDuration(from: successfulTransaction) // Extract data transfer information from the successful transaction self.requestBodyBytesSent = successfulTransaction.countOfRequestBodyBytesSent self.responseBodyBytesReceived = successfulTransaction.countOfResponseBodyBytesReceived // Extract HTTP status code from the successful transaction self.httpStatusCode = Self.extractHTTPStatusCode(from: successfulTransaction) // Extract redirect count self.redirectCount = urlMetrics.redirectCount } // MARK: - Private Calculation Methods /// Calculates DNS lookup duration /// Formula: domainLookupEndDate - domainLookupStartDate /// Represents: Time spent resolving domain name to IP address private static func calculateDomainLookupDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? { guard let start = transaction.domainLookupStartDate, let end = transaction.domainLookupEndDate else { return nil } return end.timeIntervalSince(start) } /// Calculates TCP connection establishment duration /// Formula: connectEndDate - connectStartDate /// Represents: Time spent establishing TCP connection to server private static func calculateConnectDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? { guard let start = transaction.connectStartDate, let end = transaction.connectEndDate else { return nil } return end.timeIntervalSince(start) } /// Calculates TLS/SSL handshake duration /// Formula: secureConnectionEndDate - secureConnectionStartDate /// Represents: Time spent performing TLS/SSL handshake for HTTPS connections private static func calculateSecureConnectionDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? { guard let start = transaction.secureConnectionStartDate, let end = transaction.secureConnectionEndDate else { return nil } return end.timeIntervalSince(start) } /// Calculates the image retrieval duration for a single transaction /// Formula: responseEndDate - requestStartDate /// Represents: Time from sending HTTP request to receiving complete image response private static func calculateRetrieveImageDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? { guard let start = transaction.requestStartDate, let end = transaction.responseEndDate else { return nil } return end.timeIntervalSince(start) } /// Extracts HTTP status code from response /// Returns: HTTP status code (200, 404, etc.) or nil for non-HTTP responses private static func extractHTTPStatusCode(from transaction: URLSessionTaskTransactionMetrics) -> Int? { return (transaction.response as? HTTPURLResponse)?.statusCode } } // MARK: - Convenience Properties extension NetworkMetrics { /// The download speed in bytes per second. /// /// Calculated as `responseBodyBytesReceived / retrieveImageDuration`. /// Returns `nil` if the duration is unavailable or zero, or if no data was received. /// /// - Note: This uses the actual image retrieval duration, excluding redirects and other overhead, /// to provide the most accurate representation of the data transfer rate. public var downloadSpeed: Double? { guard responseBodyBytesReceived > 0, let duration = retrieveImageDuration, duration > 0 else { return nil } return Double(responseBodyBytesReceived) / duration } /// The download speed in megabytes per second (MB/s). /// /// This is a convenience property that converts `downloadSpeed` from bytes per second /// to megabytes per second for easier readability. /// /// - Returns: Download speed in MB/s, or `nil` if `downloadSpeed` is unavailable. public var downloadSpeedMBps: Double? { guard let speed = downloadSpeed else { return nil } return speed / (1024 * 1024) } } ================================================ FILE: Sources/Networking/NetworkMonitor.swift ================================================ // // NetworkMonitor.swift // Kingfisher // // Created by Vladislav Komkov on 2025/09/22. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Network import Foundation /// A protocol for network connectivity monitoring that allows for dependency injection and testing. internal protocol NetworkMonitoring: Sendable { /// Whether the network is currently connected. var isConnected: Bool { get } /// Observes network connectivity changes with an optional timeout. /// - Parameters: /// - timeoutInterval: The timeout for waiting for network reconnection. If nil, no timeout is applied. /// - callback: The callback to be called when network state changes or timeout occurs. /// - Returns: A cancellable observer that can be used to cancel the observation. func observeConnectivity(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void) -> NetworkObserver } /// A protocol for network observers that can be cancelled. internal protocol NetworkObserver: Sendable { /// Cancels the network observation. func cancel() } /// A shared singleton that manages network connectivity monitoring. /// This prevents creating multiple NWPathMonitor instances when many NetworkRetryStrategy instances are used. /// The monitor is created lazily only when first accessed. internal final class NetworkMonitor: @unchecked Sendable, NetworkMonitoring { static let `default` = NetworkMonitor() /// Whether the network is currently connected. var isConnected: Bool { return monitor.currentPath.status == .satisfied } /// The network path monitor for observing connectivity changes. private let monitor = NWPathMonitor() /// The queue for monitoring network changes. private let monitorQueue = DispatchQueue(label: "com.onevcat.Kingfisher.NetworkMonitor", qos: .utility) /// Observers waiting for network reconnection. private var observers: [NetworkObserverImpl] = [] private let observersQueue = DispatchQueue(label: "com.onevcat.Kingfisher.NetworkMonitor.Observers", attributes: .concurrent) /// Whether the monitor has been started. private var isStarted = false private let startQueue = DispatchQueue(label: "com.onevcat.Kingfisher.NetworkMonitor.Start") private init() { // Set up path monitoring monitor.pathUpdateHandler = { [weak self] path in self?.handlePathUpdate(path) } } /// Starts monitoring if not already started. private func startMonitoring() { startQueue.sync { guard !isStarted else { return } monitor.start(queue: monitorQueue) isStarted = true } } /// Handles network path updates and notifies observers. private func handlePathUpdate(_ path: NWPath) { let connected = path.status == .satisfied guard connected else { return } // Notify all observers that network is available observersQueue.async(flags: .barrier) { let activeObservers = self.observers self.observers.removeAll() DispatchQueue.main.async { activeObservers.forEach { $0.notify(isConnected: true) } } } } /// Adds an observer for network reconnection. private func addObserver(_ observer: NetworkObserverImpl) { startMonitoring() observersQueue.async(flags: .barrier) { self.observers.append(observer) } } /// Removes an observer. internal func removeObserver(_ observer: NetworkObserverImpl) { observersQueue.async(flags: .barrier) { self.observers.removeAll { $0 === observer } } } // MARK: - NetworkMonitoring public func observeConnectivity(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void) -> NetworkObserver { let observer = NetworkObserverImpl( timeoutInterval: timeoutInterval, callback: callback, monitor: self ) addObserver(observer) return observer } } /// Internal implementation of network observer that manages timeout and callbacks. internal final class NetworkObserverImpl: @unchecked Sendable, NetworkObserver { let timeoutInterval: TimeInterval? let callback: @Sendable (Bool) -> Void private weak var monitor: NetworkMonitor? private var timeoutWorkItem: DispatchWorkItem? private let queue = DispatchQueue(label: "com.onevcat.Kingfisher.NetworkObserver", qos: .utility) init(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void, monitor: NetworkMonitor) { self.timeoutInterval = timeoutInterval self.callback = callback self.monitor = monitor // Set up timeout if specified if let timeoutInterval = timeoutInterval { let workItem = DispatchWorkItem { [weak self] in self?.notify(isConnected: false) } timeoutWorkItem = workItem queue.asyncAfter(deadline: .now() + timeoutInterval, execute: workItem) } } func notify(isConnected: Bool) { queue.async { [weak self] in guard let self else { return } // Cancel timeout if we're notifying timeoutWorkItem?.cancel() timeoutWorkItem = nil // Remove from monitor monitor?.removeObserver(self) // Call the callback DispatchQueue.main.async { self.callback(isConnected) } } } func cancel() { queue.async { [weak self] in guard let self else { return } // Cancel timeout timeoutWorkItem?.cancel() timeoutWorkItem = nil // Remove from monitor monitor?.removeObserver(self) } } } ================================================ FILE: Sources/Networking/RedirectHandler.swift ================================================ // // RedirectHandler.swift // Kingfisher // // Created by Roman Maidanovych on 2018/12/10. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// The ``ImageDownloadRedirectHandler`` is used to modify the request before redirection. /// /// This allows you to customize the image download request during redirection. You can make modifications for /// purposes such as adding an authentication token to the header, performing basic HTTP authentication, or URL /// mapping. /// /// Typically, you pass an ``ImageDownloadRedirectHandler`` as the associated value of /// ``KingfisherOptionsInfoItem/redirectHandler(_:)`` and use it as the `options` parameter in relevant methods. /// /// If you do not make any changes to the input `request` and return it as is, the downloading process will redirect /// using it. /// public protocol ImageDownloadRedirectHandler: Sendable { /// Called when a redirect is received and the downloader waiting for the request to continue the download task. /// /// - Parameters: /// - task: The current ``SessionDataTask`` that triggers this redirect. /// - response: The response received during redirection. /// - newRequest: The new request received from the URL session for redirection that can be modified. /// - Returns: The modified request. func handleHTTPRedirection( for task: SessionDataTask, response: HTTPURLResponse, newRequest: URLRequest ) async -> URLRequest? } /// A wrapper for creating an ``ImageDownloadRedirectHandler`` instance more easily. /// /// This type conforms to ``ImageDownloadRedirectHandler`` and wraps an image modification block. public struct AnyRedirectHandler: ImageDownloadRedirectHandler { let block: @Sendable (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void public func handleHTTPRedirection( for task: SessionDataTask, response: HTTPURLResponse, newRequest: URLRequest ) async -> URLRequest? { return await withCheckedContinuation { continuation in block(task, response, newRequest, { urlRequest in continuation.resume(returning: urlRequest) }) } } /// Creates a value of ``ImageDownloadRedirectHandler`` that executes the `modify` block. /// /// - Parameter handle: The block that modifies the request when a request modification task is triggered. public init(handle: @escaping @Sendable (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void) { block = handle } } ================================================ FILE: Sources/Networking/RequestModifier.swift ================================================ // // RequestModifier.swift // Kingfisher // // Created by Wei Wang on 2016/09/05. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents and wraps a method for modifying a request before an image download request starts asynchronously. /// /// Usually, you pass an ``AsyncImageDownloadRequestModifier`` instance as the associated value of /// ``KingfisherOptionsInfoItem/requestModifier(_:)`` and use it as the `options` parameter in related methods. /// /// For example, the code below defines a modifier to add a header field and its value to the request. /// /// ```swift /// class HeaderFieldModifier: AsyncImageDownloadRequestModifier { /// var onDownloadTaskStarted: ((DownloadTask?) -> Void)? = nil /// func modified(for request: URLRequest) async -> URLRequest? { /// var r = request /// let token = await service.fetchToken() /// r.setValue(token, forHTTPHeaderField: "token") /// return r /// } /// } /// /// imageView.kf.setImage(with: url, options: [.requestModifier(HeaderFieldModifier())]) /// ``` public protocol AsyncImageDownloadRequestModifier: Sendable { /// This method will be called just before the `request` is sent. /// /// This is the last chance to modify the image download request. You can modify the request for some customizing /// purposes, such as adding an auth token to the header, performing basic HTTP auth, or something like URL mapping. /// /// After making the modification, you should return the modified request, and the data will be downloaded with /// this modified request. /// /// > If you do nothing with the input `request` and return it as-is, the download process will start with it as the /// modifier doesn't exist. If you return `nil`, the downloading will be interrupted with an /// ``KingfisherError/RequestErrorReason/emptyRequest`` error. /// /// - Parameter request: The input request contains necessary information like `url`. This request is generated /// according to your resource URL as a GET request. /// - Returns: The modified request which should be used to trigger the download. func modified(for request: URLRequest) async -> URLRequest? /// A block that will be called when the download task starts. /// /// If an ``AsyncImageDownloadRequestModifier`` and asynchronous modification occur before the download, the /// related download method will not return a valid ``DownloadTask`` value. Instead, you can get one from this /// method. /// /// User the ``DownloadTask`` value to track the task, or cancel it when you need to. var onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)? { get } } /// Represents and wraps a method for modifying a request before an image download request starts synchronously. /// /// Usually, you pass an ``ImageDownloadRequestModifier`` instance as the associated value of /// ``KingfisherOptionsInfoItem/requestModifier(_:)`` and use it as the `options` parameter in related methods. /// /// For example, the code below defines a modifier to add a header field and its value to the request. /// /// ```swift /// class HeaderFieldModifier: AsyncImageDownloadRequestModifier { /// func modified(for request: URLRequest) -> URLRequest? { /// var r = request /// r.setValue("value", forHTTPHeaderField: "key") /// return r /// } /// } /// /// imageView.kf.setImage(with: url, options: [.requestModifier(HeaderFieldModifier())]) /// ``` public protocol ImageDownloadRequestModifier: AsyncImageDownloadRequestModifier { /// This method will be called just before the `request` is sent. /// /// This is the last chance to modify the image download request. You can modify the request for some customizing /// purposes, such as adding an auth token to the header, performing basic HTTP auth, or something like URL mapping. /// /// After making the modification, you should return the modified request, and the data will be downloaded with /// this modified request. /// /// > If you do nothing with the input `request` and return it as-is, the download process will start with it as the /// modifier doesn't exist. If you return `nil`, the downloading will be interrupted with an /// ``KingfisherError/RequestErrorReason/emptyRequest`` error. /// /// > Tip: If you are trying to execute an async operation during the modify, choose to conform the /// ``AsyncImageDownloadRequestModifier`` instead. /// /// - Parameter request: The input request contains necessary information like `url`. This request is generated /// according to your resource URL as a GET request. /// - Returns: The modified request which should be used to trigger the download. func modified(for request: URLRequest) -> URLRequest? } extension ImageDownloadRequestModifier { /// This is `nil` for a sync `ImageDownloadRequestModifier` by default. You can get the `DownloadTask` from the /// return value of downloader method. public var onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)? { return nil } } /// A wrapper for creating an ``ImageDownloadRequestModifier`` instance more easily. /// /// This type conforms to ``ImageDownloadRequestModifier`` and wraps an image modification block. public struct AnyModifier: ImageDownloadRequestModifier { let block: @Sendable (URLRequest) -> URLRequest? public func modified(for request: URLRequest) -> URLRequest? { return block(request) } /// Creates a value of ``ImageDownloadRequestModifier`` that runs the `modify` block. /// /// - Parameter modify: The request modifying block runs when a request modifying task comes. public init(modify: @escaping @Sendable (URLRequest) -> URLRequest?) { block = modify } } ================================================ FILE: Sources/Networking/RetryStrategy.swift ================================================ // // RetryStrategy.swift // Kingfisher // // Created by onevcat on 2020/05/04. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents a retry context that could be used to determine the current retry status. /// /// The instance of this type can be shared between different retry attempts. public class RetryContext: @unchecked Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.RetryContextPropertyQueue") /// The source from which the target image should be retrieved. public let source: Source /// The source from which the target image should be retrieved. public let error: KingfisherError private var _retriedCount: Int /// The number of retries attempted before the current retry happens. /// /// This value is `0` if the current retry is for the first time. public var retriedCount: Int { get { propertyQueue.sync { _retriedCount } } set { propertyQueue.sync { _retriedCount = newValue } } } private var _userInfo: Any? = nil /// A user-set value for passing any other information during the retry. /// /// If you choose to use ``RetryDecision/retry(userInfo:)`` as the retry decision for /// ``RetryStrategy/retry(context:retryHandler:)``, the associated value of ``RetryDecision/retry(userInfo:)`` will /// be delivered to you in the next retry. public internal(set) var userInfo: Any? { get { propertyQueue.sync { _userInfo } } set { propertyQueue.sync { _userInfo = newValue } } } init(source: Source, error: KingfisherError) { self.source = source self.error = error _retriedCount = 0 } @discardableResult func increaseRetryCount() -> RetryContext { retriedCount += 1 return self } } /// Represents the decision on the behavior for the current retry. public enum RetryDecision { /// A retry should happen. The associated `userInfo` value will be passed to the next retry in the /// ``RetryContext`` parameter. case retry(userInfo: Any?) /// There should be no more retry attempts. The image retrieving process will fail with an error. case stop } /// Defines a retry strategy that can be applied to the ``KingfisherOptionsInfoItem/retryStrategy(_:)`` option. public protocol RetryStrategy: Sendable { /// Kingfisher calls this method if an error occurs during the image retrieving process from ``KingfisherManager``. /// /// You implement this method to provide the necessary logic based on the `context` parameter. Then you need to call /// `retryHandler` to pass the retry decision back to Kingfisher. /// /// - Parameters: /// - context: The retry context containing information of the current retry attempt. /// - retryHandler: A block you need to call with a decision on whether the retry should happen or not. func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void) } /// A retry strategy that guides Kingfisher to perform retry operation with some delay. /// /// When an error of ``KingfisherError/ResponseErrorReason`` happens, Kingfisher uses the retry strategy in its option /// to retry. This strategy defines a specified maximum retry count and a certain interval mechanism. public struct DelayRetryStrategy: RetryStrategy { /// Represents the interval mechanism used in a ``DelayRetryStrategy``. public enum Interval : Sendable{ /// The next retry attempt should happen in a fixed number of seconds. /// /// For example, if the associated value is 3, the attempt happens 3 seconds after the previous decision is /// made. case seconds(TimeInterval) /// The next retry attempt should happen in an accumulated duration. /// /// For example, if the associated value is 3, the attempts happen with intervals of 3, 6, 9, 12, ... seconds. case accumulated(TimeInterval) /// Uses a block to determine the next interval. /// /// The current retry count is given as a parameter. case custom(block: @Sendable (_ retriedCount: Int) -> TimeInterval) func timeInterval(for retriedCount: Int) -> TimeInterval { let retryAfter: TimeInterval switch self { case .seconds(let interval): retryAfter = interval case .accumulated(let interval): retryAfter = Double(retriedCount + 1) * interval case .custom(let block): retryAfter = block(retriedCount) } return retryAfter } } /// The maximum number of retries allowed by the retry strategy. public let maxRetryCount: Int /// The interval between retry attempts in the retry strategy. public let retryInterval: Interval /// Creates a delayed retry strategy. /// /// - Parameters: /// - maxRetryCount: The maximum number of retries allowed. /// - retryInterval: The mechanism defining the interval between retry attempts. /// /// By default, ``Interval/seconds(_:)`` with an associated value `3` is used to establish a constant retry /// interval. public init(maxRetryCount: Int, retryInterval: Interval = .seconds(3)) { self.maxRetryCount = maxRetryCount self.retryInterval = retryInterval } public func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void) { // Retry count exceeded. guard context.retriedCount < maxRetryCount else { retryHandler(.stop) return } // User cancel the task. No retry. guard !context.error.isTaskCancelled else { retryHandler(.stop) return } // Only retry for a response error. guard case KingfisherError.responseError = context.error else { retryHandler(.stop) return } let interval = retryInterval.timeInterval(for: context.retriedCount) if interval == 0 { retryHandler(.retry(userInfo: nil)) } else { DispatchQueue.main.asyncAfter(deadline: .now() + interval) { retryHandler(.retry(userInfo: nil)) } } } } /// A retry strategy that observes network state and retries on reconnect. /// /// This strategy only retries when network becomes available after a disconnection. /// It does not use any delay mechanisms - it retries immediately when network is restored. /// /// The network monitor is created lazily only when this strategy is first used, /// ensuring no unnecessary resource usage when the strategy is not in use. public struct NetworkRetryStrategy: RetryStrategy { /// The timeout for waiting for network reconnection (in seconds). private let timeoutInterval: TimeInterval? /// The network monitoring service used to observe connectivity changes. private let networkMonitor: NetworkMonitoring /// Creates a network-aware retry strategy. /// /// - Parameters: /// - timeoutInterval: The timeout for waiting for network reconnection. If nil, no timeout is applied. Defaults to 30 seconds. public init(timeoutInterval: TimeInterval? = 30) { self.init( timeoutInterval: timeoutInterval, networkMonitor: NetworkMonitor.default ) } internal init( timeoutInterval: TimeInterval?, networkMonitor: NetworkMonitoring ) { self.timeoutInterval = timeoutInterval self.networkMonitor = networkMonitor } public func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void) { // Dispose of any previous disposable from userInfo if let previousObserver = context.userInfo as? NetworkObserver { previousObserver.cancel() } // User cancel the task. No retry. guard !context.error.isTaskCancelled else { retryHandler(.stop) return } // Only retry for a response error. guard case KingfisherError.responseError = context.error else { retryHandler(.stop) return } // Check if we have network connectivity if networkMonitor.isConnected { // Network is available, retry immediately retryHandler(.retry(userInfo: nil)) } else { // Network is not available, wait for reconnection waitForReconnection(context: context, retryHandler: retryHandler) } } // MARK: - Private helpers private func waitForReconnection( context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void ) { let observer = networkMonitor.observeConnectivity(timeoutInterval: timeoutInterval) { [weak context] isConnected in if isConnected { // Connection is restored, retry immediately retryHandler(.retry(userInfo: context?.userInfo)) } else { // Timeout reached or cancelled retryHandler(.stop) } } // Store the observer in userInfo so it can be cancelled if needed context.userInfo = observer } } ================================================ FILE: Sources/Networking/SessionDataTask.swift ================================================ // // SessionDataTask.swift // Kingfisher // // Created by Wei Wang on 2018/11/1. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents a session data task in ``ImageDownloader``. /// /// Essentially, a ``SessionDataTask`` wraps a `URLSessionDataTask` and manages the download data. /// It uses a ``SessionDataTask/CancelToken`` to track the task and manage its cancellation. public class SessionDataTask: @unchecked Sendable { /// Represents the type of token used for canceling a task. public typealias CancelToken = Int struct TaskCallback { let onCompleted: Delegate, Void>? let options: KingfisherParsedOptionsInfo } private var _mutableData: Data /// The downloaded raw data of the current task. public var mutableData: Data { lock.lock() defer { lock.unlock() } return _mutableData } // This is a copy of `task.originalRequest?.url`. It is for obtaining race-safe behavior for a pitfall on iOS 13. // Ref: https://github.com/onevcat/Kingfisher/issues/1511 public let originalURL: URL? /// The underlying download task. /// /// It is only for debugging purposes when you encounter an error. You should not modify the content of this task /// or start it yourself. public let task: URLSessionDataTask private var callbacksStore = [CancelToken: TaskCallback]() var callbacks: [SessionDataTask.TaskCallback] { lock.lock() defer { lock.unlock() } return Array(callbacksStore.values) } private var currentToken = 0 private let lock = NSLock() private var _metrics: NetworkMetrics? /// The network metrics collected during the download task. public var metrics: NetworkMetrics? { lock.lock() defer { lock.unlock() } return _metrics } let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>() let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>() var started = false var containsCallbacks: Bool { // We should be able to use `task.state != .running` to check it. // However, in some rare cases, cancelling the task does not change // task state to `.cancelling` immediately, but still in `.running`. // So we need to check callbacks count to for sure that it is safe to remove the // task in delegate. return !callbacks.isEmpty } init(task: URLSessionDataTask) { self.task = task self.originalURL = task.originalRequest?.url _mutableData = Data() } func addCallback(_ callback: TaskCallback) -> CancelToken { lock.lock() defer { lock.unlock() } callbacksStore[currentToken] = callback defer { currentToken += 1 } return currentToken } func removeCallback(_ token: CancelToken) -> TaskCallback? { lock.lock() defer { lock.unlock() } if let callback = callbacksStore[token] { callbacksStore[token] = nil return callback } return nil } @discardableResult func removeAllCallbacks() -> [TaskCallback] { lock.lock() defer { lock.unlock() } let callbacks = callbacksStore.values callbacksStore.removeAll() return Array(callbacks) } func resume() { guard !started else { return } started = true task.resume() } func cancel(token: CancelToken) { guard let callback = removeCallback(token) else { return } onCallbackCancelled.call((token, callback)) } func forceCancel() { for token in callbacksStore.keys { cancel(token: token) } } func didReceiveData(_ data: Data) { lock.lock() defer { lock.unlock() } _mutableData.append(data) } func didCollectMetrics(_ metrics: NetworkMetrics) { lock.lock() defer { lock.unlock() } _metrics = metrics } } ================================================ FILE: Sources/Networking/SessionDelegate.swift ================================================ // // SessionDelegate.swift // Kingfisher // // Created by Wei Wang on 2018/11/1. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Represents the delegate object of the downloader session. /// /// It also behaves like a task manager for downloading. @objc(KFSessionDelegate) // Fix for ObjC header name conflicting. https://github.com/onevcat/Kingfisher/issues/1530 open class SessionDelegate: NSObject, @unchecked Sendable { typealias SessionChallengeFunc = ( URLSession, URLAuthenticationChallenge ) typealias SessionTaskChallengeFunc = ( URLSession, URLSessionTask, URLAuthenticationChallenge ) private var tasks: [URL: SessionDataTask] = [:] private let lock = NSLock() let onValidStatusCode = Delegate() let onResponseReceived = Delegate() let onDownloadingFinished = Delegate<(URL, Result), Void>() let onDidDownloadData = Delegate() let onReceiveSessionChallenge = Delegate() let onReceiveSessionTaskChallenge = Delegate() func add( _ dataTask: URLSessionDataTask, url: URL, callback: SessionDataTask.TaskCallback) -> DownloadTask { lock.lock() defer { lock.unlock() } // Create a new task if necessary. let task = SessionDataTask(task: dataTask) task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in guard let task = task else { return } let (token, callback) = value let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token)) task.onTaskDone.call((.failure(error), [callback])) // No other callbacks waiting, we can clear the task now. if !task.containsCallbacks { let dataTask = task.task self.cancelTask(dataTask) self.remove(task) } } let token = task.addCallback(callback) tasks[url] = task return DownloadTask(sessionTask: task, cancelToken: token) } private func cancelTask(_ dataTask: URLSessionDataTask) { lock.lock() defer { lock.unlock() } dataTask.cancel() } func append( _ task: SessionDataTask, callback: SessionDataTask.TaskCallback) -> DownloadTask { let token = task.addCallback(callback) return DownloadTask(sessionTask: task, cancelToken: token) } private func remove(_ task: SessionDataTask) { lock.lock() defer { lock.unlock() } guard let url = task.originalURL else { return } task.removeAllCallbacks() tasks[url] = nil } private func task(for task: URLSessionTask) -> SessionDataTask? { lock.lock() defer { lock.unlock() } guard let url = task.originalRequest?.url else { return nil } guard let sessionTask = tasks[url] else { return nil } guard sessionTask.task.taskIdentifier == task.taskIdentifier else { return nil } return sessionTask } func task(for url: URL) -> SessionDataTask? { lock.lock() defer { lock.unlock() } return tasks[url] } func cancelAll() { lock.lock() let taskValues = tasks.values lock.unlock() for task in taskValues { task.forceCancel() } } func cancel(url: URL) { lock.lock() let task = tasks[url] lock.unlock() task?.forceCancel() } } extension SessionDelegate: URLSessionDataDelegate { open func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse ) async -> URLSession.ResponseDisposition { guard let httpResponse = response as? HTTPURLResponse else { let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response)) onCompleted(task: dataTask, result: .failure(error)) return .cancel } let httpStatusCode = httpResponse.statusCode guard onValidStatusCode.call(httpStatusCode) == true else { let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse)) onCompleted(task: dataTask, result: .failure(error)) return .cancel } guard let disposition = await onResponseReceived.callAsync(response) else { return .cancel } if disposition == .cancel { let error = KingfisherError.responseError(reason: .cancelledByDelegate(response: response)) self.onCompleted(task: dataTask, result: .failure(error)) } return disposition } open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { guard let task = self.task(for: dataTask) else { return } task.didReceiveData(data) task.callbacks.forEach { callback in callback.options.onDataReceived?.forEach { sideEffect in sideEffect.onDataReceived(session, task: task, data: data) } } } open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { guard let sessionTask = self.task(for: task) else { return } if let url = sessionTask.originalURL { let result: Result if let error = error { result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) } else if let response = task.response { result = .success(response) } else { result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask))) } onDownloadingFinished.call((url, result)) } let result: Result<(Data, URLResponse?), KingfisherError> if let error = error { result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) } else { if let data = onDidDownloadData.call(sessionTask) { result = .success((data, task.response)) } else { result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask))) } } onCompleted(task: task, result: result) } open func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { await onReceiveSessionChallenge.callAsync((session, challenge)) ?? (.performDefaultHandling, nil) } open func urlSession( _ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { await onReceiveSessionTaskChallenge.callAsync((session, task, challenge)) ?? (.performDefaultHandling, nil) } open func urlSession( _ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest ) async -> URLRequest? { guard let sessionDataTask = self.task(for: task), let redirectHandler = Array(sessionDataTask.callbacks).last?.options.redirectHandler else { return request } return await redirectHandler.handleHTTPRedirection( for: sessionDataTask, response: response, newRequest: request ) } open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { guard let sessionTask = self.task(for: task) else { return } // Collect network metrics for the completed task if let networkMetrics = NetworkMetrics(from: metrics) { sessionTask.didCollectMetrics(networkMetrics) } } private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?), KingfisherError>) { guard let sessionTask = self.task(for: task) else { return } let callbacks = sessionTask.removeAllCallbacks() sessionTask.onTaskDone.call((result, callbacks)) remove(sessionTask) } } ================================================ FILE: Sources/PrivacyInfo.xcprivacy ================================================ NSPrivacyAccessedAPITypes NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons C617.1 NSPrivacyTracking NSPrivacyTrackingDomains NSPrivacyCollectedDataTypes ================================================ FILE: Sources/SwiftUI/ImageBinder.swift ================================================ // // ImageBinder.swift // Kingfisher // // Created by onevcat on 2019/06/27. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(SwiftUI) && canImport(Combine) import SwiftUI import Combine @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension KFImage { /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs /// image downloading and progress reporting based on `KingfisherManager`. @MainActor class ImageBinder: ObservableObject { init() {} var downloadTask: DownloadTask? private var loading = false var loadingOrSucceeded: Bool { return loading || loadedImage != nil } // Do not use @Published due to https://github.com/onevcat/Kingfisher/issues/1717. Revert to @Published once // we can drop iOS 12. private(set) var loaded = false private(set) var animating = false var loadedImage: KFCrossPlatformImage? = nil { willSet { objectWillChange.send() } } var failureView: (() -> AnyView)? = nil { willSet { objectWillChange.send() } } var progress: Progress = .init() func markLoading() { loading = true } func markLoaded(sendChangeEvent: Bool) { loaded = true if sendChangeEvent { objectWillChange.send() } } func start(context: Context) where HoldingView: Sendable { guard let source = context.source else { CallbackQueueMain.currentOrAsync { context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource)) if let view = context.failureView { self.failureView = view } else if let image = context.options.onFailureImage { self.loadedImage = image } self.loading = false self.markLoaded(sendChangeEvent: false) } return } loading = true progress = .init() downloadTask = KingfisherManager.shared .retrieveImage( with: source, options: context.options, progressBlock: { size, total in self.updateProgress(downloaded: size, total: total) context.onProgressDelegate.call((size, total)) }, progressiveImageSetter: { image in CallbackQueueMain.currentOrAsync { self.markLoaded(sendChangeEvent: true) self.loadedImage = image } }, completionHandler: { [weak self] result in guard let self else { return } CallbackQueueMain.currentOrAsync { self.downloadTask = nil self.loading = false } switch result { case .success(let value): CallbackQueueMain.currentOrAsync { if context.swiftUITransition != nil, context.shouldApplyFade(cacheType: value.cacheType) { // Apply SwiftUI loadTransition with custom animation (higher priority than fade) self.animating = true let animation = context.swiftUIAnimation ?? .default withAnimation(animation) { self.markLoaded(sendChangeEvent: true) } } else if let fadeDuration = context.fadeTransitionDuration(cacheType: value.cacheType) { self.animating = true let animation = Animation.linear(duration: fadeDuration) withAnimation(animation) { // Trigger the view render to apply the animation. self.markLoaded(sendChangeEvent: true) } } else { self.markLoaded(sendChangeEvent: false) } self.loadedImage = value.image self.animating = false } CallbackQueueMain.async { context.onSuccessDelegate.call(value) } case .failure(let error): CallbackQueueMain.currentOrAsync { if let view = context.failureView { self.failureView = view } else if let image = context.options.onFailureImage { self.loadedImage = image } self.markLoaded(sendChangeEvent: false) } CallbackQueueMain.async { context.onFailureDelegate.call(error) } } }) } private func updateProgress(downloaded: Int64, total: Int64) { progress.totalUnitCount = total progress.completedUnitCount = downloaded objectWillChange.send() } /// Cancels the download task if it is in progress. func cancel() { downloadTask?.cancel() downloadTask = nil loading = false } /// Restores the download task priority to default if it is in progress. func restorePriorityOnAppear() { guard let downloadTask = downloadTask, loading == true else { return } downloadTask.sessionTask?.task.priority = URLSessionTask.defaultPriority } /// Reduce the download task priority if it is in progress. func reducePriorityOnDisappear() { guard let downloadTask = downloadTask, loading == true else { return } downloadTask.sessionTask?.task.priority = URLSessionTask.lowPriority } } } #endif ================================================ FILE: Sources/SwiftUI/ImageContext.swift ================================================ // // ImageContext.swift // Kingfisher // // Created by onevcat on 2021/05/08. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(SwiftUI) && canImport(Combine) import SwiftUI import Combine @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension KFImage { public class Context: @unchecked Sendable where HoldingView: Sendable { private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.KFImageContextPropertyQueue") let source: Source? var _options = KingfisherParsedOptionsInfo( KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously] ) var options: KingfisherParsedOptionsInfo { get { propertyQueue.sync { _options } } set { propertyQueue.sync { _options = newValue } } } var _configurations: [(HoldingView) -> HoldingView] = [] var configurations: [(HoldingView) -> HoldingView] { get { propertyQueue.sync { _configurations } } set { propertyQueue.sync { _configurations = newValue } } } var _renderConfigurations: [(HoldingView.RenderingView) -> Void] = [] var renderConfigurations: [(HoldingView.RenderingView) -> Void] { get { propertyQueue.sync { _renderConfigurations } } set { propertyQueue.sync { _renderConfigurations = newValue } } } var _contentConfiguration: ((HoldingView) -> AnyView)? = nil var contentConfiguration: ((HoldingView) -> AnyView)? { get { propertyQueue.sync { _contentConfiguration } } set { propertyQueue.sync { _contentConfiguration = newValue } } } var _cancelOnDisappear: Bool = false var cancelOnDisappear: Bool { get { propertyQueue.sync { _cancelOnDisappear } } set { propertyQueue.sync { _cancelOnDisappear = newValue } } } var _reducePriorityOnDisappear: Bool = false var reducePriorityOnDisappear: Bool { get { propertyQueue.sync { _reducePriorityOnDisappear } } set { propertyQueue.sync { _reducePriorityOnDisappear = newValue } } } var _placeholder: ((Progress) -> AnyView)? = nil var placeholder: ((Progress) -> AnyView)? { get { propertyQueue.sync { _placeholder } } set { propertyQueue.sync { _placeholder = newValue } } } var _failureView: (() -> AnyView)? = nil var failureView: (() -> AnyView)? { get { propertyQueue.sync { _failureView } } set { propertyQueue.sync { _failureView = newValue } } } var _startLoadingBeforeViewAppear: Bool = false var startLoadingBeforeViewAppear: Bool { get { propertyQueue.sync { _startLoadingBeforeViewAppear } } set { propertyQueue.sync { _startLoadingBeforeViewAppear = newValue } } } // SwiftUI transition support var _swiftUITransition: AnyTransition? = nil var swiftUITransition: AnyTransition? { get { propertyQueue.sync { _swiftUITransition } } set { propertyQueue.sync { _swiftUITransition = newValue } } } var _swiftUIAnimation: Animation? = nil var swiftUIAnimation: Animation? { get { propertyQueue.sync { _swiftUIAnimation } } set { propertyQueue.sync { _swiftUIAnimation = newValue } } } let onFailureDelegate = Delegate() let onSuccessDelegate = Delegate() let onProgressDelegate = Delegate<(Int64, Int64), Void>() init(source: Source?) { self.source = source } func shouldApplyFade(cacheType: CacheType) -> Bool { options.forceTransition || cacheType == .none } func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? { shouldApplyFade(cacheType: cacheType) ? options.transition.fadeDuration : nil } } } extension ImageTransition { // Only for fade effect in SwiftUI. fileprivate var fadeDuration: TimeInterval? { switch self { case .fade(let duration): return duration default: return nil } } } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension KFImage.Context: Hashable { public static func == (lhs: KFImage.Context, rhs: KFImage.Context) -> Bool { lhs.source == rhs.source && lhs.options.processor.identifier == rhs.options.processor.identifier } public func hash(into hasher: inout Hasher) { hasher.combine(source) hasher.combine(options.processor.identifier) } } #if !os(watchOS) @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) extension KFAnimatedImage { public typealias Context = KFImage.Context typealias ImageBinder = KFImage.ImageBinder } #endif #endif ================================================ FILE: Sources/SwiftUI/KFAnimatedImage.swift ================================================ // // KFAnimatedImage.swift // Kingfisher // // Created by wangxingbin on 2021/4/29. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(SwiftUI) && canImport(Combine) && !os(watchOS) import SwiftUI import Combine /// Represents an animated image view in SwiftUI that manages its content using Kingfisher. /// /// Similar to ``KFImage``, this view provides support for animated image formats like GIF. /// /// - Important: Like ``KFImage``, `KFAnimatedImage` loads disk cached images synchronously by default /// (`.loadDiskFileSynchronously()` is enabled). This prevents image flickering during SwiftUI view updates /// but may impact performance when loading large animated images from disk. You can disable this behavior /// by calling `.loadDiskFileSynchronously(false)` if you prefer better loading performance over visual consistency. /// @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) public struct KFAnimatedImage: KFImageProtocol { public typealias HoldingView = KFAnimatedImageViewRepresenter public var context: Context public init(context: KFImage.Context) { self.context = context } /// Configures current rendering view with a `block`. This block will be applied when the under-hood /// `AnimatedImageView` is created in `UIViewRepresentable.makeUIView(context:)` /// /// - Parameter block: The block applies to the animated image view. /// - Returns: A `KFAnimatedImage` view that being configured by the `block`. public func configure(_ block: @escaping (HoldingView.RenderingView) -> Void) -> Self { context.renderConfigurations.append(block) return self } #if os(iOS) /// Whether the animated frame buffer should be purged when the app enters background. /// /// This is an opt-in behavior to reduce memory footprint when your app is in background. When enabled, /// the internal `AnimatedImageView` stops animating and purges preloaded frames on /// `UIApplication.didEnterBackgroundNotification`. If the view was animating before entering background, it will /// prepare frames and resume animation on `UIApplication.willEnterForegroundNotification`. /// /// - Parameter purge: Whether to enable the frame purging behavior. Default is `true`. /// - Returns: A `KFAnimatedImage` view that configures the behavior. public func purgeFramesOnBackground(_ purge: Bool = true) -> Self { configure { $0.purgeFramesOnBackground = purge } } #endif } #if os(macOS) @available(macOS 11.0, *) typealias KFCrossPlatformViewRepresentable = NSViewRepresentable #else @available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) typealias KFCrossPlatformViewRepresentable = UIViewRepresentable #endif /// A wrapped `UIViewRepresentable` of `AnimatedImageView` @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) public struct KFAnimatedImageViewRepresenter: KFCrossPlatformViewRepresentable, KFImageHoldingView, Sendable { public typealias RenderingView = AnimatedImageView public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> KFAnimatedImageViewRepresenter { KFAnimatedImageViewRepresenter(image: image, context: context) } var image: KFCrossPlatformImage? let context: KFImage.Context #if os(macOS) public func makeNSView(context: Context) -> AnimatedImageView { return makeImageView() } public func updateNSView(_ nsView: AnimatedImageView, context: Context) { updateImageView(nsView) } #else public func makeUIView(context: Context) -> AnimatedImageView { return makeImageView() } public func updateUIView(_ uiView: AnimatedImageView, context: Context) { updateImageView(uiView) } #endif @MainActor private func makeImageView() -> AnimatedImageView { let view = AnimatedImageView() #if !os(macOS) view.isUserInteractionEnabled = true #endif self.context.renderConfigurations.forEach { $0(view) } view.image = image // Allow SwiftUI scale (fit/fill) working fine. view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) view.setContentCompressionResistancePriority(.defaultLow, for: .vertical) return view } @MainActor private func updateImageView(_ imageView: AnimatedImageView) { imageView.image = image } } #if DEBUG @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) struct KFAnimatedImage_Previews: PreviewProvider { static var previews: some View { Group { KFAnimatedImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif")!)) .onSuccess { r in print(r) } .placeholder { ProgressView() } .padding() } } } #endif #endif ================================================ FILE: Sources/SwiftUI/KFImage.swift ================================================ // // KFImage.swift // Kingfisher // // Created by onevcat on 2019/06/26. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(SwiftUI) && canImport(Combine) import SwiftUI import Combine /// Represents an image view in SwiftUI that manages its content using Kingfisher. /// /// This view asynchronously loads the content. You can set a ``Source`` to load for the ``KFImage`` through /// its ``KFImage/init(source:)`` or ``KFImage/init(_:)`` initializers or other relevant methods in ``KF`` Builder type. /// Kingfisher will first look for the required image in the cache. If it is not found, it will load it via the /// ``Source`` and provide the result for display, following sending the result to cache and for the future use. /// /// When using a `URL` valve as the ``Source``, it is similar to SwiftUI's `AsyncImage` but with additional support /// for caching. /// /// Here is a basic example of using ``KFImage``: /// /// ```swift /// var body: some View { /// KFImage(URL(string: "https://example.com/image.png")!) /// } /// ``` /// /// Usually, you can also use the value by calling additional modifiers defined on it, to configure the view: /// /// ```swift /// var body: some View { /// KFImage.url(url) /// .placeholder(placeholderImage) /// .setProcessor(processor) /// .loadDiskFileSynchronously() /// .cacheMemoryOnly() /// .onSuccess { result in } /// } /// ``` /// Here only very few are listed as demonstration. To check other available modifiers, see ``KFOptionSetter`` and its /// extension methods. /// /// - Important: `KFImage` loads disk cached images synchronously by default (`.loadDiskFileSynchronously()` is enabled). /// This prevents image flickering during SwiftUI view updates but may impact performance when loading large images from disk. /// You can disable this behavior by calling `.loadDiskFileSynchronously(false)` if you prefer better loading performance /// over visual consistency. /// @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) public struct KFImage: KFImageProtocol { /// Represent the wrapping context of the image view. /// /// Inside ``KFImage`` it is using the `SwiftUI.Image` to render the image. public var context: Context /// Initializes the ``KFImage`` with a context. /// /// This should be only used internally in Kingfisher. Do not use this initializer yourself. Instead, use /// ``KFImage/init(source:)`` or ``KFImage/init(_:)`` initializers or other relevant methods in ``KF`` Builder /// type. /// - Parameter context: The context value that the image view should wrap. public init(context: Context) { self.context = context } } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension Image: KFImageHoldingView { public typealias RenderingView = Image public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Image { Image(crossPlatformImage: image) } } // MARK: - Image compatibility. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension KFImage { public func resizable( capInsets: EdgeInsets = EdgeInsets(), resizingMode: Image.ResizingMode = .stretch) -> KFImage { configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) } } public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage { configure { $0.renderingMode(renderingMode) } } public func interpolation(_ interpolation: Image.Interpolation) -> KFImage { configure { $0.interpolation(interpolation) } } public func antialiased(_ isAntialiased: Bool) -> KFImage { configure { $0.antialiased(isAntialiased) } } /// Starts the loading process of `self` immediately. /// /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once /// could help avoiding the flickering, with some performance trade-off. /// /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. /// It does nothing now and please just remove it. /// /// - Returns: The `Self` value with changes applied. @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") public func loadImmediately(_ start: Bool = true) -> KFImage { return self } } #if DEBUG @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) struct KFImage_Previews: PreviewProvider { static var previews: some View { Group { KFImage.url(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!) .onSuccess { r in print(r) } .placeholder { p in ProgressView(p) } .resizable() .aspectRatio(contentMode: .fit) .padding() } } } #endif #endif ================================================ FILE: Sources/SwiftUI/KFImageOptions.swift ================================================ // // KFImageOptions.swift // Kingfisher // // Created by onevcat on 2020/12/20. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(SwiftUI) && canImport(Combine) import SwiftUI import Combine // MARK: - KFImage creating. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension KFImageProtocol { /// Creates a Kingfisher-compatible image view with a given ``Source``. /// /// - Parameters: /// - source: The ``Source`` object that defines data information from the network or a data provider. /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. public static func source( _ source: Source? ) -> Self { Self.init(source: source) } /// Creates a Kingfisher-compatible image view with a given ``Resource``. /// /// - Parameters: /// - resource: The ``Resource`` object that defines data information such as a key or URL. /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. public static func resource( _ resource: (any Resource)? ) -> Self { source(resource?.convertToSource()) } /// Creates a Kingfisher-compatible image view with a given `URL`. /// /// - Parameters: /// - url: The `URL` from which the image should be downloaded. /// - cacheKey: The key used to store the downloaded image in the cache. If `nil`, the `absoluteString` of `url` /// is used as the cache key. /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. public static func url( _ url: URL?, cacheKey: String? = nil ) -> Self { source(url?.convertToSource(overrideCacheKey: cacheKey)) } /// Creates a Kingfisher-compatible image view with a given ``ImageDataProvider``. /// /// - Parameters: /// - provider: The ``ImageDataProvider`` object that contains information about the data. /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. public static func dataProvider( _ provider: (any ImageDataProvider)? ) -> Self { source(provider?.convertToSource()) } /// Creates a builder for the provided raw data and a cache key. /// /// - Parameters: /// - data: The data object from which the image should be created. /// - cacheKey: The key used to store the downloaded image in the cache. /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. public static func data( _ data: Data?, cacheKey: String ) -> Self { if let data = data { return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) } else { return dataProvider(nil) } } } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension KFImageProtocol { /// Sets a placeholder `View` that is displayed during the image loading, with a progress parameter as input. /// /// - Parameter content: A view that represents the placeholder. /// - Returns: A Kingfisher-compatible image view that includes the provided `content` as its placeholder. public func placeholder(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self { context.placeholder = { progress in return AnyView(content(progress)) } return self } /// Sets a placeholder `View` that is displayed during the image loading. /// /// - Parameter content: A view that represents the placeholder. /// - Returns: A Kingfisher-compatible image view that includes the provided `content` as its placeholder. public func placeholder(@ViewBuilder _ content: @escaping () -> P) -> Self { placeholder { _ in content() } } /// Sets a failure `View` that is displayed when the image fails to load. /// /// Use this modifier to provide a custom view when image loading fails. This offers more flexibility than the /// deprecated `onFailureImage` API by allowing any SwiftUI view as the failure placeholder. /// /// Example: /// ```swift /// KFImage(url) /// .onFailureView { /// VStack { /// Image(systemName: "exclamationmark.triangle") /// .foregroundColor(.red) /// Text("Failed to load image") /// .font(.caption) /// Button("Retry") { /// // Retry logic /// } /// } /// } /// ``` /// /// - Note: If both deprecated `onFailureImage` and `onFailureView` are set, `onFailureView` takes precedence. /// /// - Parameter content: A view builder that creates the failure view. /// - Returns: A Kingfisher-compatible image view that displays the provided `content` when image loading fails. public func onFailureView(@ViewBuilder _ content: @escaping () -> F) -> Self { context.failureView = { AnyView(content()) } return self } /// Sets an image to display when the loading fails. /// /// - Deprecated: Use ``onFailureView(_:)`` instead, which lets you return any SwiftUI `View` and guarantees /// consistent behavior across SwiftUI platforms. The image-based fallback modifier is maintained purely for /// backward compatibility and will be removed in a future major release. @available(*, deprecated, message: "Use `onFailureView(_:)` to customize SwiftUI failure placeholders instead.") public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self { options.onFailureImage = .some(image) return self } /// Enables canceling the download task associated with `self` when the view disappears. /// /// - Parameter flag: A boolean value indicating whether to cancel the task. /// - Returns: A Kingfisher-compatible image view that cancels the download task when it disappears. public func cancelOnDisappear(_ flag: Bool) -> Self { context.cancelOnDisappear = flag return self } /// Sets reduce priority of the download task to low, bound to `self` when the view disappearing. /// - Parameter flag: Whether reduce the priority task or not. /// - Returns: A `KFImage` view that reduces downloading task priority when disappears. public func reducePriorityOnDisappear(_ flag: Bool) -> Self { context.reducePriorityOnDisappear = flag return self } /// Sets a fade transition for the image task. /// /// - Parameter duration: The duration of the fade transition. /// - Returns: A Kingfisher-compatible image view with the applied changes. /// /// Kingfisher will use the fade transition to animate the image if it is downloaded from the web. The transition /// will not occur when the image is retrieved from either memory or disk cache by default. If you need the /// transition to occur even when the image is retrieved from the cache, also call /// ``KFOptionSetter/forceRefresh(_:)`` on the returned view. public func fade(duration: TimeInterval) -> Self { context.options.transition = .fade(duration) return self } /// Sets whether to start the image loading before the view actually appears. /// /// - Parameter flag: A boolean value indicating whether the image loading should happen before the view appears. The default is `true`. /// - Returns: A Kingfisher-compatible image view with the applied changes. /// /// By default, Kingfisher performs lazy loading for `KFImage`. The image loading won't start until the view's /// `onAppear` is called. However, sometimes you may want to trigger aggressive loading for the view. By enabling /// this, the `KFImage` will attempt to load the view when its `body` is evaluated if the image loading has not /// yet started or if a previous loading attempt failed. /// /// > Important: This was a temporary workaround for an issue that arose in iOS 16, where the SwiftUI view's /// > `onAppear` was not called when it was deeply embedded inside a `List` or `ForEach`. This is no longer necessary /// > if built with Xcode 14.3 and deployed to iOS 16.4 or later. So, it is not needed anymore. /// > /// > Enabling this may cause performance regression, especially if you have a lot of images to load in the view. /// > Use it at your own risk. /// > /// > Please refer to [#1988](https://github.com/onevcat/Kingfisher/issues/1988) for more information. public func startLoadingBeforeViewAppear(_ flag: Bool = true) -> Self { context.startLoadingBeforeViewAppear = flag return self } /// Sets a SwiftUI transition for the image loading. /// /// - Parameters: /// - transition: The SwiftUI transition to apply when the image appears. /// - animation: The animation to use with the transition. Defaults to `.default`. /// - Returns: A Kingfisher-compatible image view with the applied transition. /// /// This is the recommended way to apply transitions in SwiftUI applications. Unlike the UIKit-based /// ``KingfisherOptionsInfoItem/transition(_:)`` option, this method uses native SwiftUI transitions, /// providing better integration with the SwiftUI animation system and access to all SwiftUI transition types. /// /// Available transitions include `.slide`, `.scale`, `.opacity`, `.move`, `.offset`, and custom transitions. /// The transition will be applied when the image is loaded from the network, following the same /// rules as the fade transition regarding cache behavior and `forceTransition`. /// /// When both `loadTransition` and `fade` are set, `loadTransition` takes precedence. /// /// Example: /// ```swift /// KFImage(url) /// .loadTransition(.slide, animation: .easeInOut(duration: 0.5)) /// ``` /// /// - Note: For UIKit/AppKit applications, use ``KingfisherOptionsInfoItem/transition(_:)`` instead. public func loadTransition(_ transition: AnyTransition, animation: Animation? = .default) -> Self { context.swiftUITransition = transition context.swiftUIAnimation = animation return self } /// Sets a SwiftUI transition for the image loading (iOS 17.0+). /// /// - Parameters: /// - transition: The SwiftUI transition conforming to the Transition protocol. /// - animation: The animation to use with the transition. Defaults to `.default`. /// - Returns: A Kingfisher-compatible image view with the applied transition. /// /// This method provides access to newer SwiftUI transitions available in iOS 17.0+, /// such as `BlurReplaceTransition`, `PushTransition`, and other transitions conforming to the `Transition` protocol. /// This is the recommended approach for SwiftUI applications on iOS 17.0+. /// /// When both `loadTransition` and `fade` are set, `loadTransition` takes precedence. /// /// Example: /// ```swift /// KFImage(url) /// .loadTransition(.blurReplace(.downUp), animation: .bouncy) /// ``` /// /// - Note: For UIKit/AppKit applications, use ``KingfisherOptionsInfoItem/transition(_:)`` instead. @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) public func loadTransition(_ transition: T, animation: Animation? = .default) -> Self { context.swiftUITransition = AnyTransition(transition) context.swiftUIAnimation = animation return self } } #endif ================================================ FILE: Sources/SwiftUI/KFImageProtocol.swift ================================================ // // KFImageProtocol.swift // Kingfisher // // Created by onevcat on 2021/05/08. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(SwiftUI) && canImport(Combine) import SwiftUI import Combine /// Represents a view that is compatible with Kingfisher in SwiftUI. /// /// As a framework user, you do not need to know the details of this protocol. As the public types, ``KFImage`` and /// ``KFAnimatedImage`` conform this type and should be used in your app to represent an image view with network and /// cache support in SwiftUI. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) @MainActor public protocol KFImageProtocol: View, KFOptionSetter { associatedtype HoldingView: KFImageHoldingView & Sendable var context: KFImage.Context { get set } init(context: KFImage.Context) } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension KFImageProtocol { @MainActor public var body: some View { ZStack { KFImageRenderer( context: context ).id(context) } } /// Creates an image view compatible with Kingfisher for loading an image from the provided `Source`. /// /// - Parameters: /// - source: The `Source` of the image that specifies where to load the target image. public init(source: Source?) { let context = KFImage.Context(source: source) self.init(context: context) } /// Creates an image view compatible with Kingfisher for loading an image from the provided `URL`. /// /// - Parameters: /// - url: The `URL` defining the location from which to load the target image. public init(_ url: URL?) { self.init(source: url?.convertToSource()) } /// Configures the current image with a `block` and returns another `Image` to use as the final content. /// /// This block will be lazily applied when creating the final `Image`. /// /// If multiple `configure` modifiers are added to the image, they will be evaluated in order. /// /// - Parameter block: The block that applies to the loaded image. The block should return an `Image` that is /// configured. /// - Returns: A ``KFImage`` or ``KFAnimatedImage`` view that configures the internal `Image` with the provided /// `block`. /// /// > If you want to configure the input image (which is usually an `Image` value) and use a non-`Image` value as /// > the configured result, use ``KFImageProtocol/contentConfigure(_:)`` instead. public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self { context.configurations.append(block) return self } /// Configures the current image with a `block` and returns a `View` to use as the final content. /// /// This block will be lazily applied when creating the final `Image`. /// /// If multiple `contentConfigure` modifiers are added to the image, only the last one will be stored and used. /// /// - Parameter block: The block applies to the loaded image. The block should return a `View` that is configured. /// - Returns: A ``KFImage`` or ``KFAnimatedImage`` view that configures the internal `Image` with the provided /// `block`. public func contentConfigure(_ block: @escaping (HoldingView) -> V) -> Self { context.contentConfiguration = { AnyView(block($0)) } return self } } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) @MainActor public protocol KFImageHoldingView: View { associatedtype RenderingView static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Self } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension KFImageProtocol { public var options: KingfisherParsedOptionsInfo { get { context.options } nonmutating set { context.options = newValue } } public var onFailureDelegate: Delegate { context.onFailureDelegate } public var onSuccessDelegate: Delegate { context.onSuccessDelegate } public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate } public var delegateObserver: AnyObject { context } } #endif ================================================ FILE: Sources/SwiftUI/KFImageRenderer.swift ================================================ // // KFImageRenderer.swift // Kingfisher // // Created by onevcat on 2021/05/08. // // Copyright (c) 2021 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(SwiftUI) && canImport(Combine) import SwiftUI import Combine /// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`. /// Declaring a `KFImage` in a `View`'s body to trigger loading from the given `Source`. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) struct KFImageRenderer : View where HoldingView: KFImageHoldingView & Sendable { @StateObject var binder: KFImage.ImageBinder = .init() let context: KFImage.Context var body: some View { if context.startLoadingBeforeViewAppear && !binder.loadingOrSucceeded && !binder.animating { binder.markLoading() DispatchQueue.main.async { binder.start(context: context) } } return ZStack { if context.swiftUITransition != nil { // SwiftUI loadTransition: insert/remove view for proper transition behavior if binder.loaded { renderedImage() } } else { // Fade transition or no transition: use opacity control renderedImage() .opacity(binder.loaded ? 1.0 : 0.0) } if binder.loadedImage == nil { ZStack { // Priority: failureView > placeholder > Color.clear // failureView is only set when image loading fails if let failureView = binder.failureView { failureView() } else if let placeholder = context.placeholder { placeholder(binder.progress) } else { Color.clear } } .onAppear { [weak binder = self.binder] in guard let binder = binder else { return } if !binder.loadingOrSucceeded { binder.start(context: context) } else { if context.reducePriorityOnDisappear { binder.restorePriorityOnAppear() } } } .onDisappear { [weak binder = self.binder] in guard let binder = binder else { return } if context.cancelOnDisappear { binder.cancel() } else if context.reducePriorityOnDisappear { binder.reducePriorityOnDisappear() } } } } // Workaround for https://github.com/onevcat/Kingfisher/issues/1988 // on iOS 16 there seems to be a bug that when in a List, the `onAppear` of the `ZStack` above in the // `binder.loadedImage == nil` not get called. Adding this empty `onAppear` fixes it and the life cycle can // work again. // // There is another "fix": adding an `else` clause and put a `Color.clear` there. But I believe this `onAppear` // should work better. // // It should be a bug in iOS 16, I guess it is some kinds of over-optimization in list cell loading caused it. .onAppear() } @ViewBuilder private func renderedImage() -> some View { if let swiftUITransition = context.swiftUITransition { // Apply SwiftUI loadTransition as the last step for correct rendering order configuredImage.transition(swiftUITransition) } else { configuredImage } } @ViewBuilder private var configuredImage: some View { let configuredImage = context.configurations .reduce(HoldingView.created(from: binder.loadedImage, context: context)) { current, config in config(current) } // Apply contentConfiguration first, then loadTransition as the final step if let contentConfiguration = context.contentConfiguration { contentConfiguration(configuredImage) } else { configuredImage } } } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension Image { // Creates an Image with either UIImage or NSImage. init(crossPlatformImage: KFCrossPlatformImage?) { #if canImport(UIKit) self.init(uiImage: crossPlatformImage ?? KFCrossPlatformImage()) #elseif canImport(AppKit) self.init(nsImage: crossPlatformImage ?? KFCrossPlatformImage()) #endif } } #if canImport(UIKit) @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension UIImage.Orientation { func toSwiftUI() -> Image.Orientation { switch self { case .down: return .down case .up: return .up case .left: return .left case .right: return .right case .upMirrored: return .upMirrored case .downMirrored: return .downMirrored case .leftMirrored: return .leftMirrored case .rightMirrored: return .rightMirrored @unknown default: return .up } } } #endif #endif ================================================ FILE: Sources/Utility/Box.swift ================================================ // // Box.swift // Kingfisher // // Created by Wei Wang on 2018/3/17. // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation class Box { var value: T init(_ value: T) { self.value = value } } ================================================ FILE: Sources/Utility/CallbackQueue.swift ================================================ // // CallbackQueue.swift // Kingfisher // // Created by onevcat on 2018/10/15. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public typealias ExecutionQueue = CallbackQueue /// Represents the behavior of the callback queue selection when a closure is dispatched. public enum CallbackQueue: Sendable { /// Dispatches the closure to `DispatchQueue.main` with an `async` behavior. case mainAsync /// Dispatches the closure to `DispatchQueue.main` with an `async` behavior if the current queue is not `.main`. /// Otherwise, it calls the closure immediately on the current main queue. case mainCurrentOrAsync /// Does not change the calling queue for the closure. case untouch /// Dispatches the closure to a specified `DispatchQueue`. case dispatch(DispatchQueue) /// Dispatches the closure to an operation-queue–like type. /// /// Use this case when you want to integrate Kingfisher's work into your own scheduling policy. /// For example, you can control concurrency, priority, or implement a LIFO execution order. /// /// - Note: Execution order and whether the block runs serially or concurrently depend on the /// provided queue. Kingfisher does not enforce ordering guarantees for this case. case operationQueue(CallbackOperationQueue) /// Executes the `block` in a dispatch queue defined by `self`. /// - Parameter block: The block needs to be executed. public func execute(_ block: @Sendable @escaping () -> Void) { switch self { case .mainAsync: CallbackQueueMain.async { block() } case .mainCurrentOrAsync: CallbackQueueMain.currentOrAsync { block() } case .untouch: block() case .dispatch(let queue): queue.async { block() } case .operationQueue(let queue): queue.addOperation(block) } } var queue: DispatchQueue { switch self { case .mainAsync: return .main case .mainCurrentOrAsync: return .main case .untouch: return OperationQueue.current?.underlyingQueue ?? .main case .dispatch(let queue): return queue case .operationQueue(let queue): return queue.underlyingQueue ?? .main } } } enum CallbackQueueMain { static func currentOrAsync(_ block: @MainActor @Sendable @escaping () -> Void) { if Thread.isMainThread { MainActor.runUnsafely { block() } } else { DispatchQueue.main.async { block() } } } static func async(_ block: @MainActor @Sendable @escaping () -> Void) { DispatchQueue.main.async { block() } } } extension MainActor { @_unavailableFromAsync static func runUnsafely(_ body: @MainActor () throws -> T) rethrows -> T { #if swift(>=5.10) return try MainActor.assumeIsolated(body) #else dispatchPrecondition(condition: .onQueue(.main)) return try withoutActuallyEscaping(body) { fn in try unsafeBitCast(fn, to: (() throws -> T).self)() } #endif } } /// A minimal abstraction used by ``CallbackQueue/operationQueue(_:)``. /// /// Conform your own type to control how Kingfisher schedules work. `OperationQueue` already /// conforms to this protocol. public protocol CallbackOperationQueue: AnyObject, Sendable { /// The underlying `DispatchQueue` if available. /// /// Kingfisher uses this value only when it needs a best-effort `DispatchQueue` representation. var underlyingQueue: DispatchQueue? { get } /// Schedules a block for execution on this queue. func addOperation(_ block: @Sendable @escaping () -> Void) } extension OperationQueue: CallbackOperationQueue {} ================================================ FILE: Sources/Utility/Delegate.swift ================================================ // // Delegate.swift // Kingfisher // // Created by onevcat on 2018/10/10. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// A class that maintains a weak reference to `self` when implementing `onXXX` behaviors. /// Instead of manually ensuring that `self` is kept as weak in a stored closure: /// /// ```swift /// // MyClass.swift /// var onDone: (() -> Void)? /// func done() { /// onDone?() /// } /// /// // ViewController.swift /// var obj: MyClass? /// /// func doSomething() { /// obj = MyClass() /// obj!.onDone = { [weak self] in /// self?.reportDone() /// } /// } /// ``` /// /// You can create a `Delegate` and observe it on `self`. This ensures there is no retain cycle: /// /// ```swift /// // MyClass.swift /// let onDone = Delegate<(), Void>() /// func done() { /// onDone.call() /// } /// /// // ViewController.swift /// var obj: MyClass? /// /// func doSomething() { /// obj = MyClass() /// obj!.onDone.delegate(on: self) { (self, _) in /// // The `self` here is shadowed and does not retain a strong reference. /// // Thus, both the `MyClass` instance and the `ViewController` instance can be released. /// self.reportDone() /// } /// } /// public class Delegate: @unchecked Sendable { public init() {} private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.DelegateQueue") private var _block: ((Input) -> Output?)? private var block: ((Input) -> Output?)? { get { propertyQueue.sync { _block } } set { propertyQueue.sync { _block = newValue } } } private var _asyncBlock: ((Input) async -> Output?)? private var asyncBlock: ((Input) async -> Output?)? { get { propertyQueue.sync { _asyncBlock } } set { propertyQueue.sync { _asyncBlock = newValue } } } public func delegate(on target: T, block: ((T, Input) -> Output)?) { self.block = { [weak target] input in guard let target = target else { return nil } return block?(target, input) } } public func delegate(on target: T, block: ((T, Input) async -> Output)?) { self.asyncBlock = { [weak target] input in guard let target = target else { return nil } return await block?(target, input) } } public func call(_ input: Input) -> Output? { return block?(input) } public func callAsFunction(_ input: Input) -> Output? { return call(input) } public func callAsync(_ input: Input) async -> Output? { return await asyncBlock?(input) } public var isSet: Bool { block != nil || asyncBlock != nil } } extension Delegate where Input == Void { public func call() -> Output? { return call(()) } public func callAsFunction() -> Output? { return call() } } extension Delegate where Input == Void, Output: OptionalProtocol { public func call() -> Output { return call(()) } public func callAsFunction() -> Output { return call() } } extension Delegate where Output: OptionalProtocol { public func call(_ input: Input) -> Output { if let result = block?(input) { return result } else { return Output._createNil } } public func callAsFunction(_ input: Input) -> Output { return call(input) } } public protocol OptionalProtocol { static var _createNil: Self { get } } extension Optional : OptionalProtocol { public static var _createNil: Optional { return nil } } ================================================ FILE: Sources/Utility/DisplayLink.swift ================================================ // // DisplayLink.swift // Kingfisher // // Created by yeatse on 2024/1/9. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) #if canImport(UIKit) import UIKit #else import AppKit import CoreVideo #endif protocol DisplayLinkCompatible: AnyObject, Sendable { var isPaused: Bool { get set } var preferredFramesPerSecond: NSInteger { get } var timestamp: CFTimeInterval { get } var duration: CFTimeInterval { get } func add(to runLoop: RunLoop, forMode mode: RunLoop.Mode) func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode) func invalidate() } #if !os(macOS) extension UIView { func compatibleDisplayLink(target: Any, selector: Selector) -> any DisplayLinkCompatible { return CADisplayLink(target: target, selector: selector) } } #if compiler(>=6) extension CADisplayLink: DisplayLinkCompatible, @retroactive @unchecked Sendable {} #else extension CADisplayLink: DisplayLinkCompatible, @unchecked Sendable {} #endif #else extension NSView { func compatibleDisplayLink(target: Any, selector: Selector) -> any DisplayLinkCompatible { #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. if #available(macOS 14.0, *) { return displayLink(target: target, selector: selector) } else { return DisplayLink(target: target, selector: selector) } #else return DisplayLink(target: target, selector: selector) #endif } } #if swift(>=5.9) @available(macOS 14.0, *) extension CADisplayLink: DisplayLinkCompatible { var preferredFramesPerSecond: NSInteger { return 0 } } #if compiler(>=6) @available(macOS 14.0, *) extension CADisplayLink: @retroactive @unchecked Sendable { } #else // compiler(>=6) @available(macOS 14.0, *) extension CADisplayLink: @unchecked Sendable { } #endif // compiler(>=6) #endif // swift(>=5.9) final class DisplayLink: DisplayLinkCompatible, @unchecked Sendable { private var link: CVDisplayLink? private var target: Any? private var selector: Selector? private var schedulers: [RunLoop: [RunLoop.Mode]] = [:] var preferredFramesPerSecond: NSInteger = 0 var timestamp: CFTimeInterval = 0 var duration: CFTimeInterval = 0 init(target: Any, selector: Selector) { self.target = target self.selector = selector CVDisplayLinkCreateWithActiveCGDisplays(&link) if let link = link { CVDisplayLinkSetOutputHandler(link) { displayLink, inNow, inOutputTime, flagsIn, flagsOut in self.displayLinkCallback( displayLink, inNow: inNow, inOutputTime: inOutputTime, flagsIn: flagsIn, flagsOut: flagsOut ) } } } deinit { self.invalidate() } private func displayLinkCallback(_ link: CVDisplayLink, inNow: UnsafePointer, inOutputTime: UnsafePointer, flagsIn: CVOptionFlags, flagsOut: UnsafeMutablePointer) -> CVReturn { let outputTime = inOutputTime.pointee DispatchQueue.main.async { guard let selector = self.selector, let target = self.target else { return } if outputTime.videoTimeScale != 0 { self.duration = CFTimeInterval(Double(outputTime.videoRefreshPeriod) / Double(outputTime.videoTimeScale)) } if self.timestamp != 0 { for scheduler in self.schedulers { scheduler.key.perform(selector, target: target, argument: nil, order: 0, modes: scheduler.value) } } self.timestamp = CFTimeInterval(Double(outputTime.hostTime) / 1_000_000_000) } return kCVReturnSuccess } var isPaused: Bool = true { didSet { guard let link = link else { return } if isPaused { if CVDisplayLinkIsRunning(link) { CVDisplayLinkStop(link) } } else { if !CVDisplayLinkIsRunning(link) { CVDisplayLinkStart(link) } } } } func add(to runLoop: RunLoop, forMode mode: RunLoop.Mode) { assert(runLoop == .main) schedulers[runLoop, default: []].append(mode) } func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode) { schedulers[runLoop]?.removeAll { $0 == mode } if let modes = schedulers[runLoop], modes.isEmpty { schedulers.removeValue(forKey: runLoop) } } func invalidate() { schedulers = [:] isPaused = true target = nil selector = nil if let link = link { CVDisplayLinkSetOutputHandler(link) { _, _, _, _, _ in kCVReturnSuccess } } } } #endif #endif ================================================ FILE: Sources/Utility/ExtensionHelpers.swift ================================================ // // ExtensionHelpers.swift // Kingfisher // // Created by onevcat on 2018/09/28. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation extension CGFloat { var isEven: Bool { return truncatingRemainder(dividingBy: 2.0) == 0 } } #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit extension NSBezierPath { convenience init(roundedRect rect: NSRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) { self.init() let maxCorner = min(rect.width, rect.height) / 2 let radiusTopLeft = min(maxCorner, max(0, topLeftRadius)) let radiusTopRight = min(maxCorner, max(0, topRightRadius)) let radiusBottomLeft = min(maxCorner, max(0, bottomLeftRadius)) let radiusBottomRight = min(maxCorner, max(0, bottomRightRadius)) guard !rect.isEmpty else { return } let topLeft = NSPoint(x: rect.minX, y: rect.maxY) let topRight = NSPoint(x: rect.maxX, y: rect.maxY) let bottomRight = NSPoint(x: rect.maxX, y: rect.minY) move(to: NSPoint(x: rect.midX, y: rect.maxY)) appendArc(from: topLeft, to: rect.origin, radius: radiusTopLeft) appendArc(from: rect.origin, to: bottomRight, radius: radiusBottomLeft) appendArc(from: bottomRight, to: topRight, radius: radiusBottomRight) appendArc(from: topRight, to: topLeft, radius: radiusTopRight) close() } convenience init(roundedRect rect: NSRect, byRoundingCorners corners: RectCorner, radius: CGFloat) { let radiusTopLeft = corners.contains(.topLeft) ? radius : 0 let radiusTopRight = corners.contains(.topRight) ? radius : 0 let radiusBottomLeft = corners.contains(.bottomLeft) ? radius : 0 let radiusBottomRight = corners.contains(.bottomRight) ? radius : 0 self.init(roundedRect: rect, topLeftRadius: radiusTopLeft, topRightRadius: radiusTopRight, bottomLeftRadius: radiusBottomLeft, bottomRightRadius: radiusBottomRight) } } extension KFCrossPlatformImage { // macOS does not support scale. This is just for code compatibility across platforms. convenience init?(data: Data, scale: CGFloat) { self.init(data: data) } } #endif #if canImport(UIKit) import UIKit extension RectCorner { var uiRectCorner: UIRectCorner { var result: UIRectCorner = [] if contains(.topLeft) { result.insert(.topLeft) } if contains(.topRight) { result.insert(.topRight) } if contains(.bottomLeft) { result.insert(.bottomLeft) } if contains(.bottomRight) { result.insert(.bottomRight) } return result } } #endif extension Date { var isPast: Bool { return isPast(referenceDate: Date()) } func isPast(referenceDate: Date) -> Bool { return timeIntervalSince(referenceDate) <= 0 } // `Date` in memory is a wrap for `TimeInterval`. But in file attribute it can only accept `Int` number. // By default the system will `round` it. But it is not friendly for testing purpose. // So we always `ceil` the value when used for file attributes. var fileAttributeDate: Date { return Date(timeIntervalSince1970: ceil(timeIntervalSince1970)) } } ================================================ FILE: Sources/Utility/Result.swift ================================================ // // Result.swift // Kingfisher // // Created by onevcat on 2018/09/22. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation // These helper methods are not public since we do not want them to be exposed or cause any conflicting. // However, they are just wrapper of `ResultUtil` static methods. extension Result where Failure: Error { /// Evaluates the given transformation closures to create a single output value. /// /// - Parameters: /// - onSuccess: A closure that transforms the success value. /// - onFailure: A closure that transforms the error value. /// - Returns: A single `Output` value. func match( onSuccess: (Success) -> Output, onFailure: (Failure) -> Output) -> Output { switch self { case let .success(value): return onSuccess(value) case let .failure(error): return onFailure(error) } } } ================================================ FILE: Sources/Utility/Runtime.swift ================================================ // // Runtime.swift // Kingfisher // // Created by Wei Wang on 2018/10/12. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation func getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> T? { if #available(iOS 14, macOS 11, watchOS 7, tvOS 14, *) { // swift 5.3 fixed this issue (https://github.com/swiftlang/swift/issues/46456) return objc_getAssociatedObject(object, key) as? T } else { return objc_getAssociatedObject(object, key) as AnyObject as? T } } func setRetainedAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: T) { objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } ================================================ FILE: Sources/Utility/SizeExtensions.swift ================================================ // // SizeExtensions.swift // Kingfisher // // Created by onevcat on 2018/09/28. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import CoreGraphics extension CGSize: KingfisherCompatibleValue {} extension KingfisherWrapper where Base == CGSize { /// Returns a size by resizing the `base` size to a target size under a given content mode. /// /// - Parameters: /// - size: The target size to resize to. /// - contentMode: The content mode of the target size when resizing. /// - Returns: The resized size under the given ``ContentMode``. public func resize(to size: CGSize, for contentMode: ContentMode) -> CGSize { switch contentMode { case .aspectFit: return constrained(size) case .aspectFill: return filling(size) case .none: return size } } /// Returns a size by resizing the `base` size to make it aspect-fit the given `size`. /// /// - Parameter size: The size in which the `base` should fit. /// - Returns: The size that fits the `base` within the input `size`, while keeping the `base` aspect. public func constrained(_ size: CGSize) -> CGSize { let aspectWidth = round(aspectRatio * size.height) let aspectHeight = round(size.width / aspectRatio) return aspectWidth > size.width ? CGSize(width: size.width, height: aspectHeight) : CGSize(width: aspectWidth, height: size.height) } /// Returns a size by resizing the `base` size to make it aspect-fill the given `size`. /// /// - Parameter size: The size that the `base` should fill. /// - Returns: The size filled by the input `size`, while keeping the `base` aspect. public func filling(_ size: CGSize) -> CGSize { let aspectWidth = round(aspectRatio * size.height) let aspectHeight = round(size.width / aspectRatio) return aspectWidth < size.width ? CGSize(width: size.width, height: aspectHeight) : CGSize(width: aspectWidth, height: size.height) } /// Returns a `CGRect` in which the `base` size is constrained to fit within a specified `size`, anchored at a /// particular `anchor` point. /// /// - Parameters: /// - size: The size to which the `base` should be constrained. /// - anchor: The anchor point where the size constraint is applied. /// - Returns: A `CGRect` that results from the constraint operation. public func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect { let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0), y: anchor.y.clamped(to: 0.0...1.0)) let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height let r = CGRect(x: x, y: y, width: size.width, height: size.height) let ori = CGRect(origin: .zero, size: base) return ori.intersection(r) } private var aspectRatio: CGFloat { return base.height == 0.0 ? 1.0 : base.width / base.height } } extension CGRect { func scaled(_ scale: CGFloat) -> CGRect { return CGRect(x: origin.x * scale, y: origin.y * scale, width: size.width * scale, height: size.height * scale) } } extension Comparable { func clamped(to limits: ClosedRange) -> Self { return min(max(self, limits.lowerBound), limits.upperBound) } } ================================================ FILE: Sources/Utility/String+SHA256.swift ================================================ // // String+SHA256.swift // Kingfisher // // Created by kaimaschke on 28.07.23. // // Copyright (c) 2023 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import CryptoKit import CommonCrypto extension String: KingfisherCompatibleValue { } extension KingfisherWrapper where Base == String { var sha256: String { guard let data = base.data(using: .utf8) else { return base } if #available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, macCatalyst 13.0, *) { let hashed = SHA256.hash(data: data) return hashed.compactMap { String(format: "%02x", $0) }.joined() } else { var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) data.withUnsafeBytes { bytes in _ = CC_SHA256(bytes.baseAddress, UInt32(data.count), &digest) } return digest.makeIterator().compactMap { String(format: "%02x", $0) }.joined() } } var ext: String? { guard let firstSeg = base.split(separator: "@").first else { return nil } var ext = "" if let index = firstSeg.lastIndex(of: ".") { let extRange = firstSeg.index(index, offsetBy: 1).. 0 ? ext : nil } } ================================================ FILE: Sources/Views/AnimatedImageView.swift ================================================ // // AnimatedImageView.swift // Kingfisher // // Created by bl4ckra1sond3tre on 4/22/16. // // The AnimatedImageView, AnimatedFrame and Animator is a modified version of // some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu) // // The MIT License (MIT) // // Copyright (c) 2019 Reda Lemeden. // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // The name and characters used in the demo of this software are property of their // respective owners. #if !os(watchOS) #if canImport(UIKit) import UIKit import ImageIO typealias KFCrossPlatformContentMode = UIView.ContentMode #elseif canImport(AppKit) import AppKit typealias KFCrossPlatformContentMode = NSImageScaling #endif /// Delegate of the ``AnimatedImageView``. /// /// It reports back some events of the animated image view. public protocol AnimatedImageViewDelegate: AnyObject { /// Called after the ``AnimatedImageView`` has finished each animation loop. /// /// - Parameters: /// - imageView: The ``AnimatedImageView`` that is being animated. /// - count: The loop count. func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) /// Called after the ``AnimatedImageView`` has reached the maximum repeat count. /// /// - Parameter imageView: The ``AnimatedImageView`` that is being animated. func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) } extension AnimatedImageViewDelegate { public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {} public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {} } let KFRunLoopModeCommon = RunLoop.Mode.common /// Represents a subclass of `UIImageView` for displaying animated images. /// /// Different from showing an animated image in a normal `UIImageView` (which loads all frames at one time), /// ``AnimatedImageView`` only tries to load several frames (defined by ``AnimatedImageView/framePreloadCount``) to /// reduce memory usage. It provides a tradeoff between memory usage and CPU time. If you have a memory issue when /// using a normal image view to load GIF data, you could give this class a try. /// /// Kingfisher supports setting GIF animated data to either `UIImageView` or ``AnimatedImageView`` out of the box. So /// it would be fairly easy to switch between them. open class AnimatedImageView: KFCrossPlatformImageView { /// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`. class TargetProxy { private weak var target: AnimatedImageView? init(target: AnimatedImageView) { self.target = target } @MainActor @objc func onScreenUpdate() { target?.updateFrameIfNeeded() } } /// An enumeration that specifies the repeat count of a GIF. public enum RepeatCount: Equatable { /// The animated image should be only played once. case once /// The animated image should be played by a finite times defined in the associated value. case finite(count: UInt) /// The animated image should be played infinitely. case infinite public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool { switch (lhs, rhs) { case let (.finite(l), .finite(r)): return l == r case (.once, .once), (.infinite, .infinite): return true case (.once, .finite(let count)), (.finite(let count), .once): return count == 1 case (.once, _), (.infinite, _), (.finite, _): return false } } } // MARK: - Public property /// Whether to automatically play the animation when the view becomes visible. /// /// The default is `true`. public var autoPlayAnimatedImage = true /// The count of frames that should be preloaded before being shown. public var framePreloadCount = 10 /// Specifies whether the GIF frames should be pre-scaled to the image view's size or not. /// /// If the downloaded image is larger than the image view's size, it will help reduce some memory usage. /// /// The default is `true`. public var needsPrescaling = true /// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen /// rendering to extract pixel information in background. This can reduce the main thread CPU usage. /// @available(*, deprecated, message: """ This property does not perform as declared and may lead to performance degradation. It is currently obsolete and scheduled for removal in a future version. """) public var backgroundDecode = true /// The animation timer's run loop mode. The default is `RunLoop.Mode.common`. /// /// Setting this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling. public var runLoopMode = KFRunLoopModeCommon { willSet { guard runLoopMode != newValue else { return } stopAnimating() displayLink.remove(from: .main, forMode: runLoopMode) displayLink.add(to: .main, forMode: newValue) startAnimating() } } /// The repeat count. The animated image will keep animating until the loop count reaches this value. /// /// Setting this value to another one will reset the current animation. /// /// The default is ``RepeatCount/infinite``, which means the animation will last forever. public var repeatCount = RepeatCount.infinite { didSet { if oldValue != repeatCount { reset() #if os(macOS) needsDisplay = true layer?.setNeedsDisplay() #else setNeedsDisplay() layer.setNeedsDisplay() #endif } } } /// The delegate of this `AnimatedImageView` object. /// /// See the ``AnimatedImageViewDelegate`` protocol for more information. public weak var delegate: (any AnimatedImageViewDelegate)? /// The ``Animator`` instance that holds the frames of a specific image in memory. public private(set) var animator: Animator? /// Purges decoded frame images from the internal buffer. /// /// By default, this keeps the current frame (to avoid a potential blink) while removing other preloaded frames. /// You can call this manually on any platform. public func purgeFrames(keepCurrentFrame: Bool = true) { animator?.purgeFrames(keepCurrentFrame: keepCurrentFrame) } #if os(iOS) /// Whether the animated frame buffer should be purged when the app enters background. /// /// This is an opt-in behavior to reduce memory footprint when your app is in background. When enabled, /// `AnimatedImageView` stops animating and purges preloaded frames on /// `UIApplication.didEnterBackgroundNotification`. If the view was animating before entering background, it will /// prepare frames and resume animation on `UIApplication.willEnterForegroundNotification`. /// /// Default is `false`. public var purgeFramesOnBackground: Bool = false { didSet { updateBackgroundFramePurgeObserversIfNeeded() } } private var isBackgroundFramePurgeObserversAdded: Bool = false private var shouldResumeAnimationAfterForeground: Bool = false #endif // MARK: - Private property // Dispatch queue used for preloading images. private lazy var preloadQueue: DispatchQueue = { return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") }() // A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. private var isDisplayLinkInitialized: Bool = false // A display link that keeps calling the `updateFrame` method on every screen refresh. private lazy var displayLink: any DisplayLinkCompatible = { isDisplayLinkInitialized = true let displayLink = self.compatibleDisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate)) displayLink.add(to: .main, forMode: runLoopMode) displayLink.isPaused = true return displayLink }() // MARK: - Override @MainActor override open var image: KFCrossPlatformImage? { didSet { if image != oldValue { reset() } #if os(macOS) needsDisplay = true layer?.setNeedsDisplay() #else setNeedsDisplay() layer.setNeedsDisplay() #endif } } open override var isHighlighted: Bool { get { super.isHighlighted } set { // Highlighted image is unsupported for animated images. // See https://github.com/onevcat/Kingfisher/issues/1679 if displayLink.isPaused { super.isHighlighted = newValue } } } // Workaround for Apple xcframework creating issue on Apple TV in Swift 5.8. // https://github.com/swiftlang/swift/issues/66015 #if os(tvOS) public override init(image: UIImage?, highlightedImage: UIImage?) { super.init(image: image, highlightedImage: highlightedImage) } required public init?(coder: NSCoder) { super.init(coder: coder) } init() { super.init(frame: .zero) } #endif deinit { // `@MainActor deinit` requires isolated deinit support and broke older Swift 6 toolchains. // Keep a single code path that assumes UIKit/AppKit deallocation on the main thread. MainActor.runUnsafely { #if os(iOS) removeBackgroundFramePurgeObservers() #endif if isDisplayLinkInitialized { displayLink.invalidate() } } } #if os(macOS) public override init(frame frameRect: NSRect) { super.init(frame: frameRect) commonInit() } public required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { super.animates = false wantsLayer = true } open override var animates: Bool { get { if isDisplayLinkInitialized { return !displayLink.isPaused } else { return super.animates } } set { if newValue { startAnimating() } else { stopAnimating() } } } open func startAnimating() { guard let animator = animator else { return } guard !animator.isReachMaxRepeatCount else { return } displayLink.isPaused = false } open func stopAnimating() { if isDisplayLinkInitialized { displayLink.isPaused = true } } open override var wantsUpdateLayer: Bool { return true } open override func updateLayer() { if let frame = animator?.currentFrameImage ?? currentFrame, let layer = layer { layer.contents = frame.kf.cgImage layer.contentsScale = frame.kf.scale layer.contentsGravity = determineContentsGravity(for: frame) currentFrame = frame } } private func determineContentsGravity(for image: NSImage) -> CALayerContentsGravity { switch imageScaling { case .scaleProportionallyDown: if image.size.width > bounds.width || image.size.height > bounds.height { return .resizeAspect } else { return .center } case .scaleProportionallyUpOrDown: return .resizeAspect case .scaleAxesIndependently: return .resize case .scaleNone: return .center default: return .resizeAspect } } open override func viewDidMoveToWindow() { super.viewDidMoveToWindow() didMove() } open override func viewDidMoveToSuperview() { super.viewDidMoveToSuperview() didMove() } #else override open var isAnimating: Bool { if isDisplayLinkInitialized { return !displayLink.isPaused } else { return super.isAnimating } } override open func startAnimating() { guard !isAnimating else { return } guard let animator = animator else { return } guard !animator.isReachMaxRepeatCount else { return } displayLink.isPaused = false } override open func stopAnimating() { super.stopAnimating() if isDisplayLinkInitialized { displayLink.isPaused = true } } override open func display(_ layer: CALayer) { layer.contents = animator?.currentFrameImage?.cgImage ?? image?.cgImage } override open func didMoveToWindow() { super.didMoveToWindow() didMove() } override open func didMoveToSuperview() { super.didMoveToSuperview() didMove() } #endif // This is for back compatibility that using regular `UIImageView` to show animated image. override func shouldPreloadAllAnimation() -> Bool { return false } // Reset the animator. private func reset() { animator = nil currentFrame = nil if let image = image, let frameSource = image.kf.frameSource { #if os(visionOS) let scale = UITraitCollection.current.displayScale #elseif os(macOS) let scale = image.recommendedLayerContentsScale(window?.backingScaleFactor ?? 0.0) let contentMode = imageScaling #else var scale: CGFloat = 0 if #available(iOS 13.0, tvOS 13.0, *) { scale = UITraitCollection.current.displayScale } else { scale = UIScreen.main.scale } #endif currentFrame = image let targetSize = bounds.scaled(scale).size let animator = Animator( frameSource: frameSource, contentMode: contentMode, size: targetSize, imageSize: image.kf.size, imageScale: image.kf.scale, framePreloadCount: framePreloadCount, repeatCount: repeatCount, preloadQueue: preloadQueue) animator.delegate = self animator.needsPrescaling = needsPrescaling animator.prepareFramesAsynchronously() self.animator = animator } didMove() } private func didMove() { if autoPlayAnimatedImage && animator != nil { if let _ = superview, let _ = window { startAnimating() } else { stopAnimating() } } } #if os(iOS) private func updateBackgroundFramePurgeObserversIfNeeded() { if purgeFramesOnBackground { guard !isBackgroundFramePurgeObserversAdded else { return } isBackgroundFramePurgeObserversAdded = true let center = NotificationCenter.default center.addObserver( self, selector: #selector(didEnterBackgroundNotification(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil ) center.addObserver( self, selector: #selector(willEnterForegroundNotification(_:)), name: UIApplication.willEnterForegroundNotification, object: nil ) } else { removeBackgroundFramePurgeObservers() } } private func removeBackgroundFramePurgeObservers() { guard isBackgroundFramePurgeObserversAdded else { return } let center = NotificationCenter.default center.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) center.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) isBackgroundFramePurgeObserversAdded = false } @objc private func didEnterBackgroundNotification(_ notification: Notification) { handleDidEnterBackground() } @objc private func willEnterForegroundNotification(_ notification: Notification) { handleWillEnterForeground() } private func handleDidEnterBackground() { guard purgeFramesOnBackground else { return } shouldResumeAnimationAfterForeground = isAnimating stopAnimating() purgeFrames(keepCurrentFrame: true) } private func handleWillEnterForeground() { guard purgeFramesOnBackground else { return } guard shouldResumeAnimationAfterForeground else { return } shouldResumeAnimationAfterForeground = false guard animator != nil, superview != nil, window != nil else { return } animator?.prepareFramesAsynchronously() startAnimating() } #endif /// If the Animator cannot prepare the next frame in time, `animator.currentFrameImage` will return nil. /// To prevent unexpected blinking in the ImageView, we maintain a cache of the currently displayed frame /// to use as a fallback in such scenarios. private var currentFrame: KFCrossPlatformImage? /// Update the current frame with the displayLink duration. @MainActor private func updateFrameIfNeeded() { guard let animator = animator else { return } guard !animator.isFinished else { stopAnimating() delegate?.animatedImageViewDidFinishAnimating(self) return } let duration: CFTimeInterval // CA based display link is opt-out from ProMotion by default. // So the duration and its FPS might not match. // See [#718](https://github.com/onevcat/Kingfisher/issues/718) // By setting CADisableMinimumFrameDuration to YES in Info.plist may // cause the preferredFramesPerSecond being 0 let preferredFramesPerSecond = displayLink.preferredFramesPerSecond if preferredFramesPerSecond == 0 { duration = displayLink.duration } else { // Some devices (like iPad Pro 10.5) will have a different FPS. duration = 1.0 / TimeInterval(preferredFramesPerSecond) } if animator.shouldChangeFrame(with: duration) { #if os(macOS) layer?.setNeedsDisplay() #else layer.setNeedsDisplay() #endif } } } @MainActor protocol AnimatorDelegate: AnyObject { func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt) } extension AnimatedImageView: AnimatorDelegate { func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) { delegate?.animatedImageView(self, didPlayAnimationLoops: count) } } extension AnimatedImageView { // Represents a single frame in a GIF. struct AnimatedFrame { // The image to display for this frame. Its value is nil when the frame is removed from the buffer. let image: KFCrossPlatformImage? // The duration that this frame should remain active. let duration: TimeInterval // A placeholder frame with no image assigned. // Used to replace frames that are no longer needed in the animation. var placeholderFrame: AnimatedFrame { return AnimatedFrame(image: nil, duration: duration) } // Whether this frame instance contains an image or not. var isPlaceholder: Bool { return image == nil } // Returns a new instance from an optional image. // // - parameter image: An optional `UIImage` instance to be assigned to the new frame. // - returns: An `AnimatedFrame` instance. func makeAnimatedFrame(image: KFCrossPlatformImage?) -> AnimatedFrame { return AnimatedFrame(image: image, duration: duration) } } } extension AnimatedImageView { // MARK: - Animator // TODO: Check the thread-safety of `Animator` for Sendable again. /// An animator which is used to drive the data behind ``AnimatedImageView``. public class Animator: @unchecked Sendable { private let size: CGSize private let imageSize: CGSize private let imageScale: CGFloat /// The maximum count of image frames that need to be preloaded. public let maxFrameCount: Int private let frameSource: any ImageFrameSource private let maxRepeatCount: RepeatCount private let maxTimeStep: TimeInterval = 1.0 private let animatedFrames = SafeArray() private var frameCount = 0 private var timeSinceLastFrameChange: TimeInterval = 0.0 private var currentRepeatCount: UInt = 0 var isFinished: Bool = false var needsPrescaling = true weak var delegate: (any AnimatorDelegate)? // Total duration of one animation loop var loopDuration: TimeInterval = 0 /// The image of the current frame. public var currentFrameImage: KFCrossPlatformImage? { return frame(at: currentFrameIndex) } /// The duration of the current active frame. public var currentFrameDuration: TimeInterval { return duration(at: currentFrameIndex) } /// The index of the current animation frame. public internal(set) var currentFrameIndex = 0 { didSet { previousFrameIndex = oldValue } } var previousFrameIndex = 0 { didSet { preloadQueue.async { self.updatePreloadedFrames() } } } var isReachMaxRepeatCount: Bool { switch maxRepeatCount { case .once: return currentRepeatCount >= 1 case .finite(let maxCount): return currentRepeatCount >= maxCount case .infinite: return false } } /// Whether the current frame is the last frame or not in the animation sequence. public var isLastFrame: Bool { return currentFrameIndex == frameCount - 1 } var preloadingIsNeeded: Bool { return maxFrameCount < frameCount - 1 } #if os(macOS) var contentMode = NSImageScaling.scaleAxesIndependently #else var contentMode = UIView.ContentMode.scaleToFill #endif private lazy var preloadQueue: DispatchQueue = { return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") }() /// Creates an animator with image source reference. /// /// - Parameters: /// - source: The reference of animated image. /// - mode: Content mode of the `AnimatedImageView`. /// - size: Size of the `AnimatedImageView`. /// - imageSize: Size of the `KingfisherWrapper`. /// - imageScale: Scale of the `KingfisherWrapper`. /// - count: Count of frames needed to be preloaded. /// - repeatCount: The repeat count should this animator uses. /// - preloadQueue: Dispatch queue used for preloading images. convenience init(imageSource source: CGImageSource, contentMode mode: KFCrossPlatformContentMode, size: CGSize, imageSize: CGSize, imageScale: CGFloat, framePreloadCount count: Int, repeatCount: RepeatCount, preloadQueue: DispatchQueue) { let frameSource = CGImageFrameSource(data: nil, imageSource: source, options: nil) self.init(frameSource: frameSource, contentMode: mode, size: size, imageSize: imageSize, imageScale: imageScale, framePreloadCount: count, repeatCount: repeatCount, preloadQueue: preloadQueue) } /// Creates an animator with a custom image frame source. /// /// - Parameters: /// - frameSource: The reference of animated image. /// - mode: Content mode of the `AnimatedImageView`. /// - size: Size of the `AnimatedImageView`. /// - imageSize: Size of the `KingfisherWrapper`. /// - imageScale: Scale of the `KingfisherWrapper`. /// - count: Count of frames needed to be preloaded. /// - repeatCount: The repeat count should this animator uses. /// - preloadQueue: Dispatch queue used for preloading images. init(frameSource source: any ImageFrameSource, contentMode mode: KFCrossPlatformContentMode, size: CGSize, imageSize: CGSize, imageScale: CGFloat, framePreloadCount count: Int, repeatCount: RepeatCount, preloadQueue: DispatchQueue) { self.frameSource = source.copy() self.contentMode = mode self.size = size self.imageSize = imageSize self.imageScale = imageScale self.maxFrameCount = count self.maxRepeatCount = repeatCount self.preloadQueue = preloadQueue } /// Gets the image frame of a given index. /// - Parameter index: The index of the desired image. /// - Returns: The decoded image at the frame. `nil` if the index is out of bounds or the image is not yet loaded. public func frame(at index: Int) -> KFCrossPlatformImage? { return animatedFrames[index]?.image } /// Gets the duration of an image for the given frame index. /// - Parameter index: The index of the desired image. /// - Returns: The duration of that frame. public func duration(at index: Int) -> TimeInterval { return animatedFrames[index]?.duration ?? .infinity } func prepareFramesAsynchronously() { frameCount = frameSource.frameCount animatedFrames.reserveCapacity(frameCount) preloadQueue.async { [weak self] in self?.setupAnimatedFrames() } } func purgeFrames(keepCurrentFrame: Bool = true) { preloadQueue.async { [weak self] in guard let self else { return } guard self.frameCount > 0, self.animatedFrames.count > 0 else { return } let keepIndex = keepCurrentFrame ? self.currentFrameIndex : nil var imagesToRelease: [KFCrossPlatformImage] = [] for index in 0.. Bool { incrementTimeSinceLastFrameChange(with: duration) if currentFrameDuration > timeSinceLastFrameChange { return false } else { resetTimeSinceLastFrameChange() incrementCurrentFrameIndex() return true } } private func setupAnimatedFrames() { resetAnimatedFrames() var duration: TimeInterval = 0 (0.. maxFrameCount { return } animatedFrames[index] = animatedFrames[index]?.makeAnimatedFrame(image: loadFrame(at: index)) } self.loopDuration = duration } private func resetAnimatedFrames() { animatedFrames.removeAll() } private func loadFrame(at index: Int) -> KFCrossPlatformImage? { let resize = needsPrescaling && size != .zero let maxSize = resize ? size : nil guard let cgImage = frameSource.frame(at: index, maxSize: maxSize) else { return nil } #if os(macOS) return KFCrossPlatformImage(cgImage: cgImage, size: .zero) #else if #available(iOS 15, tvOS 15, *) { // From iOS 15, a plain image loading causes iOS calling `-[_UIImageCGImageContent initWithCGImage:scale:]` // in ImageIO, which holds the image ref on the creating thread. // To get a workaround, create another image ref and use that to create the final image. This leads to // some performance loss, but there is little we can do. // https://github.com/onevcat/Kingfisher/issues/1844 // https://github.com/onevcat/Kingfisher/pulls/2194 guard let unretainedImage = CGImage.create(ref: cgImage) else { return KFCrossPlatformImage(cgImage: cgImage) } return KFCrossPlatformImage(cgImage: unretainedImage).preparingForDisplay() } else { return KFCrossPlatformImage(cgImage: cgImage) } #endif } private func updatePreloadedFrames() { guard preloadingIsNeeded else { return } let previousFrame = animatedFrames[previousFrameIndex] animatedFrames[previousFrameIndex] = previousFrame?.placeholderFrame // ensure the image dealloc in main thread defer { if let image = previousFrame?.image { DispatchQueue.main.async { _ = image } } } preloadIndexes(start: currentFrameIndex).forEach { index in guard let currentAnimatedFrame = animatedFrames[index] else { return } if !currentAnimatedFrame.isPlaceholder { return } animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index)) } } @MainActor private func incrementCurrentFrameIndex() { let wasLastFrame = isLastFrame currentFrameIndex = increment(frameIndex: currentFrameIndex) if isLastFrame { currentRepeatCount += 1 if isReachMaxRepeatCount { isFinished = true // Notify the delegate here because the animation is stopping. delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) } } else if wasLastFrame { // Notify the delegate that the loop completed delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) } } private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) { timeSinceLastFrameChange += min(maxTimeStep, duration) } private func resetTimeSinceLastFrameChange() { timeSinceLastFrameChange -= currentFrameDuration } private func increment(frameIndex: Int, by value: Int = 1) -> Int { return (frameIndex + value) % frameCount } private func preloadIndexes(start index: Int) -> [Int] { let nextIndex = increment(frameIndex: index) let lastIndex = increment(frameIndex: index, by: maxFrameCount) if lastIndex >= nextIndex { return [Int](nextIndex...lastIndex) } else { return [Int](nextIndex.. { private var array: Array = [] private let lock = NSLock() subscript(index: Int) -> Element? { get { lock.lock() defer { lock.unlock() } return array.indices ~= index ? array[index] : nil } set { lock.lock() defer { lock.unlock() } if let newValue = newValue, array.indices ~= index { array[index] = newValue } } } var count : Int { lock.lock() defer { lock.unlock() } return array.count } func reserveCapacity(_ count: Int) { lock.lock() defer { lock.unlock() } array.reserveCapacity(count) } func append(_ element: Element) { lock.lock() defer { lock.unlock() } array += [element] } func removeAll() { lock.lock() defer { lock.unlock() } array = [] } } #endif ================================================ FILE: Sources/Views/Indicator.swift ================================================ // // Indicator.swift // Kingfisher // // Created by João D. Moreira on 30/08/16. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if !os(watchOS) #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit public typealias IndicatorView = NSView #else import UIKit public typealias IndicatorView = UIView #endif /// Represents the activity indicator type that should be added to an image view when an image is being downloaded. public enum IndicatorType { /// No indicator. case none /// Uses the system activity indicator. case activity /// Uses an image as an indicator. GIF is supported. case image(imageData: Data) /// Uses a custom indicator. /// /// The type of the associated value should conform to the ``Indicator`` protocol. case custom(indicator: any Indicator) } /// An indicator type which can be used to show that the download task is in progress. @MainActor public protocol Indicator: Sendable { /// Called when the indicator should start animating. func startAnimatingView() /// Called when the indicator should stop animating. func stopAnimatingView() /// Center offset of the indicator. /// /// Kingfisher will use this value to determine the position of the indicator in the superview. var centerOffset: CGPoint { get } /// The indicator view which would be added to the superview. var view: IndicatorView { get } /// The size strategy used when adding the indicator to the image view. /// - Parameter imageView: The superview of the indicator. /// - Returns: An ``IndicatorSizeStrategy`` that determines how the indicator should be sized. func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy } /// The idicator size strategy used when sizing the indicator in the image view. public enum IndicatorSizeStrategy { /// Uses the intrinsic size of the indicator. case intrinsicSize /// Match the size of the super view of the indicator. case full /// Uses the associated `CGSize` to set the indicator size. case size(CGSize) } extension Indicator { /// Default implementation of ``Indicator/centerOffset-7jxdw`` of the ``Indicator``. /// /// The default value is `.zero`, which means that there is no offset for the indicator view. public var centerOffset: CGPoint { .zero } /// Default implementation of ``Indicator/sizeStrategy(in:)-5x0b4`` of the ``Indicator``. /// /// The default value is ``IndicatorSizeStrategy/full``, means that the indicator will pin to the same height and /// width as the image view. /// - Parameter imageView: The image view which holds the indicator. /// - Returns: The desired ``IndicatorSizeStrategy`` public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { .full } } // Displays a NSProgressIndicator / UIActivityIndicatorView @MainActor final class ActivityIndicator: Indicator { #if os(macOS) private let activityIndicatorView: NSProgressIndicator #else private let activityIndicatorView: UIActivityIndicatorView #endif private var animatingCount = 0 var view: IndicatorView { return activityIndicatorView } func startAnimatingView() { if animatingCount == 0 { #if os(macOS) activityIndicatorView.startAnimation(nil) #else activityIndicatorView.startAnimating() #endif activityIndicatorView.isHidden = false } animatingCount += 1 } func stopAnimatingView() { animatingCount = max(animatingCount - 1, 0) if animatingCount == 0 { #if os(macOS) activityIndicatorView.stopAnimation(nil) #else activityIndicatorView.stopAnimating() #endif activityIndicatorView.isHidden = true } } func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { return .intrinsicSize } init() { #if os(macOS) activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) activityIndicatorView.controlSize = .regular activityIndicatorView.style = .spinning #else let indicatorStyle: UIActivityIndicatorView.Style #if os(tvOS) if #available(tvOS 13.0, *) { indicatorStyle = UIActivityIndicatorView.Style.large } else { indicatorStyle = UIActivityIndicatorView.Style.white } #elseif os(visionOS) indicatorStyle = UIActivityIndicatorView.Style.medium #else if #available(iOS 13.0, * ) { indicatorStyle = UIActivityIndicatorView.Style.medium } else { indicatorStyle = UIActivityIndicatorView.Style.gray } #endif activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle) #endif } } #if canImport(UIKit) extension UIActivityIndicatorView.Style { #if compiler(>=5.1) #else static let large = UIActivityIndicatorView.Style.white #if !os(tvOS) static let medium = UIActivityIndicatorView.Style.gray #endif #endif } #endif // MARK: - ImageIndicator // Displays an ImageView. Supports gif final class ImageIndicator: Indicator { private let animatedImageIndicatorView: KFCrossPlatformImageView var view: IndicatorView { return animatedImageIndicatorView } init?( imageData data: Data, processor: any ImageProcessor = DefaultImageProcessor.default, options: KingfisherParsedOptionsInfo? = nil) { var options = options ?? KingfisherParsedOptionsInfo(nil) // Use normal image view to show animations, so we need to preload all animation data. if !options.preloadAllAnimationData { options.preloadAllAnimationData = true } guard let image = processor.process(item: .data(data), options: options) else { return nil } animatedImageIndicatorView = KFCrossPlatformImageView() animatedImageIndicatorView.image = image #if os(macOS) // Need for gif to animate on macOS animatedImageIndicatorView.imageScaling = .scaleNone animatedImageIndicatorView.canDrawSubviewsIntoLayer = true #else animatedImageIndicatorView.contentMode = .center #endif } func startAnimatingView() { #if os(macOS) animatedImageIndicatorView.animates = true #else animatedImageIndicatorView.startAnimating() #endif animatedImageIndicatorView.isHidden = false } func stopAnimatingView() { #if os(macOS) animatedImageIndicatorView.animates = false #else animatedImageIndicatorView.stopAnimating() #endif animatedImageIndicatorView.isHidden = true } } #endif ================================================ FILE: Tests/Dependency/Nocilla/LICENSE ================================================ Copyright (c) 2012 Luis Solano Bonet MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Categories/NSData+Nocilla.h ================================================ #import #import "LSHTTPBody.h" @interface NSData (Nocilla) @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Categories/NSData+Nocilla.m ================================================ #import "NSData+Nocilla.h" @implementation NSData (Nocilla) - (NSData *)data { return self; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Categories/NSString+Nocilla.h ================================================ #import #import "LSHTTPBody.h" @interface NSString (Nocilla) - (NSRegularExpression *)regex; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Categories/NSString+Nocilla.m ================================================ #import "NSString+Nocilla.h" @implementation NSString (Nocilla) - (NSRegularExpression *)regex { NSError *error = nil; NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:self options:0 error:&error]; if (error) { [NSException raise:NSInvalidArgumentException format:@"Invalid regex pattern: %@\nError: %@", self, error]; } return regex; } - (NSData *)data { return [self dataUsingEncoding:NSUTF8StringEncoding]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/DSL/LSHTTPRequestDSLRepresentation.h ================================================ #import #import "LSHTTPRequest.h" @interface LSHTTPRequestDSLRepresentation : NSObject - (id)initWithRequest:(id)request; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/DSL/LSHTTPRequestDSLRepresentation.m ================================================ #import "LSHTTPRequestDSLRepresentation.h" @interface LSHTTPRequestDSLRepresentation () @property (nonatomic, strong) id request; @end @implementation LSHTTPRequestDSLRepresentation - (id)initWithRequest:(id)request { self = [super init]; if (self) { _request = request; } return self; } - (NSString *)description { NSMutableString *result = [NSMutableString stringWithFormat:@"stubRequest(@\"%@\", @\"%@\")", self.request.method, [self.request.url absoluteString]]; if (self.request.headers.count) { [result appendString:@".\nwithHeaders(@{ "]; NSMutableArray *headerElements = [NSMutableArray arrayWithCapacity:self.request.headers.count]; NSArray *descriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"" ascending:YES]]; NSArray * sortedHeaders = [[self.request.headers allKeys] sortedArrayUsingDescriptors:descriptors]; for (NSString * header in sortedHeaders) { NSString *value = [self.request.headers objectForKey:header]; [headerElements addObject:[NSString stringWithFormat:@"@\"%@\": @\"%@\"", header, value]]; } [result appendString:[headerElements componentsJoinedByString:@", "]]; [result appendString:@" })"]; } if (self.request.body.length) { NSString *escapedBody = [[NSString alloc] initWithData:self.request.body encoding:NSUTF8StringEncoding]; escapedBody = [escapedBody stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; [result appendFormat:@".\nwithBody(@\"%@\")", escapedBody]; } return [NSString stringWithFormat:@"%@;", result]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/DSL/LSStubRequestDSL.h ================================================ #import #import "NSString+Matcheable.h" #import "NSRegularExpression+Matcheable.h" #import "NSData+Matcheable.h" @class LSStubRequestDSL; @class LSStubResponseDSL; @class LSStubRequest; @protocol LSHTTPBody; typedef LSStubRequestDSL *(^WithHeaderMethod)(NSString *, NSString *); typedef LSStubRequestDSL *(^WithHeadersMethod)(NSDictionary *); typedef LSStubRequestDSL *(^AndBodyMethod)(id); typedef LSStubResponseDSL *(^AndReturnMethod)(NSInteger); typedef LSStubResponseDSL *(^AndReturnRawResponseMethod)(NSData *rawResponseData); typedef void (^AndFailWithErrorMethod)(NSError *error); @interface LSStubRequestDSL : NSObject - (id)initWithRequest:(LSStubRequest *)request; @property (nonatomic, strong, readonly) WithHeaderMethod withHeader; @property (nonatomic, strong, readonly) WithHeadersMethod withHeaders; @property (nonatomic, strong, readonly) AndBodyMethod withBody; @property (nonatomic, strong, readonly) AndReturnMethod andReturn; @property (nonatomic, strong, readonly) AndReturnRawResponseMethod andReturnRawResponse; @property (nonatomic, strong, readonly) AndFailWithErrorMethod andFailWithError; @end #ifdef __cplusplus extern "C" { #endif LSStubRequestDSL * stubRequest(NSString *method, id url); #ifdef __cplusplus } #endif ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/DSL/LSStubRequestDSL.m ================================================ #import "LSStubRequestDSL.h" #import "LSStubResponseDSL.h" #import "LSStubRequest.h" #import "LSNocilla.h" @interface LSStubRequestDSL () @property (nonatomic, strong) LSStubRequest *request; @end @implementation LSStubRequestDSL - (id)initWithRequest:(LSStubRequest *)request { self = [super init]; if (self) { _request = request; } return self; } - (WithHeadersMethod)withHeaders { return ^(NSDictionary *headers) { for (NSString *header in headers) { NSString *value = [headers objectForKey:header]; [self.request setHeader:header value:value]; } return self; }; } - (WithHeaderMethod)withHeader { return ^(NSString * header, NSString * value) { [self.request setHeader:header value:value]; return self; }; } - (AndBodyMethod)withBody { return ^(id body) { self.request.body = body.matcher; return self; }; } - (AndReturnMethod)andReturn { return ^(NSInteger statusCode) { self.request.response = [[LSStubResponse alloc] initWithStatusCode:statusCode]; LSStubResponseDSL *responseDSL = [[LSStubResponseDSL alloc] initWithResponse:self.request.response]; return responseDSL; }; } - (AndReturnRawResponseMethod)andReturnRawResponse { return ^(NSData *rawResponseData) { self.request.response = [[LSStubResponse alloc] initWithRawResponse:rawResponseData]; LSStubResponseDSL *responseDSL = [[LSStubResponseDSL alloc] initWithResponse:self.request.response]; return responseDSL; }; } - (AndFailWithErrorMethod)andFailWithError { return ^(NSError *error) { self.request.response = [[LSStubResponse alloc] initWithError:error]; }; } @end LSStubRequestDSL * stubRequest(NSString *method, id url) { LSStubRequest *request = [[LSStubRequest alloc] initWithMethod:method urlMatcher:url.matcher]; LSStubRequestDSL *dsl = [[LSStubRequestDSL alloc] initWithRequest:request]; [[LSNocilla sharedInstance] addStubbedRequest:request]; return dsl; } ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/DSL/LSStubResponseDSL.h ================================================ #import @class LSStubResponse; @class LSStubResponseDSL; @protocol LSHTTPBody; typedef LSStubResponseDSL *(^ResponseWithBodyMethod)(id); typedef LSStubResponseDSL *(^ResponseWithHeaderMethod)(NSString *, NSString *); typedef LSStubResponseDSL *(^ResponseWithHeadersMethod)(NSDictionary *); typedef LSStubResponseDSL *(^ResponseVoidMethod)(void); @interface LSStubResponseDSL : NSObject - (id)initWithResponse:(LSStubResponse *)response; @property (nonatomic, strong, readonly) ResponseWithHeaderMethod withHeader; @property (nonatomic, strong, readonly) ResponseWithHeadersMethod withHeaders; @property (nonatomic, strong, readonly) ResponseWithBodyMethod withBody; @property (nonatomic, strong, readonly) ResponseVoidMethod delay; @property (nonatomic, strong, readonly) ResponseVoidMethod go; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/DSL/LSStubResponseDSL.m ================================================ #import "LSStubResponseDSL.h" #import "LSStubResponse.h" #import "LSHTTPBody.h" @interface LSStubResponseDSL () @property (nonatomic, strong) LSStubResponse *response; @end @implementation LSStubResponseDSL - (id)initWithResponse:(LSStubResponse *)response { self = [super init]; if (self) { _response = response; } return self; } - (ResponseWithHeaderMethod)withHeader { return ^(NSString * header, NSString * value) { [self.response setHeader:header value:value]; return self; }; } - (ResponseWithHeadersMethod)withHeaders; { return ^(NSDictionary *headers) { for (NSString *header in headers) { NSString *value = [headers objectForKey:header]; [self.response setHeader:header value:value]; } return self; }; } - (ResponseWithBodyMethod)withBody { return ^(id body) { self.response.body = [body data]; return self; }; } - (ResponseVoidMethod)delay { return ^{ [self.response delay]; return self; }; } - (ResponseVoidMethod)go { return ^{ [self.response go]; return self; }; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Diff/LSHTTPRequestDiff.h ================================================ #import #import "LSHTTPRequest.h" @interface LSHTTPRequestDiff : NSObject @property (nonatomic, assign, readonly, getter = isEmpty) BOOL empty; - (id)initWithRequest:(id)oneRequest andRequest:(id)anotherRequest; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Diff/LSHTTPRequestDiff.m ================================================ #import "LSHTTPRequestDiff.h" @interface LSHTTPRequestDiff () @property (nonatomic, strong) idoneRequest; @property (nonatomic, strong) idanotherRequest; - (BOOL)isMethodDifferent; - (BOOL)isUrlDifferent; - (BOOL)areHeadersDifferent; - (BOOL)isBodyDifferent; - (void)appendMethodDiff:(NSMutableString *)diff; - (void)appendUrlDiff:(NSMutableString *)diff; - (void)appendHeadersDiff:(NSMutableString *)diff; - (void)appendBodyDiff:(NSMutableString *)diff; @end @implementation LSHTTPRequestDiff - (id)initWithRequest:(id)oneRequest andRequest:(id)anotherRequest { self = [super init]; if (self) { _oneRequest = oneRequest; _anotherRequest = anotherRequest; } return self; } - (BOOL)isEmpty { if ([self isMethodDifferent] || [self isUrlDifferent] || [self areHeadersDifferent] || [self isBodyDifferent]) { return NO; } return YES; } - (NSString *)description { NSMutableString *diff = [@"" mutableCopy]; if ([self isMethodDifferent]) { [self appendMethodDiff:diff]; } if ([self isUrlDifferent]) { [self appendUrlDiff:diff]; } if([self areHeadersDifferent]) { [self appendHeadersDiff:diff]; } if([self isBodyDifferent]) { [self appendBodyDiff:diff]; } return [NSString stringWithString:diff]; } #pragma mark - Private Methods - (BOOL)isMethodDifferent { return ![self.oneRequest.method isEqualToString:self.anotherRequest.method]; } - (BOOL)isUrlDifferent { return ![self.oneRequest.url isEqual:self.anotherRequest.url]; } - (BOOL)areHeadersDifferent { return ![self.oneRequest.headers isEqual:self.anotherRequest.headers]; } - (BOOL)isBodyDifferent { return (((self.oneRequest.body) && (![self.oneRequest.body isEqual:self.anotherRequest.body])) || ((self.anotherRequest.body) && (![self.anotherRequest.body isEqual:self.oneRequest.body]))); } - (void)appendMethodDiff:(NSMutableString *)diff { [diff appendFormat:@"- Method: %@\n+ Method: %@\n", self.oneRequest.method, self.anotherRequest.method]; } - (void)appendUrlDiff:(NSMutableString *)diff { [diff appendFormat:@"- URL: %@\n+ URL: %@\n", [self.oneRequest.url absoluteString], [self.anotherRequest.url absoluteString]]; } - (void)appendHeadersDiff:(NSMutableString *)diff { [diff appendString:@" Headers:\n"]; NSSet *headersInOneButNotInTheOther = [self.oneRequest.headers keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { return ![self.anotherRequest.headers objectForKey:key] || ![obj isEqual:[self.anotherRequest.headers objectForKey:key]]; }]; NSSet *headersInTheOtherButNotInOne = [self.anotherRequest.headers keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { return ![self.oneRequest.headers objectForKey:key] || ![obj isEqual:[self.oneRequest.headers objectForKey:key]]; }]; NSArray *descriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"" ascending:YES]]; NSArray * sortedHeadersInOneButNotInTheOther = [headersInOneButNotInTheOther sortedArrayUsingDescriptors:descriptors]; NSArray * sortedHeadersInTheOtherButNotInOne = [headersInTheOtherButNotInOne sortedArrayUsingDescriptors:descriptors]; for (NSString *header in sortedHeadersInOneButNotInTheOther) { NSString *value = [self.oneRequest.headers objectForKey:header]; [diff appendFormat:@"-\t\"%@\": \"%@\"\n", header, value]; } for (NSString *header in sortedHeadersInTheOtherButNotInOne) { NSString *value = [self.anotherRequest.headers objectForKey:header]; [diff appendFormat:@"+\t\"%@\": \"%@\"\n", header, value]; } } - (void)appendBodyDiff:(NSMutableString *)diff { NSString *oneBody = [[NSString alloc] initWithData:self.oneRequest.body encoding:NSUTF8StringEncoding]; if (oneBody.length) { [diff appendFormat:@"- Body: \"%@\"\n", oneBody]; } NSString *anotherBody = [[NSString alloc] initWithData:self.anotherRequest.body encoding:NSUTF8StringEncoding]; if (anotherBody.length) { [diff appendFormat:@"+ Body: \"%@\"\n", anotherBody]; } } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/ASIHTTPRequestStub.h ================================================ #import @interface ASIHTTPRequestStub : NSObject - (int)stub_responseStatusCode; - (NSData *)stub_responseData; - (NSDictionary *)stub_responseHeaders; - (void)stub_startRequest; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/ASIHTTPRequestStub.m ================================================ #import "ASIHTTPRequestStub.h" #import "LSStubResponse.h" #import "LSNocilla.h" #import "LSASIHTTPRequestAdapter.h" #import @interface ASIHTTPRequestStub () @property (nonatomic, strong) LSStubResponse *stubResponse; @end @interface ASIHTTPRequestStub (Private) - (void)failWithError:(NSError *)error; - (void)requestFinished; - (void)markAsFinished; @end static void const * ASIHTTPRequestStubResponseKey = &ASIHTTPRequestStubResponseKey; @implementation ASIHTTPRequestStub - (void)setStubResponse:(LSStubResponse *)stubResponse { objc_setAssociatedObject(self, ASIHTTPRequestStubResponseKey, stubResponse, OBJC_ASSOCIATION_RETAIN); } - (LSStubResponse *)stubResponse { return objc_getAssociatedObject(self, ASIHTTPRequestStubResponseKey); } - (int)stub_responseStatusCode { return (int)self.stubResponse.statusCode; } - (NSData *)stub_responseData { return self.stubResponse.body; } - (NSDictionary *)stub_responseHeaders { return self.stubResponse.headers; } - (void)stub_startRequest { self.stubResponse = [[LSNocilla sharedInstance] responseForRequest:[[LSASIHTTPRequestAdapter alloc] initWithASIHTTPRequest:(id)self]]; if (self.stubResponse.shouldFail) { [self failWithError:self.stubResponse.error]; } else { [self requestFinished]; } [self markAsFinished]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/LSASIHTTPRequestAdapter.h ================================================ #import #import "LSHTTPRequest.h" @class ASIHTTPRequest; @interface LSASIHTTPRequestAdapter : NSObject - (instancetype)initWithASIHTTPRequest:(ASIHTTPRequest *)request; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/LSASIHTTPRequestAdapter.m ================================================ #import "LSASIHTTPRequestAdapter.h" @interface ASIHTTPRequest @property (nonatomic, strong, readonly) NSURL *url; @property (nonatomic, strong, readonly) NSString *requestMethod; @property (nonatomic, strong, readonly) NSDictionary *requestHeaders; @property (nonatomic, strong, readonly) NSData *postBody; @end @interface LSASIHTTPRequestAdapter () @property (nonatomic, strong) ASIHTTPRequest *request; @end @implementation LSASIHTTPRequestAdapter - (instancetype)initWithASIHTTPRequest:(ASIHTTPRequest *)request { self = [super init]; if (self) { _request = request; } return self; } - (NSURL *)url { return self.request.url; } - (NSString *)method { return self.request.requestMethod; } - (NSDictionary *)headers { return self.request.requestHeaders; } - (NSData *)body { return self.request.postBody; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/LSASIHTTPRequestHook.h ================================================ #import "LSHTTPClientHook.h" @interface LSASIHTTPRequestHook : LSHTTPClientHook @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/ASIHTTPRequest/LSASIHTTPRequestHook.m ================================================ #import "LSASIHTTPRequestHook.h" #import "ASIHTTPRequestStub.h" #import @implementation LSASIHTTPRequestHook - (void)load { if (!NSClassFromString(@"ASIHTTPRequest")) return; [self swizzleASIHTTPRequest]; } - (void)unload { if (!NSClassFromString(@"ASIHTTPRequest")) return; [self swizzleASIHTTPRequest]; } #pragma mark - Internal Methods - (void)swizzleASIHTTPRequest { [self swizzleASIHTTPSelector:NSSelectorFromString(@"responseStatusCode") withSelector:@selector(stub_responseStatusCode)]; [self swizzleASIHTTPSelector:NSSelectorFromString(@"responseData") withSelector:@selector(stub_responseData)]; [self swizzleASIHTTPSelector:NSSelectorFromString(@"responseHeaders") withSelector:@selector(stub_responseHeaders)]; [self swizzleASIHTTPSelector:NSSelectorFromString(@"startRequest") withSelector:@selector(stub_startRequest)]; [self addMethodToASIHTTPRequest:NSSelectorFromString(@"stubResponse")]; [self addMethodToASIHTTPRequest:NSSelectorFromString(@"setStubResponse:")]; } - (void)swizzleASIHTTPSelector:(SEL)original withSelector:(SEL)stub { Class asiHttpRequest = NSClassFromString(@"ASIHTTPRequest"); Method originalMethod = class_getInstanceMethod(asiHttpRequest, original); Method stubMethod = class_getInstanceMethod([ASIHTTPRequestStub class], stub); if (!originalMethod || !stubMethod) { [self fail]; } method_exchangeImplementations(originalMethod, stubMethod); } - (void)addMethodToASIHTTPRequest:(SEL)newMethod { Method method = class_getInstanceMethod([ASIHTTPRequestStub class], newMethod); const char *types = method_getTypeEncoding(method); class_addMethod(NSClassFromString(@"ASIHTTPRequest"), newMethod, class_getMethodImplementation([ASIHTTPRequestStub class], newMethod), types); } - (void)fail { [NSException raise:NSInternalInconsistencyException format:@"Couldn't load ASIHTTPRequest hook."]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/LSHTTPClientHook.h ================================================ #import @interface LSHTTPClientHook : NSObject - (void)load; - (void)unload; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/LSHTTPClientHook.m ================================================ #import "LSHTTPClientHook.h" @implementation LSHTTPClientHook - (void)load { [NSException raise:NSInternalInconsistencyException format:@"Method '%@' not implemented. Subclass '%@' and override it", NSStringFromSelector(_cmd), NSStringFromClass([self class])]; } - (void)unload { [NSException raise:NSInternalInconsistencyException format:@"Method '%@' not implemented. Subclass '%@' and override it", NSStringFromSelector(_cmd), NSStringFromClass([self class])]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/LSHTTPStubURLProtocol.h ================================================ #import @interface LSHTTPStubURLProtocol : NSURLProtocol @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/LSHTTPStubURLProtocol.m ================================================ #import "LSHTTPStubURLProtocol.h" #import "LSNocilla.h" #import "NSURLRequest+LSHTTPRequest.h" #import "LSStubRequest.h" #import "NSURLRequest+DSL.h" @interface NSHTTPURLResponse(UndocumentedInitializer) - (id)initWithURL:(NSURL*)URL statusCode:(NSInteger)statusCode headerFields:(NSDictionary*)headerFields requestTime:(double)requestTime; @end @implementation LSHTTPStubURLProtocol + (BOOL)canInitWithRequest:(NSURLRequest *)request { return [@[ @"http", @"https" ] containsObject:request.URL.scheme]; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return NO; } - (void)startLoading { NSURLRequest* request = [self request]; id client = [self client]; LSStubResponse* stubbedResponse = [[LSNocilla sharedInstance] responseForRequest:request]; NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; [cookieStorage setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:stubbedResponse.headers forURL:request.url] forURL:request.URL mainDocumentURL:request.URL]; [stubbedResponse waitForGo]; if (stubbedResponse.shouldFail) { [client URLProtocol:self didFailWithError:stubbedResponse.error]; } else { NSHTTPURLResponse* urlResponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:stubbedResponse.statusCode headerFields:stubbedResponse.headers requestTime:0]; if (stubbedResponse.statusCode < 300 || stubbedResponse.statusCode > 399 || stubbedResponse.statusCode == 304 || stubbedResponse.statusCode == 305 ) { NSData *body = stubbedResponse.body; [client URLProtocol:self didReceiveResponse:urlResponse cacheStoragePolicy:NSURLCacheStorageNotAllowed]; [client URLProtocol:self didLoadData:body]; [client URLProtocolDidFinishLoading:self]; } else { NSURL *newURL = [NSURL URLWithString:[stubbedResponse.headers objectForKey:@"Location"] relativeToURL:request.URL]; NSMutableURLRequest *redirectRequest = [NSMutableURLRequest requestWithURL:newURL]; [redirectRequest setAllHTTPHeaderFields:[NSHTTPCookie requestHeaderFieldsWithCookies:[cookieStorage cookiesForURL:newURL]]]; [client URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:urlResponse]; // According to: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Listings/CustomHTTPProtocol_Core_Code_CustomHTTPProtocol_m.html // needs to abort the original request [client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]]; } } } - (void)stopLoading { } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/LSNSURLHook.h ================================================ #import "LSHTTPClientHook.h" @interface LSNSURLHook : LSHTTPClientHook @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/LSNSURLHook.m ================================================ #import "LSNSURLHook.h" #import "LSHTTPStubURLProtocol.h" @implementation LSNSURLHook - (void)load { [NSURLProtocol registerClass:[LSHTTPStubURLProtocol class]]; } - (void)unload { [NSURLProtocol unregisterClass:[LSHTTPStubURLProtocol class]]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/NSURLRequest+DSL.h ================================================ #import @interface NSURLRequest (DSL) - (NSString *)toNocillaDSL; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/NSURLRequest+DSL.m ================================================ #import "NSURLRequest+DSL.h" #import "LSHTTPRequestDSLRepresentation.h" #import "NSURLRequest+LSHTTPRequest.h" @implementation NSURLRequest (DSL) - (NSString *)toNocillaDSL { return [[[LSHTTPRequestDSLRepresentation alloc] initWithRequest:self] description]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/NSURLRequest+LSHTTPRequest.h ================================================ #import #import "LSHTTPRequest.h" @interface NSURLRequest (LSHTTPRequest) @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLRequest/NSURLRequest+LSHTTPRequest.m ================================================ #import "NSURLRequest+LSHTTPRequest.h" @implementation NSURLRequest (LSHTTPRequest) - (NSURL*)url { return self.URL; } - (NSString *)method { return self.HTTPMethod; } - (NSDictionary *)headers { return self.allHTTPHeaderFields; } - (NSData *)body { if (self.HTTPBodyStream) { NSInputStream *stream = self.HTTPBodyStream; NSMutableData *data = [NSMutableData data]; [stream open]; size_t bufferSize = 4096; uint8_t *buffer = malloc(bufferSize); if (buffer == NULL) { [NSException raise:@"NocillaMallocFailure" format:@"Could not allocate %zu bytes to read HTTPBodyStream", bufferSize]; } while ([stream hasBytesAvailable]) { NSInteger bytesRead = [stream read:buffer maxLength:bufferSize]; if (bytesRead > 0) { NSData *readData = [NSData dataWithBytes:buffer length:bytesRead]; [data appendData:readData]; } else if (bytesRead < 0) { [NSException raise:@"NocillaStreamReadError" format:@"An error occurred while reading HTTPBodyStream (%ld)", (long)bytesRead]; } else if (bytesRead == 0) { break; } } free(buffer); [stream close]; return data; } return self.HTTPBody; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLSession/LSNSURLSessionHook.h ================================================ // // LSNSURLSessionHook.h // Nocilla // // Created by Luis Solano Bonet on 08/01/14. // Copyright (c) 2014 Luis Solano Bonet. All rights reserved. // #import "Nocilla.h" #import "LSHTTPClientHook.h" @interface LSNSURLSessionHook : LSHTTPClientHook @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Hooks/NSURLSession/LSNSURLSessionHook.m ================================================ // // LSNSURLSessionHook.m // Nocilla // // Created by Luis Solano Bonet on 08/01/14. // Copyright (c) 2014 Luis Solano Bonet. All rights reserved. // #import "LSNSURLSessionHook.h" #import "LSHTTPStubURLProtocol.h" #import @implementation LSNSURLSessionHook - (void)load { Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration"); [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]]; } - (void)unload { Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration"); [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]]; } - (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub { Method originalMethod = class_getInstanceMethod(original, selector); Method stubMethod = class_getInstanceMethod(stub, selector); if (!originalMethod || !stubMethod) { [NSException raise:NSInternalInconsistencyException format:@"Couldn't load NSURLSession hook."]; } method_exchangeImplementations(originalMethod, stubMethod); } - (NSArray *)protocolClasses { return @[[LSHTTPStubURLProtocol class]]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/LSNocilla.h ================================================ #import #import "Nocilla.h" @class LSStubRequest; @class LSStubResponse; @class LSHTTPClientHook; @protocol LSHTTPRequest; extern NSString * const LSUnexpectedRequest; @interface LSNocilla : NSObject + (LSNocilla *)sharedInstance; @property (nonatomic, strong, readonly) NSArray *stubbedRequests; @property (nonatomic, assign, readonly, getter = isStarted) BOOL started; - (void)start; - (void)stop; - (void)addStubbedRequest:(LSStubRequest *)request; - (void)clearStubs; - (void)registerHook:(LSHTTPClientHook *)hook; - (LSStubResponse *)responseForRequest:(id)request; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/LSNocilla.m ================================================ #import "LSNocilla.h" #import "LSNSURLHook.h" #import "LSStubRequest.h" #import "LSHTTPRequestDSLRepresentation.h" #import "LSASIHTTPRequestHook.h" #import "LSNSURLSessionHook.h" #import "LSASIHTTPRequestHook.h" NSString * const LSUnexpectedRequest = @"Unexpected Request"; @interface LSNocilla () @property (nonatomic, strong) NSMutableArray *mutableRequests; @property (nonatomic, strong) NSMutableArray *hooks; @property (nonatomic, assign, getter = isStarted) BOOL started; - (void)loadHooks; - (void)unloadHooks; @end static LSNocilla *sharedInstace = nil; @implementation LSNocilla + (LSNocilla *)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstace = [[self alloc] init]; }); return sharedInstace; } - (id)init { self = [super init]; if (self) { _mutableRequests = [NSMutableArray array]; _hooks = [NSMutableArray array]; [self registerHook:[[LSNSURLHook alloc] init]]; if (NSClassFromString(@"NSURLSession") != nil) { [self registerHook:[[LSNSURLSessionHook alloc] init]]; } [self registerHook:[[LSASIHTTPRequestHook alloc] init]]; } return self; } - (NSArray *)stubbedRequests { return [NSArray arrayWithArray:self.mutableRequests]; } - (void)start { if (!self.isStarted){ [self loadHooks]; self.started = YES; } } - (void)stop { [self unloadHooks]; [self clearStubs]; self.started = NO; } - (void)addStubbedRequest:(LSStubRequest *)request { NSUInteger index = [self.mutableRequests indexOfObject:request]; if (index == NSNotFound) { [self.mutableRequests addObject:request]; return; } [self.mutableRequests replaceObjectAtIndex:index withObject:request]; } - (void)clearStubs { [self.mutableRequests removeAllObjects]; } - (LSStubResponse *)responseForRequest:(id)actualRequest { NSArray* requests = [LSNocilla sharedInstance].stubbedRequests; for(LSStubRequest *someStubbedRequest in requests) { if ([someStubbedRequest matchesRequest:actualRequest]) { return someStubbedRequest.response; } } // Stop raise this due to it causes problem on CI. // [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]]; return nil; } - (void)registerHook:(LSHTTPClientHook *)hook { if (![self hookWasRegistered:hook]) { [[self hooks] addObject:hook]; } } - (BOOL)hookWasRegistered:(LSHTTPClientHook *)aHook { for (LSHTTPClientHook *hook in self.hooks) { if ([hook isMemberOfClass: [aHook class]]) { return YES; } } return NO; } #pragma mark - Private - (void)loadHooks { for (LSHTTPClientHook *hook in self.hooks) { [hook load]; } } - (void)unloadHooks { for (LSHTTPClientHook *hook in self.hooks) { [hook unload]; } } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSDataMatcher.h ================================================ // // LSDataMatcher.h // Nocilla // // Created by Luis Solano Bonet on 09/11/14. // Copyright (c) 2014 Luis Solano Bonet. All rights reserved. // #import #import "LSMatcher.h" @interface LSDataMatcher : LSMatcher - (instancetype)initWithData:(NSData *)data; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSDataMatcher.m ================================================ // // LSDataMatcher.m // Nocilla // // Created by Luis Solano Bonet on 09/11/14. // Copyright (c) 2014 Luis Solano Bonet. All rights reserved. // #import "LSDataMatcher.h" @interface LSDataMatcher () @property (nonatomic, copy) NSData *data; @end @implementation LSDataMatcher - (instancetype)initWithData:(NSData *)data { self = [super init]; if (self) { _data = data; } return self; } - (BOOL)matchesData:(NSData *)data { return [self.data isEqualToData:data]; } #pragma mark - Equality - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[LSDataMatcher class]]) { return NO; } return [self.data isEqual:((LSDataMatcher *)object).data]; } - (NSUInteger)hash { return self.data.hash; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSMatcheable.h ================================================ #import @class LSMatcher; @protocol LSMatcheable - (LSMatcher *)matcher; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSMatcher.h ================================================ #import @interface LSMatcher : NSObject - (BOOL)matches:(NSString *)string; - (BOOL)matchesData:(NSData *)data; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSMatcher.m ================================================ #import "LSMatcher.h" @implementation LSMatcher - (BOOL)matches:(NSString *)string { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"[LSMatcher matches:] is an abstract method" userInfo:nil]; } - (BOOL)matchesData:(NSData *)data { return [self matches:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; } #pragma mark - Equality - (BOOL)isEqual:(id)object { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"[LSMatcher isEqual:] is an abstract method" userInfo:nil]; } - (NSUInteger)hash { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"[LSMatcher hash] an abstract method" userInfo:nil]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSRegexMatcher.h ================================================ #import "LSMatcher.h" @interface LSRegexMatcher : LSMatcher - (instancetype)initWithRegex:(NSRegularExpression *)regex; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSRegexMatcher.m ================================================ #import "LSRegexMatcher.h" @interface LSRegexMatcher () @property (nonatomic, strong) NSRegularExpression *regex; @end @implementation LSRegexMatcher - (instancetype)initWithRegex:(NSRegularExpression *)regex { self = [super init]; if (self) { _regex = regex; } return self; } - (BOOL)matches:(NSString *)string { return [self.regex numberOfMatchesInString:string options:0 range:NSMakeRange(0, string.length)] > 0; } #pragma mark - Equality - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[LSRegexMatcher class]]) { return NO; } return [self.regex isEqual:((LSRegexMatcher *)object).regex]; } - (NSUInteger)hash { return self.regex.hash; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSStringMatcher.h ================================================ #import #import "LSMatcher.h" @interface LSStringMatcher : LSMatcher - (instancetype)initWithString:(NSString *)string; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/LSStringMatcher.m ================================================ #import "LSStringMatcher.h" @interface LSStringMatcher () @property (nonatomic, copy) NSString *string; @end @implementation LSStringMatcher - (instancetype)initWithString:(NSString *)string { self = [super init]; if (self) { _string = string; } return self; } - (BOOL)matches:(NSString *)string { return [self.string isEqualToString:string]; } #pragma mark - Equality - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[LSStringMatcher class]]) { return NO; } return [self.string isEqualToString:((LSStringMatcher *)object).string]; } - (NSUInteger)hash { return self.string.hash; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/NSData+Matcheable.h ================================================ // // NSData+Matcheable.h // Nocilla // // Created by Luis Solano Bonet on 09/11/14. // Copyright (c) 2014 Luis Solano Bonet. All rights reserved. // #import #import "LSMatcheable.h" @interface NSData (Matcheable) @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/NSData+Matcheable.m ================================================ // // NSData+Matcheable.m // Nocilla // // Created by Luis Solano Bonet on 09/11/14. // Copyright (c) 2014 Luis Solano Bonet. All rights reserved. // #import "NSData+Matcheable.h" #import "LSDataMatcher.h" @implementation NSData (Matcheable) - (LSMatcher *)matcher { return [[LSDataMatcher alloc] initWithData:self]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/NSRegularExpression+Matcheable.h ================================================ #import #import "LSMatcheable.h" @interface NSRegularExpression (Matcheable) @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/NSRegularExpression+Matcheable.m ================================================ #import "NSRegularExpression+Matcheable.h" #import "LSRegexMatcher.h" @implementation NSRegularExpression (Matcheable) - (LSMatcher *)matcher { return [[LSRegexMatcher alloc] initWithRegex:self]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/NSString+Matcheable.h ================================================ #import #import "LSMatcheable.h" @interface NSString (Matcheable) @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Matchers/NSString+Matcheable.m ================================================ #import "NSString+Matcheable.h" #import "LSStringMatcher.h" @implementation NSString (Matcheable) - (LSMatcher *)matcher { return [[LSStringMatcher alloc] initWithString:self]; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Model/LSHTTPBody.h ================================================ #import @protocol LSHTTPBody - (NSData *)data; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Model/LSHTTPRequest.h ================================================ #import @protocol LSHTTPRequest @property (nonatomic, strong, readonly) NSURL *url; @property (nonatomic, strong, readonly) NSString *method; @property (nonatomic, strong, readonly) NSDictionary *headers; @property (nonatomic, strong, readonly) NSData *body; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Model/LSHTTPResponse.h ================================================ #import @protocol LSHTTPResponse @property (nonatomic, assign, readonly) NSInteger statusCode; @property (nonatomic, strong, readonly) NSDictionary *headers; @property (nonatomic, strong, readonly) NSData *body; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Nocilla.h ================================================ // // Nocilla.h // Nocilla // // Created by Robert Böhnke on 26/03/15. // Copyright (c) 2015 Luis Solano Bonet. All rights reserved. // #import //! Project version number for Nocilla. FOUNDATION_EXPORT double NocillaVersionNumber; //! Project version string for Nocilla. FOUNDATION_EXPORT const unsigned char NocillaVersionString[]; #import "LSHTTPBody.h" #import "LSMatcheable.h" #import "LSNocilla.h" #import "LSStubRequestDSL.h" #import "LSStubResponseDSL.h" #import "NSData+Matcheable.h" #import "NSData+Nocilla.h" #import "NSRegularExpression+Matcheable.h" #import "NSString+Matcheable.h" #import "NSString+Nocilla.h" ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Stubs/LSStubRequest.h ================================================ #import #import "LSStubResponse.h" #import "LSHTTPRequest.h" @class LSMatcher; @class LSStubRequest; @class LSStubResponse; @interface LSStubRequest : NSObject @property (nonatomic, strong, readonly) NSString *method; @property (nonatomic, strong, readonly) LSMatcher *urlMatcher; @property (nonatomic, strong, readonly) NSDictionary *headers; @property (nonatomic, strong, readwrite) LSMatcher *body; @property (nonatomic, strong) LSStubResponse *response; - (instancetype)initWithMethod:(NSString *)method url:(NSString *)url; - (instancetype)initWithMethod:(NSString *)method urlMatcher:(LSMatcher *)urlMatcher; - (void)setHeader:(NSString *)header value:(NSString *)value; - (BOOL)matchesRequest:(id)request; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Stubs/LSStubRequest.m ================================================ #import "LSStubRequest.h" #import "LSMatcher.h" #import "NSString+Matcheable.h" @interface LSStubRequest () @property (nonatomic, strong, readwrite) NSString *method; @property (nonatomic, strong, readwrite) LSMatcher *urlMatcher; @property (nonatomic, strong, readwrite) NSMutableDictionary *mutableHeaders; -(BOOL)matchesMethod:(id)request; -(BOOL)matchesURL:(id)request; -(BOOL)matchesHeaders:(id)request; -(BOOL)matchesBody:(id)request; @end @implementation LSStubRequest - (instancetype)initWithMethod:(NSString *)method url:(NSString *)url { return [self initWithMethod:method urlMatcher:[url matcher]]; } - (instancetype)initWithMethod:(NSString *)method urlMatcher:(LSMatcher *)urlMatcher; { self = [super init]; if (self) { self.method = method; self.urlMatcher = urlMatcher; self.mutableHeaders = [NSMutableDictionary dictionary]; } return self; } - (void)setHeader:(NSString *)header value:(NSString *)value { [self.mutableHeaders setValue:value forKey:header]; } - (NSDictionary *)headers { return [NSDictionary dictionaryWithDictionary:self.mutableHeaders];; } - (NSString *)description { return [NSString stringWithFormat:@"StubRequest:\nMethod: %@\nURL: %@\nHeaders: %@\nBody: %@\nResponse: %@", self.method, self.urlMatcher, self.headers, self.body, self.response]; } - (LSStubResponse *)response { if (!_response) { _response = [[LSStubResponse alloc] initDefaultResponse]; } return _response; } - (BOOL)matchesRequest:(id)request { if ([self matchesMethod:request] && [self matchesURL:request] && [self matchesHeaders:request] && [self matchesBody:request] ) { return YES; } return NO; } -(BOOL)matchesMethod:(id)request { if (!self.method || [self.method isEqualToString:request.method]) { return YES; } return NO; } -(BOOL)matchesURL:(id)request { return [self.urlMatcher matches:[request.url absoluteString]]; } -(BOOL)matchesHeaders:(id)request { for (NSString *header in self.headers) { if (![[request.headers objectForKey:header] isEqualToString:[self.headers objectForKey:header]]) { return NO; } } return YES; } -(BOOL)matchesBody:(id)request { NSData *reqBody = request.body; if (!self.body || [self.body matchesData:reqBody]) { return YES; } return NO; } #pragma mark - Equality - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[LSStubRequest class]]) { return NO; } return [self isEqualToStubRequest:object]; } - (BOOL)isEqualToStubRequest:(LSStubRequest *)stubRequest { if (!stubRequest) { return NO; } BOOL methodEqual = [self.method isEqualToString:stubRequest.method]; BOOL urlMatcherEqual = [self.urlMatcher isEqual:stubRequest.urlMatcher]; BOOL headersEqual = [self.headers isEqual:stubRequest.headers]; BOOL bodyEqual = (self.body == nil && stubRequest.body == nil) || [self.body isEqual:stubRequest.body]; return methodEqual && urlMatcherEqual && headersEqual && bodyEqual; } - (NSUInteger)hash { return self.method.hash ^ self.urlMatcher.hash ^ self.headers.hash ^ self.body.hash; } @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Stubs/LSStubResponse.h ================================================ #import #import "LSHTTPResponse.h" @interface LSStubResponse : NSObject @property (nonatomic, assign, readonly) NSInteger statusCode; @property (nonatomic, strong) NSData *body; @property (nonatomic, strong, readonly) NSDictionary *headers; @property (nonatomic, assign, readonly) BOOL shouldFail; @property (nonatomic, strong, readonly) NSError *error; - (id)initWithError:(NSError *)error; - (id)initWithStatusCode:(NSInteger)statusCode; - (id)initWithRawResponse:(NSData *)rawResponseData; - (id)initDefaultResponse; - (void)setHeader:(NSString *)header value:(NSString *)value; - (void)delay; - (void)go; - (void)waitForGo; @end ================================================ FILE: Tests/Dependency/Nocilla/Nocilla/Stubs/LSStubResponse.m ================================================ #import "LSStubResponse.h" @interface LSStubResponse () { NSCondition *_delayLock; } @property (nonatomic, assign, readwrite) NSInteger statusCode; @property (nonatomic, strong) NSMutableDictionary *mutableHeaders; @property (nonatomic, assign) UInt64 offset; @property (nonatomic, assign, getter = isDone) BOOL done; @property (nonatomic, assign) BOOL shouldFail; @property (nonatomic, strong) NSError *error; @end @implementation LSStubResponse #pragma Initializers - (id)initDefaultResponse { self = [super init]; if (self) { self.shouldFail = NO; self.statusCode = 200; self.mutableHeaders = [NSMutableDictionary dictionary]; self.body = [@"" dataUsingEncoding:NSUTF8StringEncoding]; } return self; } - (id)initWithError:(NSError *)error { self = [super init]; if (self) { self.shouldFail = YES; self.error = error; } return self; } -(id)initWithStatusCode:(NSInteger)statusCode { self = [super init]; if (self) { self.shouldFail = NO; self.statusCode = statusCode; self.mutableHeaders = [NSMutableDictionary dictionary]; self.body = [@"" dataUsingEncoding:NSUTF8StringEncoding]; } return self; } - (id)initWithRawResponse:(NSData *)rawResponseData { self = [self initDefaultResponse]; if (self) { CFHTTPMessageRef httpMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, FALSE); if (httpMessage) { CFHTTPMessageAppendBytes(httpMessage, [rawResponseData bytes], [rawResponseData length]); self.body = rawResponseData; // By default if (CFHTTPMessageIsHeaderComplete(httpMessage)) { self.statusCode = (NSInteger)CFHTTPMessageGetResponseStatusCode(httpMessage); self.mutableHeaders = [NSMutableDictionary dictionaryWithDictionary:(__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(httpMessage)]; self.body = (__bridge_transfer NSData *)CFHTTPMessageCopyBody(httpMessage); } CFRelease(httpMessage); } } return self; } - (void)setHeader:(NSString *)header value:(NSString *)value { [self.mutableHeaders setValue:value forKey:header]; } - (NSDictionary *)headers { return [NSDictionary dictionaryWithDictionary:self.mutableHeaders]; } - (NSString *)description { return [NSString stringWithFormat:@"StubRequest:\nStatus Code: %ld\nHeaders: %@\nBody: %@", (long)self.statusCode, self.mutableHeaders, self.body]; } - (NSCondition*)delayLock { @synchronized(self) { return _delayLock; } } - (void)delay { @synchronized(self) { if(!_delayLock) _delayLock = [[NSCondition alloc] init]; } } - (void)go { NSCondition *condition = self.delayLock; @synchronized(self) { _delayLock = nil; } [condition lock]; [condition broadcast]; [condition unlock]; } - (void)waitForGo { NSCondition *condition = self.delayLock; [condition lock]; [condition wait]; [condition unlock]; } @end ================================================ FILE: Tests/Dependency/Nocilla/README.md ================================================ # 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) Stunning HTTP stubbing for iOS and OS X. Testing HTTP requests has never been easier. This 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. ## Features * Stub HTTP and HTTPS requests in your unit tests. * Supports NSURLConnection, NSURLSession and ASIHTTPRequest. * Awesome DSL that will improve the readability and maintainability of your tests. * Match requests with regular expressions. * Stub requests with errors. * Tested. * Fast. * Extendable to support more HTTP libraries. ## Installation ### As a [CocoaPod](http://cocoapods.org/) Just add this to your Podfile ```ruby pod 'Nocilla' ``` ### Other approaches * You should be able to add Nocilla to you source tree. If you are using git, consider using a `git submodule` ## Usage _Yes, the following code is valid Objective-C, or at least, it should be_ The following examples are described using [Kiwi](https://github.com/kiwi-bdd/Kiwi) ### Common parts Until Nocilla can hook directly into Kiwi, you will have to include the following snippet in the specs you want to use Nocilla: ```objc #import "Kiwi.h" #import "Nocilla.h" SPEC_BEGIN(ExampleSpec) beforeAll(^{ [[LSNocilla sharedInstance] start]; }); afterAll(^{ [[LSNocilla sharedInstance] stop]; }); afterEach(^{ [[LSNocilla sharedInstance] clearStubs]; }); it(@"should do something", ^{ // Stub here! }); SPEC_END ``` ### Stubbing requests #### Stubbing a simple request It will return the default response, which is a 200 and an empty body. ```objc stubRequest(@"GET", @"http://www.google.com"); ``` #### Stubbing requests with regular expressions ```objc stubRequest(@"GET", @"^http://(.*?)\\.example\\.com/v1/dogs\\.json".regex); ``` #### Stubbing a request with a particular header ```objc stubRequest(@"GET", @"https://api.example.com"). withHeader(@"Accept", @"application/json"); ``` #### Stubbing a request with multiple headers Using the `withHeaders` method makes sense with the Objective-C literals, but it accepts an NSDictionary. ```objc stubRequest(@"GET", @"https://api.example.com/dogs.json"). withHeaders(@{@"Accept": @"application/json", @"X-CUSTOM-HEADER": @"abcf2fbc6abgf"}); ``` #### Stubbing a request with a particular body ```objc stubRequest(@"POST", @"https://api.example.com/dogs.json"). withHeaders(@{@"Accept": @"application/json", @"X-CUSTOM-HEADER": @"abcf2fbc6abgf"}). withBody(@"{\"name\":\"foo\"}"); ``` You can also use `NSData` for the request body: ```objc stubRequest(@"POST", @"https://api.example.com/dogs.json"). withHeaders(@{@"Accept": @"application/json", @"X-CUSTOM-HEADER": @"abcf2fbc6abgf"}). withBody([@"foo" dataUsingEncoding:NSUTF8StringEncoding]); ``` It even works with regular expressions! ```objc stubRequest(@"POST", @"https://api.example.com/dogs.json"). withHeaders(@{@"Accept": @"application/json", @"X-CUSTOM-HEADER": @"abcf2fbc6abgf"}). withBody(@"^The body start with this".regex); ``` #### Returning a specific status code ```objc stubRequest(@"GET", @"http://www.google.com").andReturn(404); ``` #### Returning a specific status code and header The same approach here, you can use `withHeader` or `withHeaders` ```objc stubRequest(@"POST", @"https://api.example.com/dogs.json"). andReturn(201). withHeaders(@{@"Content-Type": @"application/json"}); ``` #### Returning a specific status code, headers and body ```objc stubRequest(@"GET", @"https://api.example.com/dogs.json"). andReturn(201). withHeaders(@{@"Content-Type": @"application/json"}). withBody(@"{\"ok\":true}"); ``` You can also use `NSData` for the response body: ```objc stubRequest(@"GET", @"https://api.example.com/dogs.json"). andReturn(201). withHeaders(@{@"Content-Type": @"application/json"}). withBody([@"bar" dataUsingEncoding:NSUTF8StringEncoding]); ``` #### Returning raw responses recorded with `curl -is` `curl -is http://api.example.com/dogs.json > /tmp/example_curl_-is_output.txt` ```objc stubRequest(@"GET", @"https://api.example.com/dogs.json"). andReturnRawResponse([NSData dataWithContentsOfFile:@"/tmp/example_curl_-is_output.txt"]); ``` #### All together ```objc stubRequest(@"POST", @"https://api.example.com/dogs.json"). withHeaders(@{@"Accept": @"application/json", @"X-CUSTOM-HEADER": @"abcf2fbc6abgf"}). withBody(@"{\"name\":\"foo\"}"). andReturn(201). withHeaders(@{@"Content-Type": @"application/json"}). withBody(@"{\"ok\":true}"); ``` #### Making a request fail This will call the failure handler (callback, delegate... whatever your HTTP client uses) with the specified error. ```objc stubRequest(@"POST", @"https://api.example.com/dogs.json"). withHeaders(@{@"Accept": @"application/json", @"X-CUSTOM-HEADER": @"abcf2fbc6abgf"}). withBody(@"{\"name\":\"foo\"}"). andFailWithError([NSError errorWithDomain:@"foo" code:123 userInfo:nil]); ``` #### Replacing a request stub If you need to change the response of a single request, simply re-stub the request: ```objc stubRequest(@"POST", @"https://api.example.com/authorize/"). andReturn(401); // Some test expectation... stubRequest(@"POST", @"https://api.example.com/authorize/"). andReturn(200); ``` ### Unexpected requests If 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. At 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. ### Testing asynchronous requests When 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. ## Who uses Nocilla. ### Submit a PR to add your company here! - [MessageBird](https://www.messagebird.com) - [Groupon](http://www.groupon.com) - [Pixable](http://www.pixable.com) - [Jackthreads](https://www.jackthreads.com) - [ShopKeep](http://www.shopkeep.com) - [Venmo](https://www.venmo.com) - [Lighthouse](http://www.lighthouselabs.co.uk) ## Other alternatives * [ILTesting](https://github.com/InfiniteLoopDK/ILTesting) * [OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs) ## Contributing 1. Fork it 2. Create your feature branch 3. Commit your changes 4. Push to the branch 5. Create new Pull Request ================================================ FILE: Tests/KingfisherTests/DataReceivingSideEffectTests.swift ================================================ // // DataReceivingSideEffectTests.swift // Kingfisher // // Created by Wei Wang on 2019/05/15. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class DataReceivingSideEffectTests: XCTestCase { var manager: KingfisherManager! override class func setUp() { super.setUp() LSNocilla.sharedInstance().start() } override class func tearDown() { LSNocilla.sharedInstance().stop() super.tearDown() } override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. let uuid = UUID() let downloader = ImageDownloader(name: "test.manager.\(uuid.uuidString)") let cache = ImageCache(name: "test.cache.\(uuid.uuidString)") manager = KingfisherManager(downloader: downloader, cache: cache) } override func tearDown() { LSNocilla.sharedInstance().clearStubs() clearCaches([manager.cache]) cleanDefaultCache() manager = nil super.tearDown() } func xtestDataReceivingSideEffectBlockCanBeCalled() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) let receiver = DataReceivingStub() let options: KingfisherOptionsInfo = [/*.onDataReceived([receiver]),*/ .waitForCache] KingfisherManager.shared.retrieveImage(with: url, options: options) { result in XCTAssertTrue(receiver.called.value) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func xtestDataReceivingSideEffectBlockCanBeCalledButNotApply() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) let receiver = DataReceivingNotApplyStub() let options: KingfisherOptionsInfo = [/*.onDataReceived([receiver]),*/ .waitForCache] KingfisherManager.shared.retrieveImage(with: url, options: options) { result in XCTAssertTrue(receiver.called.value) XCTAssertFalse(receiver.applied.value) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } } class DataReceivingStub: DataReceivingSideEffect, @unchecked Sendable { var called = LockIsolated(false) var onShouldApply: () -> Bool = { return true } func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { self.called.setValue(true) } } class DataReceivingNotApplyStub: DataReceivingSideEffect, @unchecked Sendable { var called = LockIsolated(false) var applied = LockIsolated(false) var onShouldApply: () -> Bool = { return false } func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { called.setValue(true) if onShouldApply() { applied.setValue(true) } } } ================================================ FILE: Tests/KingfisherTests/DiskStorageTests.swift ================================================ // // DiskStorageTests.swift // Kingfisher // // Created by Wei Wang on 2018/11/12. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher #if compiler(>=6) extension String: @retroactive DataTransformable { } #else extension String: DataTransformable { } #endif extension String { public func toData() throws -> Data { return data(using: .utf8)! } public static func fromData(_ data: Data) throws -> String { return String(data: data, encoding: .utf8)! } public static var empty: String { return "" } } class DiskStorageTests: XCTestCase { var storage: DiskStorage.Backend! override func setUp() { super.setUp() let uuid = UUID().uuidString let config = DiskStorage.Config(name: "test-\(uuid)", sizeLimit: 5) storage = try! DiskStorage.Backend(config: config) } override func tearDown() { try! storage.removeAll(skipCreatingDirectory: true) super.tearDown() } func testStoreAndGet() { XCTAssertFalse(storage.isCached(forKey: "1")) try! storage.store(value: "1", forKey: "1") XCTAssertTrue(storage.isCached(forKey: "1")) let value = try! storage.value(forKey: "1") XCTAssertEqual(value, "1") } func testRemove() { XCTAssertFalse(storage.isCached(forKey: "1")) try! storage.store(value: "1", forKey: "1") try! storage.remove(forKey: "1") XCTAssertFalse(storage.isCached(forKey: "1")) } func testRemoveAll() { try! storage.store(value: "1", forKey: "1") try! storage.store(value: "2", forKey: "2") try! storage.store(value: "3", forKey: "3") try! storage.removeAll() XCTAssertFalse(storage.isCached(forKey: "1")) XCTAssertFalse(storage.isCached(forKey: "2")) XCTAssertFalse(storage.isCached(forKey: "3")) } func testTotalSize() { var size = try! storage.totalSize() XCTAssertEqual(size, 0) try! storage.store(value: "1", forKey: "1") size = try! storage.totalSize() XCTAssertEqual(size, 1) } func testSetExpiration() { let now = Date() try! storage.store(value: "1", forKey: "1", expiration: .seconds(1)) XCTAssertTrue(storage.isCached(forKey: "1", referenceDate: now)) XCTAssertFalse(storage.isCached(forKey: "1", referenceDate: now.addingTimeInterval(5))) } func testConfigExpiration() { let now = Date() storage.config.expiration = .seconds(1) try! storage.store(value: "1", forKey: "1") XCTAssertTrue(storage.isCached(forKey: "1", referenceDate: now)) XCTAssertFalse(storage.isCached(forKey: "1", referenceDate: now.addingTimeInterval(5))) } func testExtendExpirationByAccessing() { let exp = expectation(description: #function) let now = Date() try! storage.store(value: "1", forKey: "1", expiration: .seconds(2)) XCTAssertTrue(storage.isCached(forKey: "1")) XCTAssertFalse(storage.isCached(forKey: "1", referenceDate: now.addingTimeInterval(5))) delay(1) { let v = try! self.storage.value(forKey: "1") XCTAssertNotNil(v) // The meta extending happens on its own queue. self.storage.metaChangingQueue.async { XCTAssertTrue(self.storage.isCached(forKey: "1", referenceDate: now.addingTimeInterval(3))) XCTAssertFalse(self.storage.isCached(forKey: "1", referenceDate: now.addingTimeInterval(10))) exp.fulfill() } } waitForExpectations(timeout: 2, handler: nil) } func testNotExtendExpirationByAccessing() { let exp = expectation(description: #function) let now = Date() try! storage.store(value: "1", forKey: "1", expiration: .seconds(2)) XCTAssertTrue(storage.isCached(forKey: "1")) XCTAssertFalse(storage.isCached(forKey: "1", referenceDate: now.addingTimeInterval(3))) delay(1) { let v = try! self.storage.value(forKey: "1", extendingExpiration: .none) XCTAssertNotNil(v) // The meta extending happens on its own queue. self.storage.metaChangingQueue.async { XCTAssertFalse(self.storage.isCached(forKey: "1", referenceDate: now.addingTimeInterval(3))) XCTAssertFalse(self.storage.isCached(forKey: "1", referenceDate: now.addingTimeInterval(10))) exp.fulfill() } } waitForExpectations(timeout: 2, handler: nil) } func testRemoveExpired() { let expiration = StorageExpiration.seconds(1) try! storage.store(value: "1", forKey: "1", expiration: expiration) try! storage.store(value: "2", forKey: "2", expiration: expiration) try! storage.store(value: "3", forKey: "3") let urls = try! self.storage.removeExpiredValues(referenceDate: Date().addingTimeInterval(2)) XCTAssertEqual(urls.count, 2) XCTAssertTrue(self.storage.isCached(forKey: "3")) } func testRemoveSizeExceeded() { let count = 10 for i in 0.. 0) } func testConfigUsesHashedFileName() { let key = "test" // hashed fileName storage.config.usesHashedFileName = true let hashedFileName = storage.cacheFileName(forKey: key) XCTAssertNotEqual(hashedFileName, key) // validation sha256 hash of the key XCTAssertEqual(hashedFileName, key.kf.sha256) // fileName without hash storage.config.usesHashedFileName = false let originalFileName = storage.cacheFileName(forKey: key) XCTAssertEqual(originalFileName, key) } func testConfigUsesHashedFileNameWithAutoExt() { let key = "test.gif" // hashed fileName storage.config.usesHashedFileName = true storage.config.autoExtAfterHashedFileName = true let hashedFileName = storage.cacheFileName(forKey: key) XCTAssertNotEqual(hashedFileName, key) // validation sha256 hash of the key XCTAssertEqual(hashedFileName, key.kf.sha256 + ".gif") // fileName without hash storage.config.usesHashedFileName = false let originalFileName = storage.cacheFileName(forKey: key) XCTAssertEqual(originalFileName, key) } func testConfigUsesHashedFileNameWithAutoExtAndProcessor() { // The key of an image with processor will be as this format. let key = "test.jpeg@com.onevcat.Kingfisher.DownsamplingImageProcessor" // hashed fileName storage.config.usesHashedFileName = true storage.config.autoExtAfterHashedFileName = true let hashedFileName = storage.cacheFileName(forKey: key) XCTAssertNotEqual(hashedFileName, key) // validation sha256 hash of the key XCTAssertEqual(hashedFileName, key.kf.sha256 + ".jpeg") // fileName without hash storage.config.usesHashedFileName = false let originalFileName = storage.cacheFileName(forKey: key) XCTAssertEqual(originalFileName, key) } func testFileMetaOrder() { let urls = [URL(string: "test1")!, URL(string: "test2")!, URL(string: "test3")!] let now = Date() let file1 = DiskStorage.FileMeta( fileURL: urls[0], lastAccessDate: now, estimatedExpirationDate: now.addingTimeInterval(1), isDirectory: false, fileSize: 1) let file2 = DiskStorage.FileMeta( fileURL: urls[1], lastAccessDate: now.addingTimeInterval(1), estimatedExpirationDate: now.addingTimeInterval(2), isDirectory: false, fileSize: 1) let file3 = DiskStorage.FileMeta( fileURL: urls[2], lastAccessDate: now.addingTimeInterval(2), estimatedExpirationDate: now.addingTimeInterval(3), isDirectory: false, fileSize: 1) let ordered = [file2, file1, file3].sorted(by: DiskStorage.FileMeta.lastAccessDate) XCTAssertTrue(ordered[0].lastAccessDate! > ordered[1].lastAccessDate!) XCTAssertTrue(ordered[1].lastAccessDate! > ordered[2].lastAccessDate!) } } ================================================ FILE: Tests/KingfisherTests/ImageCacheTests.swift ================================================ // // ImageCacheTests.swift // Kingfisher // // Created by Wei Wang on 15/4/10. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class ImageCacheTests: XCTestCase { var cache: ImageCache! var observer: NSObjectProtocol! override func setUp() { super.setUp() let uuid = UUID().uuidString let cacheName = "test-\(uuid)" cache = ImageCache(name: cacheName) } override func tearDown() { clearCaches([cache]) cache = nil if let o = observer { NotificationCenter.default.removeObserver(o) observer = nil } super.tearDown() } func testInvalidCustomCachePath() { let customPath = "/path/to/image/cache" let url = URL(fileURLWithPath: customPath) XCTAssertThrowsError(try ImageCache(name: "test", cacheDirectoryURL: url)) { error in guard case KingfisherError.cacheError(reason: .cannotCreateDirectory(let path, _)) = error else { XCTFail("Should be KingfisherError with cacheError reason.") return } XCTAssertEqual(path, customPath + "/com.onevcat.Kingfisher.ImageCache.test") } } func testCustomCachePath() { let cacheURL = try! FileManager.default.url( for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let subFolder = cacheURL.appendingPathComponent("temp") let customPath = subFolder.path let cache = try! ImageCache(name: "test", cacheDirectoryURL: subFolder) XCTAssertEqual( cache.diskStorage.directoryURL.path, (customPath as NSString).appendingPathComponent("com.onevcat.Kingfisher.ImageCache.test")) clearCaches([cache]) } func testCustomCachePathByBlock() { let cache = try! ImageCache(name: "test", cacheDirectoryURL: nil, diskCachePathClosure: { (url, path) -> URL in let modifiedPath = path + "-modified" return url.appendingPathComponent(modifiedPath, isDirectory: true) }) let cacheURL = try! FileManager.default.url( for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) XCTAssertEqual( cache.diskStorage.directoryURL.path, (cacheURL.path as NSString).appendingPathComponent("com.onevcat.Kingfisher.ImageCache.test-modified")) clearCaches([cache]) } func testMaxCachePeriodInSecond() { cache.diskStorage.config.expiration = .seconds(1) XCTAssertEqual(cache.diskStorage.config.expiration.timeInterval, 1) } func testMaxMemorySize() { cache.memoryStorage.config.totalCostLimit = 1 XCTAssert(cache.memoryStorage.config.totalCostLimit == 1, "maxMemoryCost should be able to be set.") } func testMaxDiskCacheSize() { cache.diskStorage.config.sizeLimit = 1 XCTAssert(cache.diskStorage.config.sizeLimit == 1, "maxDiskCacheSize should be able to be set.") } func testClearDiskCache() { let exp = expectation(description: #function) let key = testKeys[0] cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in self.cache.clearMemoryCache() let cacheResult = self.cache.imageCachedType(forKey: key) XCTAssertTrue(cacheResult.cached) XCTAssertEqual(cacheResult, .disk) self.cache.clearDiskCache { let cacheResult = self.cache.imageCachedType(forKey: key) XCTAssertFalse(cacheResult.cached) exp.fulfill() } } waitForExpectations(timeout: 3, handler:nil) } func testClearDiskCacheAsync() async throws { let key = testKeys[0] try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true) cache.clearMemoryCache() var cacheResult = self.cache.imageCachedType(forKey: key) XCTAssertTrue(cacheResult.cached) XCTAssertEqual(cacheResult, .disk) await cache.clearDiskCache() cacheResult = cache.imageCachedType(forKey: key) XCTAssertFalse(cacheResult.cached) } func testClearMemoryCache() { let exp = expectation(description: #function) let key = testKeys[0] cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in self.cache.clearMemoryCache() self.cache.retrieveImage(forKey: key) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value?.cacheType, .disk) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testClearMemoryCacheAsync() async throws { let key = testKeys[0] try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true) cache.clearMemoryCache() let result = try await cache.retrieveImage(forKey: key) XCTAssertNotNil(result.image) XCTAssertEqual(result.cacheType, .disk) } func testNoImageFound() { let exp = expectation(description: #function) cache.retrieveImage(forKey: testKeys[0]) { result in XCTAssertNotNil(result.value) XCTAssertNil(result.value!.image) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testNoImageFoundAsync() async throws { let result = try await cache.retrieveImage(forKey: testKeys[0]) XCTAssertNil(result.image) } func testCachedFileDoesNotExist() { let URLString = testKeys[0] let url = URL(string: URLString)! let exists = cache.imageCachedType(forKey: url.cacheKey).cached XCTAssertFalse(exists) } func testStoreImageInMemory() { let exp = expectation(description: #function) let key = testKeys[0] cache.store(testImage, forKey: key, toDisk: false) { _ in self.cache.retrieveImage(forKey: key) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value?.cacheType, .memory) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testStoreImageInMemoryAsync() async throws { let key = testKeys[0] try await cache.store(testImage, forKey: key, toDisk: false) let result = try await cache.retrieveImage(forKey: key) XCTAssertNotNil(result.image) XCTAssertEqual(result.cacheType, .memory) } func testStoreGIFToDiskWithNilOriginalShouldPreserveGIFFormat() { struct TestProcessor: ImageProcessor { let identifier: String = "com.onevcat.KingfisherTests.TestProcessor" func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): return image case .data(let data): return DefaultImageProcessor.default.process(item: .data(data), options: options) } } } let exp = expectation(description: #function) let image = KingfisherWrapper.animatedImage(data: testImageGIFData, options: .init())! XCTAssertEqual(image.kf.gifRepresentation()?.kf.imageFormat, .GIF) let options = KingfisherParsedOptionsInfo([.processor(TestProcessor())]) let key = "test-gif" cache.store(image, original: nil, forKey: key, options: options, toDisk: true) { _ in do { let storedKey = key.computedKey(with: TestProcessor().identifier) let storedData = try self.cache.diskStorage.value(forKey: storedKey) XCTAssertEqual(storedData?.kf.imageFormat, .GIF) } catch { XCTFail("Unexpected error: \(error)") } exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testCopyKingfisherStateShouldKeepEmbeddedGIFDataForDiskCache() { struct TestProcessor: ImageProcessor { let identifier: String = "com.onevcat.KingfisherTests.TestProcessor.CopyState" func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { switch item { case .image(let image): #if os(macOS) guard let cgImage = image.kf.cgImage else { return image } let newImage = KFCrossPlatformImage(cgImage: cgImage, size: image.kf.size) image.kf.copyKingfisherState(to: newImage) return newImage #else guard let cgImage = image.cgImage else { return image } let newImage = KFCrossPlatformImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation) image.kf.copyKingfisherState(to: newImage) return newImage #endif case .data(let data): return DefaultImageProcessor.default.process(item: .data(data), options: options) } } } let exp = expectation(description: #function) let image = KingfisherWrapper.animatedImage(data: testImageGIFData, options: .init())! XCTAssertEqual(image.kf.gifRepresentation()?.kf.imageFormat, .GIF) let options = KingfisherParsedOptionsInfo([.processor(TestProcessor())]) let key = "test-gif-copy-state" cache.store(image, original: nil, forKey: key, options: options, toDisk: true) { _ in do { let storedKey = key.computedKey(with: TestProcessor().identifier) let storedData = try self.cache.diskStorage.value(forKey: storedKey) XCTAssertEqual(storedData?.kf.imageFormat, .GIF) } catch { XCTFail("Unexpected error: \(error)") } exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testStoreMultipleImages() { let exp = expectation(description: #function) storeMultipleImages { let diskCachePath = self.cache.diskStorage.directoryURL.path var files: [String] = [] do { files = try FileManager.default.contentsOfDirectory(atPath: diskCachePath) } catch _ { XCTFail() } XCTAssertEqual(files.count, testKeys.count) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testStoreMultipleImagesAsync() async throws { await storeMultipleImages() let diskCachePath = cache.diskStorage.directoryURL.path let files = try FileManager.default.contentsOfDirectory(atPath: diskCachePath) XCTAssertEqual(files.count, testKeys.count) } func testCachedFileExists() { let exp = expectation(description: #function) let key = testKeys[0] let url = URL(string: key)! let exists = cache.imageCachedType(forKey: url.cacheKey).cached XCTAssertFalse(exists) cache.retrieveImage(forKey: key) { result in switch result { case .success(let value): XCTAssertNil(value.image) XCTAssertEqual(value.cacheType, .none) case .failure: XCTFail() return } self.cache.store(testImage, forKey: key, toDisk: true) { _ in self.cache.retrieveImage(forKey: key) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value?.cacheType, .memory) self.cache.clearMemoryCache() self.cache.retrieveImage(forKey: key) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value?.cacheType, .disk) exp.fulfill() } } } } waitForExpectations(timeout: 3, handler: nil) } func testCachedFileExistsAsync() async throws { let key = testKeys[0] let url = URL(string: key)! let exists = cache.imageCachedType(forKey: url.cacheKey).cached XCTAssertFalse(exists) var result = try await cache.retrieveImage(forKey: key) XCTAssertNil(result.image) XCTAssertEqual(result.cacheType, .none) try await cache.store(testImage, forKey: key, toDisk: true) result = try await cache.retrieveImage(forKey: key) XCTAssertNotNil(result.image) XCTAssertEqual(result.cacheType, .memory) cache.clearMemoryCache() result = try await cache.retrieveImage(forKey: key) XCTAssertNotNil(result.image) XCTAssertEqual(result.cacheType, .disk) } func testCachedFileWithCustomPathExtensionExists() { cache.diskStorage.config.pathExtension = "jpg" let exp = expectation(description: #function) let key = testKeys[0] let url = URL(string: key)! cache.store(testImage, forKey: key, toDisk: true) { _ in let cachePath = self.cache.cachePath(forKey: url.cacheKey) XCTAssertTrue(cachePath.hasSuffix(".jpg")) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testCachedFileWithCustomPathExtensionExistsAsync() async throws { cache.diskStorage.config.pathExtension = "jpg" let key = testKeys[0] let url = URL(string: key)! try await cache.store(testImage, forKey: key, toDisk: true) let cachePath = self.cache.cachePath(forKey: url.cacheKey) XCTAssertTrue(cachePath.hasSuffix(".jpg")) } @MainActor func testCachedImageIsFetchedSynchronouslyFromTheMemoryCache() { cache.store(testImage, forKey: testKeys[0], toDisk: false) var image: KFCrossPlatformImage? = nil cache.retrieveImage(forKey: testKeys[0]) { result in MainActor.assumeIsolated { image = try? result.get().image } } XCTAssertEqual(testImage, image) } func testCachedImageIsFetchedSynchronouslyFromTheMemoryCacheAsync() async throws { try await cache.store(testImage, forKey: testKeys[0], toDisk: false) let result = try await cache.retrieveImage(forKey: testKeys[0]) XCTAssertEqual(testImage, result.image) } func testIsImageCachedForKey() { let exp = expectation(description: #function) let key = testKeys[0] XCTAssertFalse(cache.imageCachedType(forKey: key).cached) cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in XCTAssertTrue(self.cache.imageCachedType(forKey: key).cached) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testIsImageCachedForKeyAsync() async throws { let key = testKeys[0] XCTAssertFalse(cache.imageCachedType(forKey: key).cached) try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true) XCTAssertTrue(cache.imageCachedType(forKey: key).cached) } func testCleanDiskCacheNotification() { let exp = expectation(description: #function) let key = testKeys[0] cache.diskStorage.config.expiration = .seconds(0.1) let selfCache = self.cache cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in self.observer = NotificationCenter.default.addObserver( forName: .KingfisherDidCleanDiskCache, object: self.cache, queue: .main ) { noti in let receivedCache = noti.object as? ImageCache XCTAssertNotNil(receivedCache) XCTAssertTrue(receivedCache === selfCache) guard let hashes = noti.userInfo?[KingfisherDiskCacheCleanedHashKey] as? [String] else { XCTFail("Notification should contains Strings in key 'KingfisherDiskCacheCleanedHashKey'") exp.fulfill() return } XCTAssertEqual(hashes.count, 1) XCTAssertEqual(hashes.first!, selfCache!.hash(forKey: key)) exp.fulfill() } delay(2) { // File writing in disk cache has an approximate (round) creating time. 1 second is not enough. self.cache.cleanExpiredDiskCache() } } waitForExpectations(timeout: 5, handler: nil) } func testCannotRetrieveCacheWithProcessorIdentifier() { let exp = expectation(description: #function) let key = testKeys[0] let p = RoundCornerImageProcessor(cornerRadius: 40) cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in self.cache.retrieveImage(forKey: key, options: [.processor(p)]) { result in XCTAssertNotNil(result.value) XCTAssertNil(result.value!.image) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testCannotRetrieveCacheWithProcessorIdentifierAsync() async throws { let key = testKeys[0] let p = RoundCornerImageProcessor(cornerRadius: 40) try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true) let result = try await cache.retrieveImage(forKey: key, options: [.processor(p)]) XCTAssertNotNil(result) XCTAssertNil(result.image) } func testRetrieveCacheWithProcessorIdentifier() { let exp = expectation(description: #function) let key = testKeys[0] let p = RoundCornerImageProcessor(cornerRadius: 40) cache.store( testImage, original: testImageData, forKey: key, processorIdentifier: p.identifier, toDisk: true) { _ in self.cache.retrieveImage(forKey: key, options: [.processor(p)]) { result in XCTAssertNotNil(result.value?.image) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testRetrieveCacheWithProcessorIdentifierAsync() async throws { let key = testKeys[0] let p = RoundCornerImageProcessor(cornerRadius: 40) try await cache.store( testImage, original: testImageData, forKey: key, processorIdentifier: p.identifier, toDisk: true ) let result = try await cache.retrieveImage(forKey: key, options: [.processor(p)]) XCTAssertNotNil(result.image) } func testDefaultCache() { let exp = expectation(description: #function) let key = testKeys[0] let cache = ImageCache.default cache.store(testImage, forKey: key) { _ in XCTAssertTrue(cache.memoryStorage.isCached(forKey: key)) XCTAssertTrue(cache.diskStorage.isCached(forKey: key)) cleanDefaultCache() exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDefaultCacheAsync() async throws { let key = testKeys[0] let cache = ImageCache.default try await cache.store(testImage, forKey: key) XCTAssertTrue(cache.memoryStorage.isCached(forKey: key)) XCTAssertTrue(cache.diskStorage.isCached(forKey: key)) cleanDefaultCache() } func testRetrieveDiskCacheSynchronously() { let exp = expectation(description: #function) let key = testKeys[0] cache.store(testImage, forKey: key, toDisk: true) { _ in var cacheType = self.cache.imageCachedType(forKey: key) XCTAssertEqual(cacheType, .memory) self.cache.memoryStorage.remove(forKey: key) cacheType = self.cache.imageCachedType(forKey: key) XCTAssertEqual(cacheType, .disk) let dispatched = LockIsolated(false) self.cache.retrieveImageInDiskCache(forKey: key, options: [.loadDiskFileSynchronously]) { result in XCTAssertFalse(dispatched.value) exp.fulfill() } // This should be called after the completion handler above. dispatched.setValue(true) } waitForExpectations(timeout: 3, handler: nil) } func testRetrieveDiskCacheAsynchronously() { let exp = expectation(description: #function) let key = testKeys[0] cache.store(testImage, forKey: key, toDisk: true) { _ in var cacheType = self.cache.imageCachedType(forKey: key) XCTAssertEqual(cacheType, .memory) self.cache.memoryStorage.remove(forKey: key) cacheType = self.cache.imageCachedType(forKey: key) XCTAssertEqual(cacheType, .disk) let dispatched = LockIsolated(false) self.cache.retrieveImageInDiskCache(forKey: key, options: nil) { result in XCTAssertTrue(dispatched.value) exp.fulfill() } // This should be called before the completion handler above. dispatched.setValue(true) } waitForExpectations(timeout: 3, handler: nil) } #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) func testModifierShouldOnlyApplyForFinalResultWhenMemoryLoad() { let exp = expectation(description: #function) let key = testKeys[0] let modifierCalled = LockIsolated(false) let modifier = AnyImageModifier { image in modifierCalled.setValue(true) return image.withRenderingMode(.alwaysTemplate) } cache.store(testImage, original: testImageData, forKey: key) { _ in self.cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)]) { result in XCTAssertEqual(result.value?.image?.renderingMode, .automatic) XCTAssertFalse(modifierCalled.value) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testModifierShouldOnlyApplyForFinalResultWhenMemoryLoadAsync() async throws { let key = testKeys[0] let modifierCalled = LockIsolated(false) let modifier = AnyImageModifier { image in modifierCalled.setValue(true) return image.withRenderingMode(.alwaysTemplate) } try await cache.store(testImage, original: testImageData, forKey: key) let result = try await cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)]) XCTAssertFalse(modifierCalled.value) XCTAssertEqual(result.image?.renderingMode, .automatic) } func testModifierShouldOnlyApplyForFinalResultWhenDiskLoad() { let exp = expectation(description: #function) let key = testKeys[0] let modifierCalled = LockIsolated(false) let modifier = AnyImageModifier { image in modifierCalled.setValue(true) return image.withRenderingMode(.alwaysTemplate) } cache.store(testImage, original: testImageData, forKey: key) { _ in self.cache.clearMemoryCache() self.cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)]) { result in XCTAssertEqual(result.value?.image?.renderingMode, .automatic) XCTAssertFalse(modifierCalled.value) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testModifierShouldOnlyApplyForFinalResultWhenDiskLoadAsync() async throws { let key = testKeys[0] let modifierCalled = LockIsolated(false) let modifier = AnyImageModifier { image in modifierCalled.setValue(true) return image.withRenderingMode(.alwaysTemplate) } try await cache.store(testImage, original: testImageData, forKey: key) cache.clearMemoryCache() let result = try await cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)]) XCTAssertFalse(modifierCalled.value) // The renderingMode is expected to be the default value `.automatic`. The image modifier should only apply to // the image manager result. XCTAssertEqual(result.image?.renderingMode, .automatic) } #endif func testStoreToMemoryWithExpiration() { let exp = expectation(description: #function) let key = testKeys[0] cache.store( testImage, original: testImageData, forKey: key, options: KingfisherParsedOptionsInfo([.memoryCacheExpiration(.seconds(0.5))]), toDisk: true) { _ in XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory) delay(1) { XCTAssertEqual(self.cache.imageCachedType(forKey: key), .disk) exp.fulfill() } } waitForExpectations(timeout: 5, handler: nil) } func testStoreToMemoryWithExpirationAsync() async throws { let key = testKeys[0] try await cache.store( testImage, original: testImageData, forKey: key, options: KingfisherParsedOptionsInfo([.memoryCacheExpiration(.seconds(0.5))]), toDisk: true ) XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory) // After 1 sec, the cache only remains on disk. try await Task.sleep(nanoseconds: NSEC_PER_SEC) XCTAssertEqual(self.cache.imageCachedType(forKey: key), .disk) } func testStoreToDiskWithExpiration() { let exp = expectation(description: #function) let key = testKeys[0] cache.store( testImage, original: testImageData, forKey: key, options: KingfisherParsedOptionsInfo([.diskCacheExpiration(.expired)]), toDisk: true) { _ in XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory) self.cache.clearMemoryCache() XCTAssertEqual(self.cache.imageCachedType(forKey: key), .none) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testStoreToDiskWithExpirationAsync() async throws { let key = testKeys[0] try await cache.store( testImage, original: testImageData, forKey: key, options: KingfisherParsedOptionsInfo([.diskCacheExpiration(.expired)]), toDisk: true ) XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory) self.cache.clearMemoryCache() XCTAssertEqual(self.cache.imageCachedType(forKey: key), .none) } func testCalculateDiskStorageSize() { let exp = expectation(description: #function) cache.calculateDiskStorageSize { result in switch result { case .success(let size): XCTAssertEqual(size, 0) self.storeMultipleImages { self.cache.calculateDiskStorageSize { result in switch result { case .success(let size): XCTAssertEqual(size, UInt(testImagePNGData.count * testKeys.count)) case .failure: XCTAssert(false) } exp.fulfill() } } case .failure: XCTAssert(false) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testDiskCacheStillWorkWhenFolderDeletedExternally() { let exp = expectation(description: #function) let key = testKeys[0] let url = URL(string: key)! let exists = cache.imageCachedType(forKey: url.cacheKey) XCTAssertEqual(exists, .none) cache.store(testImage, forKey: key, toDisk: true) { _ in self.cache.retrieveImage(forKey: key) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value?.cacheType, .memory) self.cache.clearMemoryCache() self.cache.retrieveImage(forKey: key) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value?.cacheType, .disk) self.cache.clearMemoryCache() try! FileManager.default.removeItem(at: self.cache.diskStorage.directoryURL) let exists = self.cache.imageCachedType(forKey: url.cacheKey) XCTAssertEqual(exists, .none) self.cache.store(testImage, forKey: key, toDisk: true) { _ in self.cache.clearMemoryCache() let cacheType = self.cache.imageCachedType(forKey: url.cacheKey) XCTAssertEqual(cacheType, .disk) exp.fulfill() } } } } waitForExpectations(timeout: 3, handler: nil) } func testDiskCacheCalculateSizeWhenFolderDeletedExternally() { let exp = expectation(description: #function) let key = testKeys[0] cache.calculateDiskStorageSize { result in XCTAssertEqual(result.value, 0) self.cache.store(testImage, forKey: key, toDisk: true) { _ in self.cache.calculateDiskStorageSize { result in XCTAssertEqual(result.value, UInt(testImagePNGData.count)) try! FileManager.default.removeItem(at: self.cache.diskStorage.directoryURL) self.cache.calculateDiskStorageSize { result in XCTAssertEqual(result.value, 0) exp.fulfill() } } } } waitForExpectations(timeout: 3, handler: nil) } func testCalculateDiskStorageSizeAsync() async throws { let size = try await cache.diskStorageSize XCTAssertEqual(size, 0) await storeMultipleImages() let newSize = try await cache.diskStorageSize XCTAssertEqual(newSize, UInt(testImagePNGData.count * testKeys.count)) } func testStoreFileWithForcedExtension() async throws { let key = testKeys[0] try await cache.store(testImage, forKey: key, forcedExtension: "jpg", toDisk: true) let pathWithoutExtension = cache.cachePath(forKey: key) XCTAssertFalse(FileManager.default.fileExists(atPath: pathWithoutExtension)) let pathWithExtension = cache.cachePath(forKey: key, forcedExtension: "jpg") XCTAssertTrue(FileManager.default.fileExists(atPath: pathWithExtension)) XCTAssertEqual(cache.imageCachedType(forKey: key), .memory) XCTAssertEqual(cache.imageCachedType(forKey: key, forcedExtension: "jpg"), .memory) cache.clearMemoryCache() XCTAssertEqual(cache.imageCachedType(forKey: key), .none) XCTAssertEqual(cache.imageCachedType(forKey: key, forcedExtension: "jpg"), .disk) } func testPossibleCacheFileURLIfOnDiskNotCached() { let url = URL(string: "https://example.com/photo")! let resource = LivePhotoResource(downloadURL: url) let fileURL = cache.possibleCacheFileURLIfOnDisk( forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, referenceFileType: .heic ) // Not cached XCTAssertNil(fileURL) } func testPossibleCacheFileURLIfOnDiskCachedWithWrongFileType() async throws { let url = URL(string: "https://example.com/photo")! let resource = LivePhotoResource(downloadURL: url, fileType: .heic) // Cache without a file type extension try await cache.storeToDisk( testImageData, forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier ) let fileURL = cache.possibleCacheFileURLIfOnDisk( forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, referenceFileType: .heic ) // Not cached XCTAssertNil(fileURL) } func testPossibleCacheFileURLIfOnDiskCachedWithExplicitFileType() async throws { let url = URL(string: "https://example.com/photo")! let resource = LivePhotoResource(downloadURL: url, fileType: .heic) // Cache without a file type extension try await cache.storeToDisk( testImageData, forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: "heic" ) let fileURL = cache.possibleCacheFileURLIfOnDisk( forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, referenceFileType: .heic ) let result = try XCTUnwrap(fileURL) XCTAssertTrue(result.absoluteString.hasSuffix(".heic")) } func testPossibleCacheFileURLIfOnDiskCachedGuessingFileTypeNotHit() async throws { let url = URL(string: "https://example.com/photo")! let resource = LivePhotoResource(downloadURL: url, fileType: .heic) let fileURL = cache.possibleCacheFileURLIfOnDisk( forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, referenceFileType: .other("") ) XCTAssertNil(fileURL) } func testPossibleCacheFileURLIfOnDiskCachedGuessingFileType() async throws { let url = URL(string: "https://example.com/photo")! let resource = LivePhotoResource(downloadURL: url, fileType: .heic) // Cache without a file type extension try await cache.storeToDisk( testImageData, forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: "heic" ) let fileURL = cache.possibleCacheFileURLIfOnDisk( forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, referenceFileType: .other("") ) let result = try XCTUnwrap(fileURL) XCTAssertTrue(result.absoluteString.hasSuffix(".heic")) } func testPossibleCacheFileURLIfOnDiskCachedArbitraryFileType() async throws { let url = URL(string: "https://example.com/photo")! let resource = LivePhotoResource(downloadURL: url, fileType: .heic) // Cache without a file type extension try await cache.storeToDisk( testImageData, forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: "myExt" ) let fileURL = cache.possibleCacheFileURLIfOnDisk( forKey: resource.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, referenceFileType: .other("myExt") ) let result = try XCTUnwrap(fileURL) XCTAssertTrue(result.absoluteString.hasSuffix(".myExt")) } #if !os(macOS) && !os(watchOS) func testKingfisherWrapperUIApplicationSharedReturnsNilInUnitTest() { // UIApplication.shared is not available in some Unit Tests contexts. // This tests that accessing it via KingfisherWrapper does not cause a crash. XCTAssertNil(KingfisherWrapper.shared) } #endif // MARK: - Helper private func storeMultipleImages(_ completionHandler: @escaping () -> Void) { let group = DispatchGroup() testKeys.forEach { group.enter() cache.store(testImage, original: testImageData, forKey: $0, toDisk: true) { _ in group.leave() } } group.notify(queue: .main, execute: completionHandler) } private func storeMultipleImages() async { await withCheckedContinuation { storeMultipleImages($0.resume) } } } @dynamicMemberLookup public final class LockIsolated: @unchecked Sendable { private var _value: Value private let lock = NSRecursiveLock() /// Initializes lock-isolated state around a value. /// /// - Parameter value: A value to isolate with a lock. public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { self._value = try value() } public subscript(dynamicMember keyPath: KeyPath) -> Subject { self.lock.sync { self._value[keyPath: keyPath] } } /// Perform an operation with isolated access to the underlying value. /// /// Useful for modifying a value in a single transaction. /// /// ```swift /// // Isolate an integer for concurrent read/write access: /// var count = LockIsolated(0) /// /// func increment() { /// // Safely increment it: /// self.count.withValue { $0 += 1 } /// } /// ``` /// /// - Parameter operation: An operation to be performed on the the underlying value with a lock. /// - Returns: The result of the operation. public func withValue( _ operation: @Sendable (inout Value) throws -> T ) rethrows -> T { try self.lock.sync { var value = self._value defer { self._value = value } return try operation(&value) } } /// Overwrite the isolated value with a new value. /// /// ```swift /// // Isolate an integer for concurrent read/write access: /// var count = LockIsolated(0) /// /// func reset() { /// // Reset it: /// self.count.setValue(0) /// } /// ``` /// /// > Tip: Use ``withValue(_:)`` instead of ``setValue(_:)`` if the value being set is derived /// > from the current value. That is, do this: /// > /// > ```swift /// > self.count.withValue { $0 += 1 } /// > ``` /// > /// > ...and not this: /// > /// > ```swift /// > self.count.setValue(self.count + 1) /// > ``` /// > /// > ``withValue(_:)`` isolates the entire transaction and avoids data races between reading and /// > writing the value. /// /// - Parameter newValue: The value to replace the current isolated value with. public func setValue(_ newValue: @autoclosure @Sendable () throws -> Value) rethrows { try self.lock.sync { self._value = try newValue() } } } final class ImageCacheAsyncCachedTypeTests: XCTestCase { var cache: ImageCache! override func setUp() { super.setUp() let uuid = UUID().uuidString cache = ImageCache(name: "test-\(uuid)") } override func tearDown() { clearCaches([cache]) cache = nil super.tearDown() } func testImageCachedTypeAsyncMemoryHit() { let expectation = expectation(description: "memory hit") let key = "memory-hit" let computedKey = key.computedKey(with: DefaultImageProcessor.default.identifier) cache.memoryStorage.store(value: testImage, forKey: computedKey) cache.imageCachedTypeAsync(forKey: key, callbackQueue: .untouch) { type in XCTAssertEqual(type, .memory) XCTAssertTrue(Thread.isMainThread) expectation.fulfill() } waitForExpectations(timeout: 2) } func testImageCachedTypeAsyncDiskHitRunsOffMainThreadWhenCallbackUntouch() { let expectation = expectation(description: "disk hit") let key = "disk-hit" let computedKey = key.computedKey(with: DefaultImageProcessor.default.identifier) try? cache.diskStorage.store(value: testImageData, forKey: computedKey, expiration: .never) cache.imageCachedTypeAsync(forKey: key, callbackQueue: .untouch) { type in XCTAssertEqual(type, .disk) XCTAssertFalse(Thread.isMainThread) expectation.fulfill() } waitForExpectations(timeout: 2) } func testImageCachedTypeAsyncNone() { let expectation = expectation(description: "none") let key = "missing" cache.imageCachedTypeAsync(forKey: key, callbackQueue: .untouch) { type in XCTAssertEqual(type, .none) expectation.fulfill() } waitForExpectations(timeout: 2) } func testImageCachedTypeAsyncDeliversOnMainQueueWhenRequested() { let expectation = expectation(description: "main callback") let key = "main-callback" let computedKey = key.computedKey(with: DefaultImageProcessor.default.identifier) try? cache.diskStorage.store(value: testImageData, forKey: computedKey, expiration: .never) DispatchQueue.global().async { self.cache.imageCachedTypeAsync(forKey: key, callbackQueue: .mainAsync) { type in XCTAssertEqual(type, .disk) XCTAssertTrue(Thread.isMainThread) expectation.fulfill() } } waitForExpectations(timeout: 2) } func testImageCachedTypeAsyncAwaitReturns() async { let key = "async-await" let computedKey = key.computedKey(with: DefaultImageProcessor.default.identifier) try? cache.diskStorage.store(value: testImageData, forKey: computedKey, expiration: .never) let type = await cache.imageCachedTypeAsync(forKey: key) XCTAssertEqual(type, .disk) } } extension LockIsolated where Value: Sendable { /// The lock-isolated value. public var value: Value { self.lock.sync { self._value } } } extension NSRecursiveLock { @inlinable @discardableResult @_spi(Internals) public func sync(work: () throws -> R) rethrows -> R { self.lock() defer { self.unlock() } return try work() } } ================================================ FILE: Tests/KingfisherTests/ImageDataProviderTests.swift ================================================ // // ImageDataProviderTests.swift // Kingfisher // // Created by onevcat on 2018/11/18. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class ImageDataProviderTests: XCTestCase { func testCacheKeySelectionForPickerProviders() { #if os(iOS) || os(macOS) || os(visionOS) let uuid: () -> String = { "uuid" } if #available(iOS 16.0, macOS 13.0, *) { XCTAssertEqual( PhotosPickerItemImageDataProvider._cacheKey(providedCacheKey: "custom", itemIdentifier: nil, uuidString: uuid), "custom" ) XCTAssertEqual( PhotosPickerItemImageDataProvider._cacheKey(providedCacheKey: nil, itemIdentifier: "item", uuidString: uuid), "item" ) XCTAssertEqual( PhotosPickerItemImageDataProvider._cacheKey(providedCacheKey: nil, itemIdentifier: nil, uuidString: uuid), "uuid" ) } if #available(iOS 14.0, macOS 13.0, *) { XCTAssertEqual( PHPickerResultImageDataProvider._cacheKey( providedCacheKey: "custom", assetIdentifier: nil, contentTypeIdentifier: "public.image", uuidString: uuid ), "custom" ) XCTAssertEqual( PHPickerResultImageDataProvider._cacheKey( providedCacheKey: nil, assetIdentifier: "asset", contentTypeIdentifier: "public.image", uuidString: uuid ), "asset_public.image" ) XCTAssertEqual( PHPickerResultImageDataProvider._cacheKey( providedCacheKey: nil, assetIdentifier: nil, contentTypeIdentifier: "public.image", uuidString: uuid ), "uuid_public.image" ) } #endif } func testLocalFileImageDataProvider() { let document = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let fileURL = document.appendingPathComponent("test") try! testImageData.write(to: fileURL) let provider = LocalFileImageDataProvider(fileURL: fileURL) XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey) XCTAssertEqual(fileURL.cacheKey, fileURL.localFileCacheKey) XCTAssertEqual(provider.fileURL, fileURL) let exp = expectation(description: #function) provider.data { result in XCTAssertEqual(result.value, testImageData) try! FileManager.default.removeItem(at: fileURL) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testLocalFileImageDataProviderAsync() async { let fm = FileManager.default let document = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let fileURL = document.appendingPathComponent("test") try! testImageData.write(to: fileURL) let provider = LocalFileImageDataProvider(fileURL: fileURL) XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey) XCTAssertEqual(fileURL.cacheKey, fileURL.localFileCacheKey) XCTAssertEqual(provider.fileURL, fileURL) let value = try? await provider.data XCTAssertEqual(value, testImageData) try! fm.removeItem(at: fileURL) } func testLocalFileImageDataProviderMainQueue() { let fm = FileManager.default let document = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let fileURL = document.appendingPathComponent("test") try! testImageData.write(to: fileURL) let provider = LocalFileImageDataProvider(fileURL: fileURL, loadingQueue: .mainCurrentOrAsync) XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey) XCTAssertEqual(provider.fileURL, fileURL) let called = LockIsolated(false) provider.data { result in XCTAssertEqual(result.value, testImageData) try! FileManager.default.removeItem(at: fileURL) called.setValue(true) } XCTAssertTrue(called.value) } func testAVAssetImageDataProviderCacheKeyVariesForRemote() { let remoteURL1 = URL(string: "https://example.com/1/hello.mp4")! let remoteURL2 = URL(string: "https://example.com/2/hello.mp4")! let provider1 = AVAssetImageDataProvider(assetURL: remoteURL1, seconds: 10) XCTAssertEqual(provider1.cacheKey, "https://example.com/1/hello.mp4_10.0") let provider2 = AVAssetImageDataProvider(assetURL: remoteURL2, seconds: 10) XCTAssertNotEqual(provider1.cacheKey, provider2.cacheKey) } // AVAssetImageDataProvider fix for appending to #1825 func testAVAssetImageDataProviderCacheKeyConsistForDifferentAppSandbox() { let localURL1 = URL(string: "file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/DEF/Kingfisher-Demo.app/video/hello.mp4")! let localURL2 = URL(string: "file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/XYZ/Kingfisher-Demo.app/video/hello.mp4")! let provider1 = AVAssetImageDataProvider(assetURL: localURL1, seconds: 10) XCTAssertEqual(provider1.cacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/video/hello.mp4_10.0") let provider2 = AVAssetImageDataProvider(assetURL: localURL2, seconds: 10) XCTAssertEqual(provider1.cacheKey, provider2.cacheKey) } func testLocalFileImageDataProviderMainQueueAsync() async { let fm = FileManager.default let document = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let fileURL = document.appendingPathComponent("test") try! testImageData.write(to: fileURL) let provider = LocalFileImageDataProvider(fileURL: fileURL, loadingQueue: .mainCurrentOrAsync) XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey) XCTAssertEqual(provider.fileURL, fileURL) var called = false let value = try? await provider.data XCTAssertEqual(value, testImageData) try! fm.removeItem(at: fileURL) called = true XCTAssertTrue(called) } func testLocalFileCacheKey() { let url1 = URL(string: "file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/DEF/Kingfisher-Demo.app/images/kingfisher-1.jpg")! XCTAssertEqual(url1.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg") let url2 = URL(string: "file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.app/images/kingfisher-1.jpg")! XCTAssertEqual(url2.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg") let url3 = URL(string: "file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.app/images/kingfisher-1.jpg?foo=bar")! XCTAssertEqual(url3.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg?foo=bar") let url4 = URL(string: "file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.appex/images/kingfisher-1.jpg")! XCTAssertEqual(url4.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.appex/images/kingfisher-1.jpg") let url5 = URL(string: "file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.other/images/kingfisher-1.jpg")! XCTAssertEqual(url5.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.other/images/kingfisher-1.jpg") } func testLocalFileExplicitKey() { let url1 = URL(string: "file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/DEF/Kingfisher-Demo.app/images/kingfisher-1.jpg")! let imageResource = KF.ImageResource(downloadURL: url1, cacheKey: "hello") let source = imageResource.convertToSource() XCTAssertEqual(source.cacheKey, "hello") } func testBase64ImageDataProvider() { let base64String = testImageData.base64EncodedString() let provider = Base64ImageDataProvider(base64String: base64String, cacheKey: "123") XCTAssertEqual(provider.cacheKey, "123") var syncCalled = false provider.data { result in XCTAssertEqual(result.value, testImageData) syncCalled = true } XCTAssertTrue(syncCalled) } } ================================================ FILE: Tests/KingfisherTests/ImageDownloaderTests.swift ================================================ // // ImageDownloaderTests.swift // Kingfisher // // Created by Wei Wang on 15/4/10. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class ImageDownloaderTests: XCTestCase { var downloader: ImageDownloader! override class func setUp() { super.setUp() LSNocilla.sharedInstance().start() } override class func tearDown() { LSNocilla.sharedInstance().stop() super.tearDown() } override func setUp() { super.setUp() downloader = ImageDownloader(name: "test") } override func tearDown() { LSNocilla.sharedInstance().clearStubs() downloader = nil super.tearDown() } func testDownloadAnImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.value) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDownloadAnImageAsync() async throws { let url = testURLs[0] stub(url, data: testImageData) let result = try await downloader.downloadImage(with: url, options: .empty) XCTAssertEqual(result.originalData, testImageData) } func testDownloadMultipleImages() { let exp = expectation(description: #function) let group = DispatchGroup() for url in testURLs { group.enter() stub(url, data: testImageData) downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.value) group.leave() } } group.notify(queue: .main, execute: exp.fulfill) waitForExpectations(timeout: 3, handler: nil) } func testDownloadMultipleImagesAsync() async throws { try await withThrowingTaskGroup(of: ImageLoadingResult.self) { group in for url in testURLs { stub(url, data: testImageData) group.addTask { try await self.downloader.downloadImage(with: url) } } for try await result in group { XCTAssertEqual(result.originalData, testImageData) } } } func testDownloadAnImageWithMultipleCallback() { let exp = expectation(description: #function) let group = DispatchGroup() let url = testURLs[0] stub(url, data: testImageData) for _ in 0...5 { group.enter() downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.value) group.leave() } } group.notify(queue: .main, execute: exp.fulfill) waitForExpectations(timeout: 5, handler: nil) } func testDownloadWithModifyingRequest() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let modifier = URLModifier(url: url) let someURL = URL(string: "some_strange_url")! let task = downloader.downloadImage(with: someURL, options: [.requestModifier(modifier)]) { result in XCTAssertNotNil(result.value) XCTAssertEqual(result.value?.url, url) exp.fulfill() } XCTAssertTrue(task.isInitialized) waitForExpectations(timeout: 3, handler: nil) } func testDownloadWithAsyncModifyingRequest() { let exp = expectation(description: #function) let downloadTaskStarted = LockIsolated(false) let url = testURLs[0] stub(url, data: testImageData) let asyncModifier = AsyncURLModifier(url: url, onDownloadTaskStarted: { task in XCTAssertNotNil(task) downloadTaskStarted.setValue(true) }) let someURL = URL(string: "some_strange_url")! let task = downloader.downloadImage(with: someURL, options: [.requestModifier(asyncModifier)]) { result in XCTAssertNotNil(result.value) XCTAssertEqual(result.value?.url, url) XCTAssertTrue(downloadTaskStarted.value) exp.fulfill() } // The returned task is nil since the download is not starting immediately. XCTAssertFalse(task.isInitialized) waitForExpectations(timeout: 3, handler: nil) } func testDownloadWithModifyingRequestToNil() { let nilModifier = AnyModifier { _ in return nil } let exp = expectation(description: #function) let someURL = URL(string: "some_strange_url")! downloader.downloadImage(with: someURL, options: [.requestModifier(nilModifier)]) { result in XCTAssertNotNil(result.error) guard case .requestError(reason: .emptyRequest) = result.error! else { XCTFail() fatalError() } exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testServerInvalidStatusCode() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, statusCode: 404) downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isInvalidResponseStatusCode(404)) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDownloadResultErrorAndRetry() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, errorCode: -1) downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.error) LSNocilla.sharedInstance().clearStubs() stub(url, data: testImageData) // Retry the download self.downloader.downloadImage(with: url) { result in XCTAssertNil(result.error) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testDownloadEmptyURL() { let exp = expectation(description: #function) let modifier = URLModifier(url: nil) let url = URL(string: "http://onevcat.com")! downloader.downloadImage( with: url, options: [.requestModifier(modifier)], progressBlock: { received, totalSize in XCTFail("The progress block should not be called.") }) { result in XCTAssertNotNil(result.error) if case .requestError(reason: .invalidURL(let request)) = result.error! { XCTAssertNil(request.url) } else { XCTFail() } exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDownloadTaskProperty() { let task = downloader.downloadImage(with: URL(string: "1234")!) XCTAssertNotNil(task, "The task should exist.") } func testCancelDownloadTask() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData, length: 123) let task = downloader.downloadImage( with: url, progressBlock: { _, _ in XCTFail() }) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) delay(0.1) { exp.fulfill() } } XCTAssertTrue(task.isInitialized) task.cancel() _ = stub.go() waitForExpectations(timeout: 3, handler: nil) } func testCancelDownloadTaskAsync() async throws { let url = testURLs[0] let stub = delayedStub(url, data: testImageData, length: 123) let checker = CallingChecker() try await checker.checkCancelBehavior(stub: stub) { _ = try await self.downloader.downloadImage(with: url) } } func testCancelOneDownloadTask() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) let group = DispatchGroup() group.enter() let task1 = downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.error) group.leave() } group.enter() _ = downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.value?.image) group.leave() } task1.cancel() delay(0.1) { _ = stub.go() } group.notify(queue: .main) { delay(0.1) { exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testCancelAllDownloadTasks() { let exp = expectation(description: #function) let url1 = testURLs[0] let stub1 = delayedStub(url1, data: testImageData) let url2 = testURLs[1] let stub2 = delayedStub(url2, data: testImageData) let group = DispatchGroup() let urls = [url1, url1, url2] urls.forEach { group.enter() downloader.downloadImage(with: $0) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) group.leave() } } delay(0.1) { self.downloader.cancelAll() _ = stub1.go() _ = stub2.go() } group.notify(queue: .main) { delay(0.1) { exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testCancelDownloadTaskForURL() { let exp = expectation(description: #function) let url1 = testURLs[0] let stub1 = delayedStub(url1, data: testImageData) let url2 = testURLs[1] let stub2 = delayedStub(url2, data: testImageData) let group = DispatchGroup() group.enter() downloader.downloadImage(with: url1) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) group.leave() } group.enter() downloader.downloadImage(with: url1) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) group.leave() } group.enter() downloader.downloadImage(with: url2) { result in XCTAssertNotNil(result.value) group.leave() } delay(0.1) { self.downloader.cancel(url: url1) _ = stub1.go() _ = stub2.go() } group.notify(queue: .main) { delay(0.1) { exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } // Issue 532 https://github.com/onevcat/Kingfisher/issues/532#issuecomment-305644311 func testCancelThenRestartSameDownload() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData, length: 123) let group = DispatchGroup() group.enter() let downloadTask = downloader.downloadImage( with: url, progressBlock: { _, _ in XCTFail()}) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) group.leave() } XCTAssertTrue(downloadTask.isInitialized) downloadTask.cancel() _ = stub.go() group.enter() downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.value) if let error = result.error { print(error) } group.leave() } group.notify(queue: .main) { delay(0.1) { exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testDownloadTaskNilWithNilURL() { let modifier = URLModifier(url: nil) let downloadTask = downloader.downloadImage(with: URL(string: "url")!, options: [.requestModifier(modifier)]) XCTAssertFalse(downloadTask.isInitialized) } func testDownloadWithProcessor() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let p = RoundCornerImageProcessor(cornerRadius: 40) let roundCornered = testImage.kf.image(withRoundRadius: 40, fit: testImage.kf.size) downloader.downloadImage(with: url, options: [.processor(p)]) { result in XCTAssertNotNil(result.value) let image = result.value!.image XCTAssertFalse(image.renderEqual(to: testImage)) XCTAssertTrue(image.renderEqual(to: roundCornered)) XCTAssertEqual(result.value!.originalData, testImageData) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDownloadWithDifferentProcessors() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) let p1 = RoundCornerImageProcessor(cornerRadius: 40) let roundCornered = testImage.kf.image(withRoundRadius: 40, fit: testImage.kf.size) let p2 = BlurImageProcessor(blurRadius: 3.0) let blurred = testImage.kf.blurred(withRadius: 3.0) let group = DispatchGroup() group.enter() let task1 = downloader.downloadImage(with: url, options: [.processor(p1)]) { result in XCTAssertTrue(result.value!.image.renderEqual(to: roundCornered)) group.leave() } group.enter() let task2 = downloader.downloadImage(with: url, options: [.processor(p2)]) { result in XCTAssertTrue(result.value!.image.renderEqual(to: blurred)) group.leave() } XCTAssertNotNil(task1) XCTAssertEqual(task1.sessionTask?.task, task2.sessionTask?.task) _ = stub.go() group.notify(queue: .main, execute: exp.fulfill) waitForExpectations(timeout: 3, handler: nil) } func testDownloadedDataCouldBeModified() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let modifier = URLNilDataModifier() downloader.delegate = modifier downloader.downloadImage(with: url) { result in XCTAssertNil(result.value) XCTAssertNotNil(result.error) if case .responseError(reason: .dataModifyingFailed) = result.error! { } else { XCTFail() } self.downloader.delegate = nil // hold delegate _ = modifier exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDownloadedDataCouldBeModifiedWithTask() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let modifier = TaskNilDataModifier() downloader.delegate = modifier downloader.downloadImage(with: url) { result in XCTAssertNil(result.value) XCTAssertNotNil(result.error) if case .responseError(reason: .dataModifyingFailed) = result.error! { } else { XCTFail() } self.downloader.delegate = nil // hold delegate _ = modifier exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) func testModifierShouldOnlyApplyForFinalResultWhenDownload() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let modifierCalled = LockIsolated(false) let modifier = AnyImageModifier { image in modifierCalled.setValue(true) return image.withRenderingMode(.alwaysTemplate) } downloader.downloadImage(with: url, options: [.imageModifier(modifier)]) { result in XCTAssertEqual(result.value?.image.renderingMode, .automatic) XCTAssertFalse(modifierCalled.value) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } #endif func testDownloadTaskTakePriorityOption() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let task = downloader.downloadImage(with: url, options: [.downloadPriority(URLSessionTask.highPriority)]) { _ in exp.fulfill() } XCTAssertEqual(task.sessionTask?.task.priority, URLSessionTask.highPriority) waitForExpectations(timeout: 3, handler: nil) } func testSessionDelegate() { class ExtensionDelegate: SessionDelegate, @unchecked Sendable { //'exp' only for test public let exp: XCTestExpectation init(_ expectation:XCTestExpectation) { exp = expectation } override func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { exp.fulfill() } } downloader.sessionDelegate = ExtensionDelegate(expectation(description: #function)) let url = testURLs[0] stub(url, data: testImageData) downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.value) } waitForExpectations(timeout: 3, handler: nil) } func testDownloaderReceiveResponsePass() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, headers: ["someKey": "someValue"]) let handler = TaskResponseCompletion() let obj = NSObject() handler.onReceiveResponse.delegate(on: obj) { (obj, response) in guard let httpResponse = response as? HTTPURLResponse else { XCTFail("Should be an HTTP response.") return .cancel } XCTAssertEqual(httpResponse.statusCode, 200) XCTAssertEqual(httpResponse.url, url) XCTAssertEqual(httpResponse.allHeaderFields["someKey"] as? String, "someValue") return .allow } downloader.delegate = handler downloader.downloadImage(with: url) { result in XCTAssertNotNil(result.value) XCTAssertNil(result.error) self.downloader.delegate = nil // hold delegate _ = handler exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDownloaderReceiveResponseFailure() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, headers: ["someKey": "someValue"]) let handler = TaskResponseCompletion() let obj = NSObject() handler.onReceiveResponse.delegate(on: obj) { (obj, response) in guard let httpResponse = response as? HTTPURLResponse else { XCTFail("Should be an HTTP response.") return .cancel } XCTAssertEqual(httpResponse.statusCode, 200) XCTAssertEqual(httpResponse.url, url) XCTAssertEqual(httpResponse.allHeaderFields["someKey"] as? String, "someValue") return .cancel } downloader.delegate = handler downloader.downloadImage(with: url) { result in XCTAssertNil(result.value) XCTAssertNotNil(result.error) if case .responseError(reason: .cancelledByDelegate) = result.error! { } else { XCTFail() } self.downloader.delegate = nil // hold delegate _ = handler exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDownloadingLivePhotoResources() async throws { let url = testURLs[0] stub(url, data: testImageData) let result = try await downloader.downloadLivePhotoResource(with: url, options: .init(.empty)) XCTAssertEqual(result.originalData, testImageData) XCTAssertEqual(result.url, url) } func testConcurrentDownloadSameURL() { let exp = expectation(description: #function) // Given let url = testURLs[0] stub(url, data: testImageData) final class CallbackCounter: @unchecked Sendable { private let lock = NSLock() private var value = 0 func increment() { lock.lock() value += 1 lock.unlock() } func read() -> Int { lock.lock() let current = value lock.unlock() return current } } let callbackCounter = CallbackCounter() let expectedCount = 10 exp.expectedFulfillmentCount = expectedCount // When DispatchQueue.concurrentPerform(iterations: expectedCount) { index in downloader.downloadImage(with: url) { result in switch result { case .success(let imageResult): XCTAssertNotNil(imageResult.image) XCTAssertEqual(imageResult.url, url) XCTAssertEqual(imageResult.originalData, testImageData) callbackCounter.increment() exp.fulfill() case .failure(let error): XCTFail("Download should succeed: \(error)") exp.fulfill() } } } // Then waitForExpectations(timeout: 3) { _ in let finalCount = callbackCounter.read() XCTAssertEqual(finalCount, expectedCount, "All \(expectedCount) concurrent requests should receive callbacks") } } } class URLNilDataModifier: ImageDownloaderDelegate { func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? { return nil } } class TaskNilDataModifier: ImageDownloaderDelegate { func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with dataTask: SessionDataTask) -> Data? { return nil } } class TaskResponseCompletion: ImageDownloaderDelegate { let onReceiveResponse = Delegate() func imageDownloader(_ downloader: ImageDownloader, didReceive response: URLResponse) async -> URLSession.ResponseDisposition { return onReceiveResponse.call(response)! } } final class URLModifier: ImageDownloadRequestModifier { let url: URL? init(url: URL?) { self.url = url } func modified(for request: URLRequest) -> URLRequest? { var r = request r.url = url return r } } final class AsyncURLModifier: AsyncImageDownloadRequestModifier { let url: URL? let onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)? init(url: URL?, onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)?) { self.url = url self.onDownloadTaskStarted = onDownloadTaskStarted } func modified(for request: URLRequest) async -> URLRequest? { var r = request r.url = url // Simulate an async action try? await Task.sleep(nanoseconds: 1_000_000) return r } } ================================================ FILE: Tests/KingfisherTests/ImageDrawingTests.swift ================================================ // // ImageDrawingTests.swift // Kingfisher // // Created by onevcat on 2018/10/26. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class ImageDrawingTests: XCTestCase { func testImageResizing() { let result = testImage.kf.resize(to: CGSize(width: 20, height: 20)) XCTAssertEqual(result.size, CGSize(width: 20, height: 20)) } func testImageCropping() { let result = testImage.kf.crop(to: CGSize(width: 20, height: 20), anchorOn: .zero) XCTAssertEqual(result.size, CGSize(width: 20, height: 20)) } func testImageScaling() { XCTAssertEqual(testImage.kf.scale, 1) let result = testImage.kf.scaled(to: 2.0) #if os(macOS) // No scale supported on macOS. XCTAssertEqual(result.kf.scale, 1) XCTAssertEqual(result.size.height, testImage.size.height) #else XCTAssertEqual(result.kf.scale, 2) XCTAssertEqual(result.size.height, testImage.size.height / 2) #endif } } ================================================ FILE: Tests/KingfisherTests/ImageExtensionTests.swift ================================================ // // ImageExtensionTests.swift // Kingfisher // // Created by Wei Wang on 15/10/24. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest import ImageIO @testable import Kingfisher class ImageExtensionTests: XCTestCase { func testImageFormat() { var format: ImageFormat format = testImageJEPGData.kf.imageFormat XCTAssertEqual(format, .JPEG) format = testImagePNGData.kf.imageFormat XCTAssertEqual(format, .PNG) format = testImageGIFData.kf.imageFormat XCTAssertEqual(format, .GIF) let raw: [UInt8] = [1, 2, 3, 4, 5, 6, 7, 8] format = Data(raw).kf.imageFormat XCTAssertEqual(format, .unknown) } func testGenerateJPEGImage() { let options = ImageCreatingOptions() let image = KingfisherWrapper.image(data: testImageJEPGData, options: options) XCTAssertNotNil(image) XCTAssertNil(image?.kf.imageFrameCount) XCTAssertTrue(image!.renderEqual(to: KFCrossPlatformImage(data: testImageJEPGData)!)) } func testGenerateGIFImage() { let options = ImageCreatingOptions() let image = KingfisherWrapper.animatedImage(data: testImageGIFData, options: options) XCTAssertNotNil(image) #if os(iOS) || os(tvOS) || os(visionOS) XCTAssertEqual(image!.kf.imageFrameCount!, 8) #else XCTAssertEqual(image!.kf.images!.count, 8) XCTAssertEqual(image!.kf.duration, 0.8, accuracy: 0.001) #endif } #if os(iOS) || os(tvOS) || os(visionOS) func testScaleForGIFImage() { let options = ImageCreatingOptions(scale: 2.0, duration: 0.0, preloadAll: false, onlyFirstFrame: false) let image = KingfisherWrapper.animatedImage(data: testImageGIFData, options: options) XCTAssertNotNil(image) XCTAssertEqual(image!.scale, 2.0) } #endif func testGIFRepresentation() { let options = ImageCreatingOptions() let image = KingfisherWrapper.animatedImage(data: testImageGIFData, options: options)! let data = image.kf.gifRepresentation() XCTAssertNotNil(data) XCTAssertEqual(data?.kf.imageFormat, ImageFormat.GIF) let preloadOptions = ImageCreatingOptions(preloadAll: true) let allLoadImage = KingfisherWrapper.animatedImage(data: data!, options: preloadOptions)! let allLoadData = allLoadImage.kf.gifRepresentation() XCTAssertNotNil(allLoadData) XCTAssertEqual(allLoadData?.kf.imageFormat, ImageFormat.GIF) } func testGenerateSingleFrameGIFImage() { let options = ImageCreatingOptions() let image = KingfisherWrapper.animatedImage(data: testImageSingleFrameGIFData, options: options) XCTAssertNotNil(image) #if os(iOS) || os(tvOS) || os(visionOS) XCTAssertEqual(image!.kf.imageFrameCount!, 1) #else XCTAssertEqual(image!.kf.images!.count, 1) XCTAssertEqual(image!.kf.duration, Double.infinity) #endif } func testGenerateFromNonImage() { let data = "hello".data(using: .utf8)! let options = ImageCreatingOptions() let image = KingfisherWrapper.image(data: data, options: options) XCTAssertNil(image) } func testPreloadAllAnimationData() { let preloadOptions = ImageCreatingOptions(preloadAll: true) let image = KingfisherWrapper.animatedImage(data: testImageSingleFrameGIFData, options: preloadOptions)! XCTAssertNotNil(image, "The image should be initiated.") #if os(iOS) || os(tvOS) || os(visionOS) XCTAssertNil(image.kf.imageSource, "Image source should be nil") #endif XCTAssertEqual(image.kf.duration, image.kf.duration) XCTAssertEqual(image.kf.images!.count, image.kf.images!.count) } func testLoadOnlyFirstFrame() { let preloadOptions = ImageCreatingOptions(preloadAll: true, onlyFirstFrame: true) let image = KingfisherWrapper.animatedImage(data: testImageGIFData, options: preloadOptions)! XCTAssertNotNil(image, "The image should be initiated.") XCTAssertNil(image.kf.images, "The image should be nil") } func testSizeContent() { func getRatio(image: KFCrossPlatformImage) -> CGFloat { return image.size.height / image.size.width } let image = testImage let ratio = getRatio(image: image) let targetSize = CGSize(width: 100, height: 50) let fillImage = image.kf.resize(to: targetSize, for: .aspectFill) XCTAssertEqual(getRatio(image: fillImage), ratio) XCTAssertEqual(max(fillImage.size.width, fillImage.size.height), 100) let fitImage = image.kf.resize(to: targetSize, for: .aspectFit) XCTAssertEqual(getRatio(image: fitImage), ratio) XCTAssertEqual(max(fitImage.size.width, fitImage.size.height), 50) let resizeImage = image.kf.resize(to: targetSize) XCTAssertEqual(resizeImage.size.width, 100) XCTAssertEqual(resizeImage.size.height, 50) } func testSizeConstraintByAnchor() { let size = CGSize(width: 100, height: 100) let topLeft = CGPoint(x: 0, y: 0) let top = CGPoint(x: 0.5, y: 0) let topRight = CGPoint(x: 1, y: 0) let center = CGPoint(x: 0.5, y: 0.5) let bottomRight = CGPoint(x: 1, y: 1) let invalidAnchor = CGPoint(x: -1, y: 2) let inSize = CGSize(width: 20, height: 20) let outX = CGSize(width: 120, height: 20) let outY = CGSize(width: 20, height: 120) let outSize = CGSize(width: 120, height: 120) let kf = size.kf XCTAssertEqual( kf.constrainedRect(for: inSize, anchor: topLeft), CGRect(x: 0, y: 0, width: 20, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outX, anchor: topLeft), CGRect(x: 0, y: 0, width: 100, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outY, anchor: topLeft), CGRect(x: 0, y: 0, width: 20, height: 100)) XCTAssertEqual( kf.constrainedRect(for: outSize, anchor: topLeft), CGRect(x: 0, y: 0, width: 100, height: 100)) XCTAssertEqual( kf.constrainedRect(for: inSize, anchor: top), CGRect(x: 40, y: 0, width: 20, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outX, anchor: top), CGRect(x: 0, y: 0, width: 100, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outY, anchor: top), CGRect(x: 40, y: 0, width: 20, height: 100)) XCTAssertEqual( kf.constrainedRect(for: outSize, anchor: top), CGRect(x: 0, y: 0, width: 100, height: 100)) XCTAssertEqual( kf.constrainedRect(for: inSize, anchor: topRight), CGRect(x: 80, y: 0, width: 20, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outX, anchor: topRight), CGRect(x: 0, y: 0, width: 100, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outY, anchor: topRight), CGRect(x: 80, y: 0, width: 20, height: 100)) XCTAssertEqual( kf.constrainedRect(for: outSize, anchor: topRight), CGRect(x: 0, y: 0, width: 100, height: 100)) XCTAssertEqual( kf.constrainedRect(for: inSize, anchor: center), CGRect(x: 40, y: 40, width: 20, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outX, anchor: center), CGRect(x: 0, y: 40, width: 100, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outY, anchor: center), CGRect(x: 40, y: 0, width: 20, height: 100)) XCTAssertEqual( kf.constrainedRect(for: outSize, anchor: center), CGRect(x: 0, y: 0, width: 100, height: 100)) XCTAssertEqual( kf.constrainedRect(for: inSize, anchor: bottomRight), CGRect(x: 80, y: 80, width: 20, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outX, anchor: bottomRight), CGRect(x: 0, y: 80, width: 100, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outY, anchor: bottomRight), CGRect(x:80, y: 0, width: 20, height: 100)) XCTAssertEqual( kf.constrainedRect(for: outSize, anchor: bottomRight), CGRect(x: 0, y: 0, width: 100, height: 100)) XCTAssertEqual( kf.constrainedRect(for: inSize, anchor: invalidAnchor), CGRect(x: 0, y: 80, width: 20, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outX, anchor: invalidAnchor), CGRect(x: 0, y: 80, width: 100, height: 20)) XCTAssertEqual( kf.constrainedRect(for: outY, anchor: invalidAnchor), CGRect(x:0, y: 0, width: 20, height: 100)) XCTAssertEqual( kf.constrainedRect(for: outSize, anchor: invalidAnchor), CGRect(x: 0, y: 0, width: 100, height: 100)) } func testDecodeScale() { #if os(iOS) || os(tvOS) || os(visionOS) let image = testImage XCTAssertEqual(image.size, CGSize(width: 64, height: 64)) XCTAssertEqual(image.scale, 1.0) let image_2x = KingfisherWrapper.image(cgImage: image.cgImage!, scale: 2.0, refImage: image) XCTAssertEqual(image_2x.size, CGSize(width: 32, height: 32)) XCTAssertEqual(image_2x.scale, 2.0) let decoded = image.kf.decoded XCTAssertEqual(decoded.size, CGSize(width: 64, height: 64)) XCTAssertEqual(decoded.scale, 1.0) let decodedDifferentScale = image.kf.decoded(scale: 2.0) XCTAssertEqual(decodedDifferentScale.size, CGSize(width: 32, height: 32)) XCTAssertEqual(decodedDifferentScale.scale, 2.0) let decoded_2x = image_2x.kf.decoded XCTAssertEqual(decoded_2x.size, CGSize(width: 32, height: 32)) XCTAssertEqual(decoded_2x.scale, 2.0) #endif } func testNormalized() { // Full loaded GIF image should not be normalized since it is a set of images. let options = ImageCreatingOptions() let gifImage = KingfisherWrapper.animatedImage(data: testImageGIFData, options: options) XCTAssertNotNil(gifImage) XCTAssertEqual(gifImage!.kf.normalized, gifImage!) #if os(iOS) || os(tvOS) || os(visionOS) // No need to normalize up orientation image. let normalImage = testImage XCTAssertEqual(normalImage.imageOrientation, .up) XCTAssertEqual(normalImage.kf.normalized, testImage) let colorImage = UIImage.from(color: .red, size: CGSize(width: 100, height: 200)) let rotatedImage = UIImage(cgImage: colorImage.cgImage!, scale: colorImage.scale, orientation: .right) XCTAssertEqual(rotatedImage.imageOrientation, .right) let rotatedNormalizedImage = rotatedImage.kf.normalized XCTAssertEqual(rotatedNormalizedImage.imageOrientation, .up) XCTAssertEqual(rotatedNormalizedImage.size, CGSize(width: 200, height: 100)) #endif } func testDownsampling() { let size = CGSize(width: 15, height: 15) XCTAssertEqual(testImage.size, CGSize(width: 64, height: 64)) XCTAssertEqual(testImage.kf.scale, 1.0) let image = KingfisherWrapper.downsampledImage(data: testImageData, to: size, scale: 1) XCTAssertEqual(image?.size, size) XCTAssertEqual(image?.kf.scale, 1.0) } func testDownsamplingWithScale() { let size = CGSize(width: 15, height: 15) XCTAssertEqual(testImage.size, CGSize(width: 64, height: 64)) XCTAssertEqual(testImage.kf.scale, 1.0) let image2x = KingfisherWrapper.downsampledImage(data: testImageData, to: size, scale: 2) #if os(macOS) XCTAssertEqual(image2x?.size, CGSize(width: 30, height: 30)) XCTAssertEqual(image2x?.kf.scale, 1.0) #else XCTAssertEqual(image2x?.size, size) XCTAssertEqual(image2x?.kf.scale, 2.0) #endif let image3x = KingfisherWrapper.downsampledImage(data: testImageData, to: size, scale: 3) #if os(macOS) XCTAssertEqual(image3x?.size, CGSize(width: 45, height: 45)) XCTAssertEqual(image3x?.kf.scale, 1.0) #else XCTAssertEqual(image3x?.size, size) XCTAssertEqual(image3x?.kf.scale, 3.0) #endif } func testDownsamplingWithEdgeCaseSize() { // Zero size would fail downsampling before iOS 17.4. let result = KingfisherWrapper.downsampledImage(data: testImageData, to: .zero, scale: 1) if #available(iOS 17.4, macOS 14.4, tvOS 17.4, *) { XCTAssertEqual(result?.size, CGSize(width: 64, height: 64)) } else { XCTAssertNil(result) } let largerSize = CGSize(width: 100, height: 100) let largerImage = KingfisherWrapper.downsampledImage(data: testImageData, to: largerSize, scale: 1) // You can not "downsample" an image to a larger size. XCTAssertEqual(largerImage?.size, CGSize(width: 64, height: 64)) } #if os(macOS) func testSVGImageSize() { let svgString = """ """ guard let data = svgString.data(using: .utf8), let image = NSImage(data: data) else { XCTFail("Failed to create image from SVG data") return } let size = image.kf.size XCTAssertEqual(size.width, 100) XCTAssertEqual(size.height, 200) } #endif } #if !os(watchOS) #if canImport(UIKit) import UIKit #endif final class AnimatedImageViewAnimatorTests: XCTestCase { func testAnimatorPurgeFramesKeepsCurrentFrameByDefault() { let source = CGImageSourceCreateWithData(testImageGIFData as CFData, nil)! let queue = DispatchQueue(label: "com.onevcat.KingfisherTests.AnimatorPreload") #if os(macOS) let contentMode: KFCrossPlatformContentMode = .scaleAxesIndependently #else let contentMode: KFCrossPlatformContentMode = .scaleToFill #endif let animator = AnimatedImageView.Animator( imageSource: source, contentMode: contentMode, size: CGSize(width: 40, height: 40), imageSize: CGSize(width: 40, height: 40), imageScale: 1, framePreloadCount: 2, repeatCount: .infinite, preloadQueue: queue ) animator.prepareFramesAsynchronously() queue.sync { } XCTAssertNotNil(animator.frame(at: 0)) XCTAssertNotNil(animator.frame(at: 1)) animator.purgeFrames() queue.sync { } XCTAssertNotNil(animator.frame(at: 0)) XCTAssertNil(animator.frame(at: 1)) } func testAnimatorPurgeFramesCanPurgeCurrentFrame() { let source = CGImageSourceCreateWithData(testImageGIFData as CFData, nil)! let queue = DispatchQueue(label: "com.onevcat.KingfisherTests.AnimatorPreload") #if os(macOS) let contentMode: KFCrossPlatformContentMode = .scaleAxesIndependently #else let contentMode: KFCrossPlatformContentMode = .scaleToFill #endif let animator = AnimatedImageView.Animator( imageSource: source, contentMode: contentMode, size: CGSize(width: 40, height: 40), imageSize: CGSize(width: 40, height: 40), imageScale: 1, framePreloadCount: 2, repeatCount: .infinite, preloadQueue: queue ) animator.prepareFramesAsynchronously() queue.sync { } XCTAssertNotNil(animator.frame(at: 0)) animator.purgeFrames(keepCurrentFrame: false) queue.sync { } XCTAssertNil(animator.frame(at: 0)) } } #if os(iOS) final class AnimatedImageViewBackgroundPurgeTests: XCTestCase { @MainActor func testPurgeFramesOnBackgroundStopsAnimation() { let imageView = AnimatedImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 40)) imageView.purgeFramesOnBackground = true imageView.image = KingfisherWrapper.animatedImage( data: testImageGIFData, options: .init(scale: 1, duration: 0, preloadAll: false, onlyFirstFrame: false) ) imageView.startAnimating() XCTAssertTrue(imageView.isAnimating) NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) RunLoop.main.run(until: Date().addingTimeInterval(0.05)) XCTAssertFalse(imageView.isAnimating) } @MainActor func testPurgeFramesOnBackgroundCanResumeOnForegroundWhenViewAttached() { let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) let host = UIViewController() window.rootViewController = host window.makeKeyAndVisible() let imageView = AnimatedImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 40)) imageView.purgeFramesOnBackground = true imageView.image = KingfisherWrapper.animatedImage( data: testImageGIFData, options: .init(scale: 1, duration: 0, preloadAll: false, onlyFirstFrame: false) ) host.view.addSubview(imageView) imageView.startAnimating() XCTAssertTrue(imageView.isAnimating) NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) RunLoop.main.run(until: Date().addingTimeInterval(0.05)) XCTAssertFalse(imageView.isAnimating) NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil) RunLoop.main.run(until: Date().addingTimeInterval(0.05)) XCTAssertTrue(imageView.isAnimating) } } #endif #endif ================================================ FILE: Tests/KingfisherTests/ImageModifierTests.swift ================================================ // // ImageModifierTests.swift // Kingfisher // // Created by Ethan Gill on 2017/11/29. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest import Kingfisher class ImageModifierTests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testAnyImageModifier() { let m = AnyImageModifier { image in return image } let image = KFCrossPlatformImage(data: testImagePNGData)! let modifiedImage = m.modify(image) XCTAssert(modifiedImage == image) } #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) func testRenderingModeImageModifier() { let m1 = RenderingModeImageModifier(renderingMode: .alwaysOriginal) let image = KFCrossPlatformImage(data: testImagePNGData)! let alwaysOriginalImage = m1.modify(image) XCTAssert(alwaysOriginalImage.renderingMode == .alwaysOriginal) let m2 = RenderingModeImageModifier(renderingMode: .alwaysTemplate) let alwaysTemplateImage = m2.modify(image) XCTAssert(alwaysTemplateImage.renderingMode == .alwaysTemplate) } func testFlipsForRightToLeftLayoutDirectionImageModifier() { let m = FlipsForRightToLeftLayoutDirectionImageModifier() let image = KFCrossPlatformImage(data: testImagePNGData)! let modifiedImage = m.modify(image) XCTAssert(modifiedImage.flipsForRightToLeftLayoutDirection == true) } func testAlignmentRectInsetsImageModifier() { let insets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) let m = AlignmentRectInsetsImageModifier(alignmentInsets: insets) let image = KFCrossPlatformImage(data: testImagePNGData)! let modifiedImage = m.modify(image) XCTAssert(modifiedImage.alignmentRectInsets == insets) } #endif } ================================================ FILE: Tests/KingfisherTests/ImagePrefetcherTests.swift ================================================ // // ImagePrefetcherTests.swift // Kingfisher // // Created by Claire Knight on 24/02/2016 // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher #if os(macOS) import AppKit #else import UIKit #endif class ImagePrefetcherTests: XCTestCase { override class func setUp() { super.setUp() LSNocilla.sharedInstance().start() } override class func tearDown() { LSNocilla.sharedInstance().stop() super.tearDown() } override func setUp() { super.setUp() cleanDefaultCache() } override func tearDown() { cleanDefaultCache() super.tearDown() } func testPrefetchingImages() { let exp = expectation(description: #function) testURLs.forEach { stub($0, data: testImageData) } let progressCalledCount = LockIsolated(0) let prefetcher = ImagePrefetcher( urls: testURLs, options: [.waitForCache], progressBlock: { _, _, _ in progressCalledCount.withValue { $0 += 1 } }) { skippedResources, failedResources, completedResources in XCTAssertEqual(skippedResources.count, 0) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, testURLs.count) XCTAssertEqual(progressCalledCount.value, testURLs.count) for url in testURLs { XCTAssertTrue(KingfisherManager.shared.cache.imageCachedType(forKey: url.absoluteString).cached) } exp.fulfill() } prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } func testCancelPrefetching() { let exp = expectation(description: #function) let stubs = testURLs.map { delayedStub($0, data: testImageData) } let maxConcurrentCount = 2 let prefetcher = ImagePrefetcher( urls: testURLs, options: [.waitForCache], completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(skippedResources.count, 0) XCTAssertEqual(failedResources.count, testURLs.count) XCTAssertEqual(completedResources.count, 0) delay(0.1) { exp.fulfill() } } ) prefetcher.maxConcurrentDownloads = maxConcurrentCount prefetcher.start() DispatchQueue.main.async { prefetcher.stop() stubs.forEach { _ = $0.go() } } waitForExpectations(timeout: 3, handler: nil) } func testPrefetcherCouldSkipCachedImages() { let exp = expectation(description: #function) KingfisherManager.shared.cache.store(KFCrossPlatformImage(), forKey: testKeys[0]) testURLs.forEach { stub($0, data: testImageData) } let prefetcher = ImagePrefetcher( urls: testURLs, options: [.waitForCache], completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(skippedResources.count, 1) XCTAssertEqual(skippedResources[0].downloadURL, testURLs[0]) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, testURLs.count - 1) exp.fulfill() } ) prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } func testPrefetcherForceRefreshDownloadImages() { let exp = expectation(description: #function) KingfisherManager.shared.cache.store(KFCrossPlatformImage(), forKey: testKeys[0]) testURLs.forEach { stub($0, data: testImageData) } let prefetcher = ImagePrefetcher(urls: testURLs, options: [.forceRefresh, .waitForCache], completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(skippedResources.count, 0) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, testURLs.count) exp.fulfill() }) prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } func testPrefetchWithWrongInitParameters() { let exp = expectation(description: #function) let prefetcher = ImagePrefetcher(urls: [], options: [.waitForCache], completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(skippedResources.count, 0) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, 0) exp.fulfill() }) prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } func testFetchWithProcessor() { let exp = expectation(description: #function) testURLs.forEach { stub($0, data: testImageData, length: 123) } let p = RoundCornerImageProcessor(cornerRadius: 20) @Sendable func prefetchAgain() { let progressCalledCount = LockIsolated(0) let prefetcher = ImagePrefetcher( urls: testURLs, options: [.processor(p), .waitForCache], progressBlock: { _, _, _ in progressCalledCount.withValue { $0 += 1 } }) { skippedResources, failedResources, completedResources in XCTAssertEqual(skippedResources.count, testURLs.count) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, 0) XCTAssertEqual(progressCalledCount.value, testURLs.count) for url in testURLs { let cached = KingfisherManager.shared.cache.imageCachedType( forKey: url.absoluteString, processorIdentifier: p.identifier).cached XCTAssertTrue(cached) } exp.fulfill() } prefetcher.start() } let progressCalledCount = LockIsolated(0) let prefetcher = ImagePrefetcher( urls: testURLs, options: [.processor(p), .waitForCache], progressBlock: { _, _, _ in progressCalledCount.withValue { $0 += 1 } }) { skippedResources, failedResources, completedResources in XCTAssertEqual(skippedResources.count, 0) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, testURLs.count) XCTAssertEqual(progressCalledCount.value, testURLs.count) for url in testURLs { let cached = KingfisherManager.shared.cache.imageCachedType( forKey: url.absoluteString, processorIdentifier: p.identifier).cached XCTAssertTrue(cached) } prefetchAgain() } prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } func testAlsoPrefetchToMemory() { let exp = expectation(description: #function) let cache = KingfisherManager.shared.cache let key = testKeys[0] cache.store(KFCrossPlatformImage(), forKey: key) cache.store(testImage, forKey: key) { result in cache.memoryStorage.remove(forKey: key) XCTAssertEqual(cache.imageCachedType(forKey: key), .disk) testURLs.forEach { stub($0, data: testImageData) } let prefetcher = ImagePrefetcher( urls: testURLs, options: [.waitForCache, .alsoPrefetchToMemory], completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(cache.imageCachedType(forKey: key), .memory) XCTAssertEqual(skippedResources.count, 1) XCTAssertEqual(skippedResources[0].downloadURL, testURLs[0]) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, testURLs.count - 1) exp.fulfill() } ) prefetcher.start() } waitForExpectations(timeout: 3, handler: nil) } func testNotPrefetchToMemory() { let exp = expectation(description: #function) let cache = KingfisherManager.shared.cache let key = testKeys[0] cache.store(testImage, forKey: key) { result in cache.memoryStorage.remove(forKey: key) XCTAssertEqual(cache.imageCachedType(forKey: key), .disk) testURLs.forEach { stub($0, data: testImageData) } let prefetcher = ImagePrefetcher( urls: testURLs, options: [.waitForCache], completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(cache.imageCachedType(forKey: key), .disk) XCTAssertEqual(skippedResources.count, 1) XCTAssertEqual(skippedResources[0].downloadURL, testURLs[0]) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, testURLs.count - 1) exp.fulfill() } ) prefetcher.start() } waitForExpectations(timeout: 3, handler: nil) } func testPrefetchMoreTaskThanMaxConcurrency() { let exp = expectation(description: #function) testURLs.forEach { stub($0, data: testImageData) } let prefetcher = ImagePrefetcher( urls: testURLs, options: [.waitForCache], completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(skippedResources.count, 0) XCTAssertEqual(failedResources.count, 0) XCTAssertEqual(completedResources.count, testURLs.count) exp.fulfill() } ) prefetcher.maxConcurrentDownloads = 1 prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } func testPrefetchMultiTimes() { let exp = expectation(description: #function) let group = DispatchGroup() testURLs.forEach { stub($0, data: testImageData) } for _ in 0..<10000 { group.enter() let prefetcher = ImagePrefetcher( resources: testURLs, options: [.cacheMemoryOnly], completionHandler: { _, _, _ in group.leave() } ) prefetcher.start() } group.notify(queue: .main) { exp.fulfill() } waitForExpectations(timeout: 15, handler: nil) } func testPrefetchSources() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let sources: [Source] = [ .provider(SimpleImageDataProvider(cacheKey: "1") { .success(testImageData) }), .provider(SimpleImageDataProvider(cacheKey: "2") { .success(testImageData) }), .network(url) ] let counter = LockIsolated(0) let prefetcher = ImagePrefetcher( sources: sources, options: [.waitForCache], progressBlock: { skipped, failed, completed in counter.withValue { $0 += 1 } XCTAssertEqual(skipped.count, 0) XCTAssertEqual(failed.count, 0) XCTAssertEqual(completed.count, counter.value) }, completionHandler: { skipped, failed, completed in XCTAssertEqual(skipped.count, 0) XCTAssertEqual(failed.count, 0) XCTAssertEqual(completed.count, sources.count) XCTAssertEqual(counter.value, sources.count) let allCached = [ImageCache.default.isCached(forKey: "1"), ImageCache.default.isCached(forKey: "2"), ImageCache.default.isCached(forKey: url.absoluteString) ].allSatisfy { $0 == true } XCTAssertTrue(allCached) exp.fulfill() }) prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } } ================================================ FILE: Tests/KingfisherTests/ImageProcessorTests.swift ================================================ // // ImageProcessorTests.swift // Kingfisher // // Created by onevcat on 2019/03/02. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class ImageProcessorTests: XCTestCase { // Issue 1125. https://github.com/onevcat/Kingfisher/issues/1125 func testDownsamplingSizes() { XCTAssertEqual(testImage.size, CGSize(width: 64, height: 64)) let emptyOption = KingfisherParsedOptionsInfo(nil) let targetSize = CGSize(width: 20, height: 40) let downsamplingProcessor = DownsamplingImageProcessor(size: targetSize) let resultFromData = downsamplingProcessor.process(item: .data(testImageData), options: emptyOption) XCTAssertEqual(resultFromData!.size, CGSize(width: 40, height: 40)) let resultFromImage = downsamplingProcessor.process(item: .image(testImage), options: emptyOption) XCTAssertEqual(resultFromImage!.size, CGSize(width: 40, height: 40)) } func testProcessorConcating() { let p1 = BlurImageProcessor(blurRadius: 10) let p2 = RoundCornerImageProcessor(cornerRadius: 10) let p3 = TintImageProcessor(tint: .blue) let two = p1 |> p2 let three = p1 |> p2 |> p3 XCTAssertNotNil(two) XCTAssertNotNil(three) } func testParsingColorRGBA() { let sRGB = KFCrossPlatformColor(red: 0.5, green: 0.6, blue: 0.7, alpha: 0.8) let rgba = sRGB.rgba XCTAssertEqual(rgba.r, 0.5, accuracy: 0.01) XCTAssertEqual(rgba.g, 0.6, accuracy: 0.01) XCTAssertEqual(rgba.b, 0.7, accuracy: 0.01) XCTAssertEqual(rgba.a, 0.8, accuracy: 0.01) let extended = KFCrossPlatformColor(displayP3Red: 0, green: 1, blue: 0, alpha: 0.8) let rgbaExt = extended.rgba // extended sRGB XCTAssertTrue(rgbaExt.r < 0) XCTAssertTrue(rgbaExt.g > 1) XCTAssertTrue(rgbaExt.b < 0) XCTAssertEqual(rgbaExt.a, 0.8) let blackWhite = KFCrossPlatformColor(white: 0.3, alpha: 1.0) let rgbaBlackWhite = blackWhite.rgba XCTAssertEqual(rgbaBlackWhite.r, 0.3, accuracy: 0.01) XCTAssertEqual(rgbaBlackWhite.g, 0.3, accuracy: 0.01) XCTAssertEqual(rgbaBlackWhite.b, 0.3, accuracy: 0.01) XCTAssertEqual(rgbaBlackWhite.a, 1.0, accuracy: 0.01) } } ================================================ FILE: Tests/KingfisherTests/ImageViewExtensionTests.swift ================================================ // // UIImageViewExtensionTests.swift // Kingfisher // // Created by Wei Wang on 15/4/17. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class ImageViewExtensionTests: XCTestCase { var imageView: KFCrossPlatformImageView! override class func setUp() { super.setUp() LSNocilla.sharedInstance().start() } override class func tearDown() { LSNocilla.sharedInstance().stop() super.tearDown() } override func setUp() { super.setUp() imageView = KFCrossPlatformImageView() KingfisherManager.shared.downloader = ImageDownloader(name: "testDownloader") KingfisherManager.shared.defaultOptions = [.waitForCache] cleanDefaultCache() } override func tearDown() { LSNocilla.sharedInstance().clearStubs() imageView = nil cleanDefaultCache() KingfisherManager.shared.defaultOptions = .empty super.tearDown() } @MainActor func testImageDownloadForImageView() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) var progressBlockIsCalled = false imageView.kf.setImage( with: url, progressBlock: { _, _ in progressBlockIsCalled = true XCTAssertTrue(Thread.isMainThread) }) { result in XCTAssertTrue(progressBlockIsCalled) XCTAssertNotNil(result.value) let value = result.value! XCTAssertTrue(value.image.renderEqual(to: testImage)) XCTAssertTrue(self.imageView.image!.renderEqual(to: testImage)) XCTAssertEqual(value.cacheType, .none) XCTAssertTrue(Thread.isMainThread) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageDownloadCompletionHandlerRunningOnMainQueue() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let customQueue = DispatchQueue(label: "com.kingfisher.testQueue") imageView.kf.setImage( with: url, options: [.callbackQueue(.dispatch(customQueue))], progressBlock: { _, _ in XCTAssertTrue(Thread.isMainThread) }) { result in XCTAssertTrue(Thread.isMainThread) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageDownloadWithResourceForImageView() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) var progressBlockIsCalled = false let resource = KF.ImageResource(downloadURL: url) imageView.kf.setImage( with: resource, progressBlock: { _, _ in progressBlockIsCalled = true }) { result in XCTAssertTrue(progressBlockIsCalled) XCTAssertNotNil(result.value) let value = result.value! XCTAssertTrue(value.image.renderEqual(to: testImage)) XCTAssertTrue(self.imageView.image!.renderEqual(to: testImage)) XCTAssertEqual(value.cacheType, .none) XCTAssertTrue(Thread.isMainThread) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageDownloadCancelForImageView() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData, length: 123) let task = imageView.kf.setImage( with: url, progressBlock: { _, _ in XCTFail() }) { result in XCTAssertNotNil(result.error) delay(0.1) { exp.fulfill() } } XCTAssertNotNil(task) task?.cancel() _ = stub.go() waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageDownloadCancelPartialTaskBeforeRequest() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) let group = DispatchGroup() group.enter() let task1 = KF.url(url) .onFailure { _ in group.leave() } .set(to: imageView) group.enter() KF.url(url) .onSuccess { _ in group.leave() } .set(to: imageView) group.enter() let anotherImageView = KFCrossPlatformImageView() KF.url(url) .onSuccess { _ in group.leave() } .set(to: anotherImageView) task1?.cancel() _ = stub.go() group.notify(queue: .main) { delay(0.1) { exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageDownloadCancelAllTasksAfterRequestStarted() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) let group = DispatchGroup() group.enter() let task1 = imageView.kf.setImage(with: url) { result in XCTAssertNotNil(result.error) group.leave() } group.enter() let task2 = imageView.kf.setImage(with: url) { result in XCTAssertNotNil(result.error) group.leave() } group.enter() let task3 = imageView.kf.setImage(with: url) { result in XCTAssertNotNil(result.error) group.leave() } task1?.cancel() task2?.cancel() task3?.cancel() _ = stub.go() group.notify(queue: .main) { delay(0.1) { exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageDownloadMultipleCaches() { let cache1 = ImageCache(name: "cache1") let cache2 = ImageCache(name: "cache2") cache1.clearDiskCache() cache2.clearDiskCache() cleanDefaultCache() let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let key = url.cacheKey imageView.kf.setImage(with: url, options: [.targetCache(cache1)]) { result in XCTAssertTrue(cache1.imageCachedType(forKey: key).cached) XCTAssertFalse(cache2.imageCachedType(forKey: key).cached) XCTAssertFalse(KingfisherManager.shared.cache.imageCachedType(forKey: key).cached) self.imageView.kf.setImage(with: url, options: [.targetCache(cache2), .waitForCache]) { result in XCTAssertTrue(cache1.imageCachedType(forKey: key).cached) XCTAssertTrue(cache2.imageCachedType(forKey: key).cached) XCTAssertFalse(KingfisherManager.shared.cache.imageCachedType(forKey: key).cached) exp.fulfill() } } waitForExpectations(timeout: 5) { error in clearCaches([cache1, cache2]) } } @MainActor func testIndicatorViewExisting() { imageView.kf.indicatorType = .activity XCTAssertNotNil(imageView.kf.indicator) XCTAssertTrue(imageView.kf.indicator is ActivityIndicator) imageView.kf.indicatorType = .none XCTAssertNil(imageView.kf.indicator) } @MainActor func testCustomizeStructIndicatorExisting() { struct StructIndicator: Indicator { let view = KFCrossPlatformView() func startAnimatingView() {} func stopAnimatingView() {} } imageView.kf.indicatorType = .custom(indicator: StructIndicator()) XCTAssertNotNil(imageView.kf.indicator) XCTAssertTrue(imageView.kf.indicator is StructIndicator) imageView.kf.indicatorType = .none XCTAssertNil(imageView.kf.indicator) } @MainActor func testActivityIndicatorViewAnimating() { imageView.kf.indicatorType = .activity let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) imageView.kf.setImage(with: url, progressBlock: { receivedSize, totalSize in let indicator = self.imageView.kf.indicator XCTAssertNotNil(indicator) XCTAssertFalse(indicator!.view.isHidden) }) { result in let indicator = self.imageView.kf.indicator XCTAssertTrue(indicator!.view.isHidden) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testCanUseImageIndicatorViewAnimating() { imageView.kf.indicatorType = .image(imageData: testImageData) XCTAssertTrue(imageView.kf.indicator is ImageIndicator) let image = (imageView.kf.indicator?.view as? KFCrossPlatformImageView)?.image XCTAssertNotNil(image) XCTAssertTrue(image!.renderEqual(to: testImage)) let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) imageView.kf.setImage(with: url, progressBlock: { receivedSize, totalSize in let indicator = self.imageView.kf.indicator XCTAssertNotNil(indicator) XCTAssertFalse(indicator!.view.isHidden) }) { result in let indicator = self.imageView.kf.indicator XCTAssertTrue(indicator!.view.isHidden) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testCancelImageTask() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) imageView.kf.setImage(with: url, progressBlock: { _, _ in XCTFail() }) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) delay(0.1) { exp.fulfill() } } self.imageView.kf.cancelDownloadTask() _ = stub.go() waitForExpectations(timeout: 3, handler: nil) } @MainActor func testDownloadForMultipleURLs() { let exp = expectation(description: #function) stub(testURLs[0], data: testImageData) stub(testURLs[1], data: testImageData) let group = DispatchGroup() group.enter() imageView.kf.setImage(with: testURLs[0]) { result in // The download succeeded, but not with the resource we want. XCTAssertNotNil(result.error) if case .imageSettingError( reason: .notCurrentSourceTask(let result, _, let source)) = result.error! { XCTAssertEqual(source.url, testURLs[0]) XCTAssertEqual(result?.originalSource.url, testURLs[0]) XCTAssertNotEqual(result!.image, self.imageView.image) } else { XCTFail() } group.leave() } group.enter() self.imageView.kf.setImage(with: testURLs[1]) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value?.source.url, testURLs[1]) XCTAssertEqual(result.value!.image, self.imageView.image) group.leave() } group.notify(queue: .main, execute: exp.fulfill) waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingNilURL() { let exp = expectation(description: #function) let url: URL? = nil imageView.kf.setImage(with: url, progressBlock: { _, _ in XCTFail() }) { result in XCTAssertNotNil(result.error) guard case .imageSettingError(reason: .emptySource) = result.error! else { XCTFail() fatalError() } exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingImageWhileKeepingCurrentOne() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) imageView.image = testImage imageView.kf.setImage(with: url) { result in } XCTAssertNil(imageView.image) imageView.image = testImage imageView.kf.setImage(with: url, options: [.keepCurrentImageWhileLoading]) { result in XCTAssertEqual(self.imageView.image, result.value!.image) XCTAssertNotEqual(self.imageView.image, testImage) exp.fulfill() } XCTAssertEqual(testImage, imageView.image) waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingImageKeepingRespectingPlaceholder() { let exp = expectation(description: #function) // While current image is nil, set placeholder let url = testURLs[0] imageView.kf.setImage(with: url, placeholder: testImage, options: [.keepCurrentImageWhileLoading]) { result in exp.fulfill() } XCTAssertEqual(testImage, imageView.image) waitForExpectations(timeout: 3, handler: nil) } @MainActor func testMe() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) // While current image is not nil, keep it let anotherImage = KFCrossPlatformImage(data: testImageJEPGData) imageView.image = anotherImage imageView.kf.setImage(with: url, placeholder: testImage, options: [.keepCurrentImageWhileLoading]) { result in XCTAssertNotEqual(self.imageView.image, anotherImage) exp.fulfill() } XCTAssertEqual(anotherImage, imageView.image) waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSetGIFImageOnlyFirstFrameThenFullFrames() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageGIFData, length: 123) func loadFullGIFImage() { ImageCache.default.clearMemoryCache() imageView.kf.setImage(with: url, progressBlock: { _, _ in XCTFail() }) { result in let image = result.value?.image XCTAssertNotNil(image) XCTAssertNotNil(image!.kf.images) XCTAssertEqual(image!.kf.images?.count, 8) XCTAssertEqual(result.value!.cacheType, .disk) XCTAssertTrue(Thread.isMainThread) exp.fulfill() } } var progressBlockIsCalled = false imageView.kf.setImage(with: url, options: [.onlyLoadFirstFrame, .waitForCache], progressBlock: { _, _ in progressBlockIsCalled = true XCTAssertTrue(Thread.isMainThread) }) { result in XCTAssertTrue(progressBlockIsCalled) let image = result.value?.image XCTAssertNotNil(image) XCTAssertNil(image!.kf.images) XCTAssert(result.value!.cacheType == .none) let memory = KingfisherManager.shared.cache.memoryStorage.value(forKey: url.cacheKey) XCTAssertNotNil(memory) let disk = try! KingfisherManager.shared.cache.diskStorage.value(forKey: url.cacheKey) XCTAssertNotNil(disk) XCTAssertTrue(Thread.isMainThread) loadFullGIFImage() } waitForExpectations(timeout: 3, handler: nil) } // https://github.com/onevcat/Kingfisher/issues/1923 @MainActor func testLoadGIFImageWithDifferentOptions() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageGIFData) imageView.kf.setImage(with: url) { result in let fullImage = result.value?.image XCTAssertNotNil(fullImage) XCTAssertEqual(fullImage!.kf.images?.count, 8) self.imageView.kf.setImage(with: url, options: [.onlyLoadFirstFrame]) { result in let firstFrameImage = result.value?.image XCTAssertNotNil(firstFrameImage) XCTAssertNil(firstFrameImage!.kf.images) exp.fulfill() } } waitForExpectations(timeout: 3) } // https://github.com/onevcat/Kingfisher/issues/665 // The completion handler should be called even when the image view loading url gets changed. @MainActor func testIssue665() { let exp = expectation(description: #function) stub(testURLs[0], data: testImageData) stub(testURLs[1], data: testImageData) let group = DispatchGroup() group.enter() imageView.kf.setImage(with: testURLs[0]) { _ in group.leave() } group.enter() imageView.kf.setImage(with: testURLs[1]) { _ in group.leave() } group.notify(queue: .main, execute: exp.fulfill) waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageSettingWithPlaceholder() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) let emptyImage = KFCrossPlatformImage() var processBlockCalled = false imageView.kf.setImage( with: url, placeholder: emptyImage, progressBlock: { _, _ in processBlockCalled = true XCTAssertEqual(self.imageView.image, emptyImage) }) { result in XCTAssertTrue(processBlockCalled) XCTAssertTrue(self.imageView.image!.renderEqual(to: testImage)) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageSettingWithCustomizePlaceholder() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) let view = KFCrossPlatformView() var processBlockCalled = false imageView.kf.setImage( with: url, placeholder: view, progressBlock: { _, _ in processBlockCalled = true XCTAssertNil(self.imageView.image) XCTAssertTrue(self.imageView.subviews.contains(view)) }) { result in XCTAssertTrue(processBlockCalled) XCTAssertTrue(self.imageView.image!.renderEqual(to: testImage)) XCTAssertFalse(self.imageView.subviews.contains(view)) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingNonWorkingImageWithCustomizePlaceholderAndFailureImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, errorCode: 404) let view = KFCrossPlatformView() imageView.kf.setImage( with: url, placeholder: view, options: [.onFailureImage(testImage)]) { result in XCTAssertEqual(self.imageView.image, testImage) XCTAssertFalse(self.imageView.subviews.contains(view)) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingNonWorkingImageWithFailureImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, errorCode: 404) imageView.kf.setImage(with: url, options: [.onFailureImage(testImage)]) { result in XCTAssertNil(result.value) if case KingfisherError.responseError(let reason) = result.error!, case .URLSessionError(error: let nsError) = reason { XCTAssertEqual((nsError as NSError).code, 404) } else { XCTFail() } XCTAssertEqual(self.imageView.image, testImage) exp.fulfill() } XCTAssertNil(imageView.image) waitForExpectations(timeout: 5, handler: nil) } @MainActor func testSettingNonWorkingImageWithEmptyFailureImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, errorCode: 404) imageView.kf.setImage(with: url, placeholder: testImage, options: [.onFailureImage(nil)]) { result in XCTAssertNil(result.value) XCTAssertNil(self.imageView.image) exp.fulfill() } XCTAssertEqual(testImage, imageView.image) waitForExpectations(timeout: 5, handler: nil) } @MainActor func testSettingNonWorkingImageWithoutFailureImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, errorCode: 404) imageView.kf.setImage(with: url, placeholder: testImage) { result in XCTAssertNil(result.value) XCTAssertEqual(testImage, self.imageView.image) exp.fulfill() } XCTAssertEqual(testImage, imageView.image) waitForExpectations(timeout: 5, handler: nil) } // https://github.com/onevcat/Kingfisher/issues/1053 @MainActor func testSetSameURLWithDifferentProcessors() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let size1 = CGSize(width: 10, height: 10) let p1 = ResizingImageProcessor(referenceSize: size1) let size2 = CGSize(width: 20, height: 20) let p2 = ResizingImageProcessor(referenceSize: size2) let group = DispatchGroup() group.enter() imageView.kf.setImage(with: url, options: [.processor(p1), .cacheMemoryOnly]) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isNotCurrentTask) group.leave() } group.enter() imageView.kf.setImage(with: url, options: [.processor(p2), .cacheMemoryOnly]) { result in XCTAssertNotNil(result.value) XCTAssertEqual(result.value!.image.size, size2) group.leave() } group.notify(queue: .main) { exp.fulfill() } waitForExpectations(timeout: 5, handler: nil) } @MainActor func testMemoryImageCacheExtendingExpirationTask() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let options: KingfisherOptionsInfo = [.cacheMemoryOnly, .memoryCacheExpiration(.seconds(1)), .memoryCacheAccessExtendingExpiration(.expirationTime(.seconds(100)))] imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .none) let cacheKey = result.value!.source.cacheKey as NSString let expirationTime1 = ImageCache.default.memoryStorage.storage.object(forKey: cacheKey)?.estimatedExpiration XCTAssertNotNil(expirationTime1) delay(0.1, block: { self.imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .memory) let expirationTime2 = ImageCache.default.memoryStorage.storage.object(forKey: cacheKey)?.estimatedExpiration XCTAssertNotNil(expirationTime2) XCTAssertNotEqual(expirationTime1, expirationTime2) XCTAssert(expirationTime1!.isPast(referenceDate: expirationTime2!)) XCTAssertGreaterThan(expirationTime2!.timeIntervalSince(expirationTime1!), 10) exp.fulfill() } }) } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testMemoryImageCacheNotExtendingExpirationTask() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let options: KingfisherOptionsInfo = [.cacheMemoryOnly, .memoryCacheExpiration(.seconds(1)), .memoryCacheAccessExtendingExpiration(.none)] imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .none) let cacheKey = result.value!.source.cacheKey as NSString let expirationTime1 = ImageCache.default.memoryStorage.storage.object(forKey: cacheKey)?.estimatedExpiration XCTAssertNotNil(expirationTime1) delay(0.1, block: { self.imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .memory) let expirationTime2 = ImageCache.default.memoryStorage.storage.object(forKey: cacheKey)?.estimatedExpiration XCTAssertNotNil(expirationTime2) XCTAssertEqual(expirationTime1, expirationTime2) exp.fulfill() } }) } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testDiskImageCacheExtendingExpirationTask() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let options: KingfisherOptionsInfo = [.memoryCacheExpiration(.expired), .diskCacheExpiration(.seconds(2)), .diskCacheAccessExtendingExpiration(.expirationTime(.seconds(100)))] imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .none) delay(1, block: { self.imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .disk) delay(2, block: { self.imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .disk) exp.fulfill() } }) } }) } waitForExpectations(timeout: 5, handler: nil) } @MainActor func testDiskImageCacheNotExtendingExpirationTask() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let options: KingfisherOptionsInfo = [.memoryCacheExpiration(.expired), .diskCacheExpiration(.seconds(2)), .diskCacheAccessExtendingExpiration(.none)] imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .none) delay(1, block: { self.imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .disk) delay(2, block: { self.imageView.kf.setImage(with: url, options: options) { result in XCTAssertNotNil(result.value?.image) XCTAssertTrue(result.value!.cacheType == .none) exp.fulfill() } }) } }) } waitForExpectations(timeout: 5, handler: nil) } @MainActor func testImageSettingWithAlternativeSource() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) imageView.kf.setImage( with: .network(brokenURL), options: [.alternativeSources([.network(url)])] ) { result in XCTAssertNotNil(result.value) XCTAssertEqual(result.value!.source.url, url) XCTAssertEqual(result.value!.originalSource.url, brokenURL) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testImageSettingCanCancelAlternativeSource() { let exp = expectation(description: #function) let url = testURLs[0] let dataStub = delayedStub(url, data: testImageData) let brokenURL = testURLs[1] let brokenStub = delayedStub(brokenURL, data: Data()) var finishCalled = false delay(0.1) { _ = brokenStub.go() } delay(0.3) { self.imageView.kf.cancelDownloadTask() } delay(0.5) { _ = dataStub.go() XCTAssertTrue(finishCalled) exp.fulfill() } imageView.kf.setImage( with: .network(brokenURL), options: [.alternativeSources([.network(url)])] ) { result in finishCalled = true XCTAssertNotNil(result.error) guard case .requestError(reason: .taskCancelled(let task, _)) = result.error! else { XCTFail("The error should be a task cancelled.") return } XCTAssertEqual(task.task.originalRequest?.url, url, "Should be the alternative url cancelled.") } waitForExpectations(timeout: 3, handler: nil) } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @MainActor func testLowDataModeSource() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) // Stub a failure of `.constrained`. It is what happens when an image downloading fails when low data mode on. let brokenURL = testURLs[1] let error = URLError( .notConnectedToInternet, userInfo: [NSURLErrorNetworkUnavailableReasonKey: URLError.NetworkUnavailableReason.constrained.rawValue] ) stub(brokenURL, error: error) imageView.kf.setImage(with: .network(brokenURL), options: [.lowDataMode(.network(url))]) { result in XCTAssertNotNil(result.value) XCTAssertEqual(result.value?.source.url, url) XCTAssertEqual(result.value?.originalSource.url, brokenURL) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } } #if compiler(>=6) extension KFCrossPlatformView: @retroactive Placeholder {} #else extension KFCrossPlatformView: Placeholder {} #endif ================================================ FILE: Tests/KingfisherTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 8.8.0 CFBundleSignature ???? CFBundleVersion 3260 ================================================ FILE: Tests/KingfisherTests/KingfisherManagerTests.swift ================================================ // // KingfisherManagerTests.swift // Kingfisher // // Created by Wei Wang on 15/10/22. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher actor CallingChecker { var called = false func mark() { called = true } func checkCancelBehavior( stub: LSStubResponseDSL, block: @escaping () async throws -> Void ) async throws { let task = Task { do { _ = try await block() XCTFail() } catch { mark() XCTAssertTrue((error as! KingfisherError).isTaskCancelled) } } try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10) task.cancel() _ = stub.go() try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10) XCTAssertTrue(called) } } class KingfisherManagerTests: XCTestCase { var manager: KingfisherManager! override class func setUp() { super.setUp() LSNocilla.sharedInstance().start() } override class func tearDown() { LSNocilla.sharedInstance().stop() super.tearDown() } override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. let uuid = UUID() let downloader = ImageDownloader(name: "test.manager.\(uuid.uuidString)") let cache = ImageCache(name: "test.cache.\(uuid.uuidString)") manager = KingfisherManager(downloader: downloader, cache: cache) manager.defaultOptions = [.waitForCache] } override func tearDown() { LSNocilla.sharedInstance().clearStubs() clearCaches([manager.cache]) cleanDefaultCache() manager = nil super.tearDown() } func testRetrieveImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let manager = self.manager! manager.retrieveImage(with: url) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .none) manager.retrieveImage(with: url) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .memory) manager.cache.clearMemoryCache() manager.retrieveImage(with: url) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .disk) manager.cache.clearMemoryCache() manager.cache.clearDiskCache { manager.retrieveImage(with: url) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .none) exp.fulfill() }}}}} waitForExpectations(timeout: 3, handler: nil) } func testRetrieveImageAsync() async throws { let url = testURLs[0] stub(url, data: testImageData) let manager = self.manager! var result = try await manager.retrieveImage(with: url) XCTAssertNotNil(result.image) XCTAssertEqual(result.cacheType, .none) result = try await manager.retrieveImage(with: url) XCTAssertNotNil(result.image) XCTAssertEqual(result.cacheType, .memory) manager.cache.clearMemoryCache() result = try await manager.retrieveImage(with: url) XCTAssertNotNil(result.image) XCTAssertEqual(result.cacheType, .disk) manager.cache.clearMemoryCache() await manager.cache.clearDiskCache() result = try await manager.retrieveImage(with: url) XCTAssertNotNil(result.image) XCTAssertEqual(result.cacheType, .none) } func testRetrieveImageWithProcessor() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let p = RoundCornerImageProcessor(cornerRadius: 20) let manager = self.manager! manager.retrieveImage(with: url, options: [.processor(p)]) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .none) manager.retrieveImage(with: url) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .none, "Need a processor to get correct image. Cannot get from cache, need download again.") manager.retrieveImage(with: url, options: [.processor(p)]) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .memory) self.manager.cache.clearMemoryCache() manager.retrieveImage(with: url, options: [.processor(p)]) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .disk) self.manager.cache.clearMemoryCache() self.manager.cache.clearDiskCache { self.manager.retrieveImage(with: url, options: [.processor(p)]) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .none) exp.fulfill() }}}}}} waitForExpectations(timeout: 3, handler: nil) } func testRetrieveImageOriginalCacheClaimsCachedButReturnsNoneShouldStillCallback() { let exp = expectation(description: #function) let uuid = UUID() let originalCache = ImageCache(name: "test.original.\(uuid.uuidString)") let targetCache = ImageCache(name: "test.target.\(uuid.uuidString)") addTeardownBlock { clearCaches([originalCache, targetCache]) } let url = testURLs[0] let key = url.cacheKey stub(url, data: testImageData) // Store invalid data as the "original" image. This makes `imageCachedType` report `.disk`, // while `retrieveImage` returns `.none` due to deserialization failure. do { try originalCache.diskStorage.store(value: Data([0x01, 0x02, 0x03]), forKey: key) } catch { XCTFail("Failed to prepare original cache: \(error)") return } XCTAssertTrue( originalCache.imageCachedType(forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier).cached ) let p = RoundCornerImageProcessor(cornerRadius: 20) manager.retrieveImage( with: url, options: [ .processor(p), .originalCache(originalCache), .targetCache(targetCache) ] ) { result in // It should never hang without calling the completion handler. XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value?.cacheType, CacheType.none) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testRetrieveImageForceRefresh() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) manager.cache.store( testImage, original: testImageData, forKey: url.cacheKey, processorIdentifier: DefaultImageProcessor.default.identifier, cacheSerializer: DefaultCacheSerializer.default, toDisk: true) { _ in XCTAssertTrue(self.manager.cache.imageCachedType(forKey: url.cacheKey).cached) self.manager.retrieveImage(with: url, options: [.forceRefresh]) { result in XCTAssertNotNil(result.value?.image) XCTAssertEqual(result.value!.cacheType, .none) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testRetrieveImageCancel() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData, length: 123) let task = manager.retrieveImage(with: url) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) exp.fulfill() } XCTAssertNotNil(task) task?.cancel() _ = stub.go() waitForExpectations(timeout: 3, handler: nil) } func testRetrieveImageCancelAsync() async throws { let url = testURLs[0] let stub = delayedStub(url, data: testImageData, length: 123) let checker = CallingChecker() try await checker.checkCancelBehavior(stub: stub) { _ = try await self.manager.retrieveImage(with: url) } } /// Test to reproduce the Swift Task Continuation Misuse issue /// This test verifies that continuations are properly resumed even under rapid cancellation scenarios /// /// NOTE: Single test run may not reproduce the issue, but running this test repeatedly /// (e.g., 100 times in Xcode) will almost certainly trigger the SWIFT TASK CONTINUATION MISUSE warning. /// This confirms the existence of a race condition in the async retrieveImage implementation. func testRetrieveImageContinuationMisuseReproduction() async throws { let url = testURLs[0] let stub = delayedStub(url, data: testImageData, length: 123) // Create multiple concurrent tasks that are cancelled quickly // This should reproduce the continuation leak scenario let taskCount = 50 // Increased to make race condition more likely var tasks: [Task] = [] for i in 0..( options: info, originalSource: .network(URL(string: "0")!)) let source1 = context.popAlternativeSource() XCTAssertNotNil(source1) guard case .network(let r1) = source1! else { XCTFail("Should be a network source, but \(source1!)") return } XCTAssertEqual(r1.downloadURL.absoluteString, "1") let source2 = context.popAlternativeSource() XCTAssertNotNil(source2) guard case .network(let r2) = source2! else { XCTFail("Should be a network source, but \(source2!)") return } XCTAssertEqual(r2.downloadURL.absoluteString, "2") XCTAssertNil(context.popAlternativeSource()) } func testRetrievingWithAlternativeSource() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) _ = manager.retrieveImage( with: .network(brokenURL), options: [.alternativeSources([.network(url)])]) { result in XCTAssertNotNil(result.value) XCTAssertEqual(result.value!.source.url, url) XCTAssertEqual(result.value!.originalSource.url, brokenURL) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testRetrievingErrorsWithAlternativeSource() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: Data()) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) let anotherBrokenURL = URL(string: "anotherBrokenURL")! stub(anotherBrokenURL, data: Data()) _ = manager.retrieveImage( with: .network(brokenURL), options: [.alternativeSources([.network(anotherBrokenURL), .network(url)])]) { result in defer { exp.fulfill() } XCTAssertNil(result.value) XCTAssertNotNil(result.error) guard case .imageSettingError(reason: let reason) = result.error! else { XCTFail("The error should be image setting error") return } guard case .alternativeSourcesExhausted(let errorInfo) = reason else { XCTFail("The error reason should be alternativeSourcesFailed") return } XCTAssertEqual(errorInfo.count, 3) XCTAssertEqual(errorInfo[0].source.url, brokenURL) XCTAssertEqual(errorInfo[1].source.url, anotherBrokenURL) XCTAssertEqual(errorInfo[2].source.url, url) } waitForExpectations(timeout: 3, handler: nil) } func testRetrievingAlternativeSourceTaskUpdateBlockCalled() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) let downloadTaskUpdatedCount = LockIsolated(0) let task = manager.retrieveImage( with: .network(brokenURL), options: [.alternativeSources([.network(url)])], downloadTaskUpdated: { newTask in downloadTaskUpdatedCount.withValue { $0 += 1 } XCTAssertEqual(newTask?.sessionTask?.task.currentRequest?.url, url) }) { result in XCTAssertEqual(downloadTaskUpdatedCount.value, 1) exp.fulfill() } XCTAssertEqual(task?.sessionTask?.task.currentRequest?.url, brokenURL) waitForExpectations(timeout: 3, handler: nil) } func testRetrievingAlternativeSourceCancelled() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) let task = manager.retrieveImage( with: .network(brokenURL), options: [.alternativeSources([.network(url)])] ) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) exp.fulfill() } task?.cancel() waitForExpectations(timeout: 3, handler: nil) } func testRetrievingAlternativeSourceCanCancelUpdatedTask() { let exp = expectation(description: #function) let url = testURLs[0] let dataStub = delayedStub(url, data: testImageData) let called = LockIsolated(false) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) let task = manager.retrieveImage( with: .network(brokenURL), options: [.alternativeSources([.network(url)])], downloadTaskUpdated: { newTask in XCTAssertNotNil(newTask) newTask?.cancel() called.setValue(true) } ) { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error?.isTaskCancelled ?? false) delay(0.3) { _ = dataStub.go() XCTAssertTrue(called.value) exp.fulfill() } } XCTAssertNotNil(task) XCTAssertTrue(task!.isInitialized) waitForExpectations(timeout: 3, handler: nil) } func testDownsamplingHandleScale2x() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) _ = manager.retrieveImage( with: .network(url), options: [.processor(DownsamplingImageProcessor(size: .init(width: 4, height: 4))), .scaleFactor(2)]) { result in let image = result.value?.image XCTAssertNotNil(image) #if os(macOS) XCTAssertEqual(image?.size, .init(width: 8, height: 8)) XCTAssertEqual(image?.kf.scale, 1) #else XCTAssertEqual(image?.size, .init(width: 4, height: 4)) XCTAssertEqual(image?.kf.scale, 2) #endif exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testDownsamplingHandleScale3x() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) _ = manager.retrieveImage( with: .network(url), options: [.processor(DownsamplingImageProcessor(size: .init(width: 4, height: 4))), .scaleFactor(3)]) { result in let image = result.value?.image XCTAssertNotNil(image) #if os(macOS) XCTAssertEqual(image?.size, .init(width: 12, height: 12)) XCTAssertEqual(image?.kf.scale, 1) #else XCTAssertEqual(image?.size, .init(width: 4, height: 4)) XCTAssertEqual(image?.kf.scale, 3) #endif exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testCacheCallbackCoordinatorStateChanging() { var coordinator = CacheCallbackCoordinator( shouldWaitForCache: false, shouldCacheOriginal: false) var called = false coordinator.apply(.cacheInitiated) { called = true } XCTAssertTrue(called) XCTAssertEqual(coordinator.state, .done) coordinator.apply(.cachingImage) { XCTFail() } XCTAssertEqual(coordinator.state, .done) coordinator = CacheCallbackCoordinator( shouldWaitForCache: true, shouldCacheOriginal: false) called = false coordinator.apply(.cacheInitiated) { XCTFail() } XCTAssertEqual(coordinator.state, .idle) coordinator.apply(.cachingImage) { called = true } XCTAssertTrue(called) XCTAssertEqual(coordinator.state, .done) coordinator = CacheCallbackCoordinator( shouldWaitForCache: false, shouldCacheOriginal: true) coordinator.apply(.cacheInitiated) { called = true } XCTAssertEqual(coordinator.state, .done) coordinator.apply(.cachingOriginalImage) { XCTFail() } XCTAssertEqual(coordinator.state, .done) coordinator = CacheCallbackCoordinator( shouldWaitForCache: true, shouldCacheOriginal: true) coordinator.apply(.cacheInitiated) { XCTFail() } XCTAssertEqual(coordinator.state, .idle) coordinator.apply(.cachingOriginalImage) { XCTFail() } XCTAssertEqual(coordinator.state, .originalImageCached) coordinator.apply(.cachingImage) { called = true } XCTAssertEqual(coordinator.state, .done) coordinator = CacheCallbackCoordinator( shouldWaitForCache: true, shouldCacheOriginal: true) coordinator.apply(.cacheInitiated) { XCTFail() } XCTAssertEqual(coordinator.state, .idle) coordinator.apply(.cachingImage) { XCTFail() } XCTAssertEqual(coordinator.state, .imageCached) coordinator.apply(.cachingOriginalImage) { called = true } XCTAssertEqual(coordinator.state, .done) } func testCallbackClearAfterSuccess() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) let task = LockIsolated(nil) let callbackCount = LockIsolated(0) let t: DownloadTask? = manager.retrieveImage(with: url) { result in let count = callbackCount.withValue { value in value += 1 return value } XCTAssertEqual(count, 1, "Callback should not be invoked again.") XCTAssertNotNil(result.value?.image) task.value?.cancel() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { exp.fulfill() } } task.setValue(t) waitForExpectations(timeout: 3, handler: nil) } func testCanUseCustomizeDefaultCacheSerializer() { let exp = expectation(description: #function) let url = testURLs[0] var cacheSerializer = DefaultCacheSerializer() cacheSerializer.preferCacheOriginalData = true manager.cache.store( testImage, original: testImageData, forKey: url.cacheKey, processorIdentifier: DefaultImageProcessor.default.identifier, cacheSerializer: cacheSerializer, toDisk: true) { result in let computedKey = url.cacheKey.computedKey(with: DefaultImageProcessor.default.identifier) let fileURL = self.manager.cache.diskStorage.cacheFileURL(forKey: computedKey) let data = try! Data(contentsOf: fileURL) XCTAssertEqual(data, testImageData) exp.fulfill() } waitForExpectations(timeout: 3.0) } func testCanUseCustomizeDefaultCacheSerializerStoreEncoded() { let exp = expectation(description: #function) let url = testURLs[0] var cacheSerializer = DefaultCacheSerializer() cacheSerializer.compressionQuality = 0.8 manager.cache.store( testImage, original: testImageJEPGData, forKey: url.cacheKey, processorIdentifier: DefaultImageProcessor.default.identifier, cacheSerializer: cacheSerializer, toDisk: true) { result in let computedKey = url.cacheKey.computedKey(with: DefaultImageProcessor.default.identifier) let fileURL = self.manager.cache.diskStorage.cacheFileURL(forKey: computedKey) let data = try! Data(contentsOf: fileURL) XCTAssertNotEqual(data, testImageJEPGData) XCTAssertEqual(data, testImage.kf.jpegRepresentation(compressionQuality: 0.8)) exp.fulfill() } waitForExpectations(timeout: 3.0) } func testImageResultContainsDataWhenDownloaded() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) manager.retrieveImage(with: url) { result in XCTAssertNotNil(result.value?.data()) XCTAssertEqual(result.value!.data(), testImageData) XCTAssertEqual(result.value!.cacheType, .none) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testImageResultContainsDataWhenLoadFromMemoryCache() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) manager.retrieveImage(with: url) { _ in self.manager.retrieveImage(with: url) { result in XCTAssertEqual(result.value!.cacheType, .memory) XCTAssertNotNil(result.value?.data()) XCTAssertEqual( result.value!.data(), DefaultCacheSerializer.default.data(with: result.value!.image, original: nil) ) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testImageResultContainsDataWhenLoadFromDiskCache() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData) manager.retrieveImage(with: url) { _ in self.manager.cache.clearMemoryCache() self.manager.retrieveImage(with: url) { result in XCTAssertEqual(result.value!.cacheType, .disk) XCTAssertNotNil(result.value?.data()) XCTAssertEqual( result.value!.data(), DefaultCacheSerializer.default.data(with: result.value!.image, original: nil) ) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } // https://github.com/onevcat/Kingfisher/issues/1923 func testAnimatedImageShouldRecreateFromCache() { let exp = expectation(description: #function) let url = testURLs[0] let data = testImageGIFData stub(url, data: data) let p = SimpleProcessor() manager.retrieveImage(with: url, options: [.processor(p), .onlyLoadFirstFrame]) { result in XCTAssertTrue(p.processed) XCTAssertTrue(result.value!.image.creatingOptions!.onlyFirstFrame) p.processed = false self.manager.retrieveImage(with: url, options: [.processor(p)]) { result in XCTAssertTrue(p.processed) XCTAssertFalse(result.value!.image.creatingOptions!.onlyFirstFrame) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testAnimatedImageShouldNotRecreateWithSameOptions() { let exp = expectation(description: #function) let url = testURLs[0] let data = testImageGIFData stub(url, data: data) let p = SimpleProcessor() manager.retrieveImage(with: url, options: [.processor(p), .onlyLoadFirstFrame]) { result in XCTAssertTrue(p.processed) XCTAssertTrue(result.value!.image.creatingOptions!.onlyFirstFrame) p.processed = false self.manager.retrieveImage(with: url, options: [.processor(p), .onlyLoadFirstFrame]) { result in XCTAssertFalse(p.processed) XCTAssertTrue(result.value!.image.creatingOptions!.onlyFirstFrame) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testMissingResourceOfLivePhotoFound() { let resource = KF.ImageResource(downloadURL: LivePhotoURL.mov) let source = LivePhotoSource(resources: [resource]) let missing = manager.missingResources(source, options: .init(.empty)) XCTAssertEqual(missing.count, 1) } func testMissingResourceOfLivePhotoNotFound() async throws { let resource = KF.ImageResource(downloadURL: LivePhotoURL.mov) try await manager.cache.storeToDisk( testImageData, forKey: resource.cacheKey, forcedExtension: resource.downloadURL.pathExtension ) let source = LivePhotoSource(resources: [resource]) let missing = manager.missingResources(source, options: .init(.empty)) XCTAssertEqual(missing.count, 0) } func testMissingResourceOfLivePhotoFoundOne() async throws { let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.heic) let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.mov) try await manager.cache.storeToDisk( testImageData, forKey: resource1.cacheKey, forcedExtension: resource1.downloadURL.pathExtension ) let source = LivePhotoSource(resources: [resource1, resource2]) let missing = manager.missingResources(source, options: .init(.empty)) XCTAssertEqual(missing.count, 1) XCTAssertEqual(missing[0].downloadURL, resource2.downloadURL) } func testMissingResourceOfLivePhotoForceRefresh() async throws { let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.heic) let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.mov) try await manager.cache.storeToDisk( testImageData, forKey: resource1.cacheKey, forcedExtension: resource1.downloadURL.pathExtension ) let source = LivePhotoSource(resources: [resource1, resource2]) let missing = manager.missingResources(source, options: .init([.forceRefresh])) XCTAssertEqual(missing.count, 2) XCTAssertEqual(missing[0].downloadURL, resource1.downloadURL) XCTAssertEqual(missing[1].downloadURL, resource2.downloadURL) } func testDownloadAndCacheLivePhotoResourcesAll() async throws { let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov) let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic) stub(resource1.downloadURL, data: testImageData) stub(resource2.downloadURL, data: testImageData) let result = try await manager.downloadAndCache( resources: [resource1, resource2].map { LivePhotoResource.init(resource: $0) }, options: .init(.empty)) XCTAssertEqual(result.count, 2) let urls = result.compactMap(\.url) XCTAssertTrue(urls.contains(LivePhotoURL.mov)) XCTAssertTrue(urls.contains(LivePhotoURL.heic)) let resourceCached1 = manager.cache.imageCachedType( forKey: resource1.cacheKey, forcedExtension: resource1.downloadURL.pathExtension ) let resourceCached2 = manager.cache.imageCachedType( forKey: resource2.cacheKey, forcedExtension: resource2.downloadURL.pathExtension ) XCTAssertEqual(resourceCached1, .disk) XCTAssertEqual(resourceCached2, .disk) } func testRetrieveLivePhotoFromNetwork() async throws { let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov) let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic) stub(resource1.downloadURL, data: testImageData) stub(resource2.downloadURL, data: testImageData) let resource1Cached = manager.cache.isCached( forKey: resource1.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier ) let resource2Cached = manager.cache.isCached( forKey: resource2.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier ) XCTAssertFalse(resource1Cached) XCTAssertFalse(resource2Cached) let source = LivePhotoSource(resources: [resource1, resource2]) let result = try await manager.retrieveLivePhoto(with: source) XCTAssertEqual(result.fileURLs.count, 2) result.fileURLs.forEach { url in XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) } XCTAssertEqual(result.cacheType, .none) XCTAssertEqual(result.data(), [testImageData, testImageData]) let urlsInSource = result.source.resources.map(\.downloadURL) XCTAssertTrue(urlsInSource.contains(LivePhotoURL.mov)) XCTAssertTrue(urlsInSource.contains(LivePhotoURL.heic)) } func testRetrieveLivePhotoFromLocal() async throws { let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov) let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic) try await manager.cache.storeToDisk( testImageData, forKey: resource1.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource1.downloadURL.pathExtension ) try await manager.cache.storeToDisk( testImageData, forKey: resource2.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource2.downloadURL.pathExtension ) let resource1Cached = manager.cache.isCached( forKey: resource1.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource1.downloadURL.pathExtension ) let resource2Cached = manager.cache.isCached( forKey: resource2.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource2.downloadURL.pathExtension ) XCTAssertTrue(resource1Cached) XCTAssertTrue(resource2Cached) let source = LivePhotoSource(resources: [resource1, resource2]) let result = try await manager.retrieveLivePhoto(with: source) XCTAssertEqual(result.fileURLs.count, 2) result.fileURLs.forEach { url in XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) } XCTAssertEqual(result.cacheType, .disk) XCTAssertEqual(result.data(), []) let urlsInSource = result.source.resources.map(\.downloadURL) XCTAssertTrue(urlsInSource.contains(LivePhotoURL.mov)) XCTAssertTrue(urlsInSource.contains(LivePhotoURL.heic)) } func testRetrieveLivePhotoMixed() async throws { let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov) let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic) try await manager.cache.storeToDisk( testImageData, forKey: resource1.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource1.downloadURL.pathExtension ) stub(resource2.downloadURL, data: testImageData) let resource1Cached = manager.cache.isCached( forKey: resource1.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource1.downloadURL.pathExtension ) let resource2Cached = manager.cache.isCached( forKey: resource2.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource2.downloadURL.pathExtension ) XCTAssertTrue(resource1Cached) XCTAssertFalse(resource2Cached) let source = LivePhotoSource(resources: [resource1, resource2]) let result = try await manager.retrieveLivePhoto(with: source) XCTAssertEqual(result.fileURLs.count, 2) result.fileURLs.forEach { url in XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) } XCTAssertEqual(result.cacheType, .none) XCTAssertEqual(result.data(), [testImageData]) let urlsInSource = result.source.resources.map(\.downloadURL) XCTAssertTrue(urlsInSource.contains(LivePhotoURL.mov)) XCTAssertTrue(urlsInSource.contains(LivePhotoURL.heic)) } func testRetrieveLivePhotoNetworkThenCache() async throws { let resource1 = KF.ImageResource(downloadURL: LivePhotoURL.mov) let resource2 = KF.ImageResource(downloadURL: LivePhotoURL.heic) stub(resource1.downloadURL, data: testImageData) stub(resource2.downloadURL, data: testImageData) let resource1Cached = manager.cache.isCached( forKey: resource1.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource1.downloadURL.pathExtension ) let resource2Cached = manager.cache.isCached( forKey: resource2.cacheKey, processorIdentifier: LivePhotoImageProcessor.default.identifier, forcedExtension: resource2.downloadURL.pathExtension ) XCTAssertFalse(resource1Cached) XCTAssertFalse(resource2Cached) let source = LivePhotoSource(resources: [resource1, resource2]) let result = try await manager.retrieveLivePhoto(with: source) XCTAssertEqual(result.fileURLs.count, 2) result.fileURLs.forEach { url in XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) } XCTAssertEqual(result.cacheType, .none) XCTAssertEqual(result.data(), [testImageData, testImageData]) let urlsInSource = result.source.resources.map(\.downloadURL) XCTAssertTrue(urlsInSource.contains(LivePhotoURL.mov)) XCTAssertTrue(urlsInSource.contains(LivePhotoURL.heic)) let localResult = try await manager.retrieveLivePhoto(with: source) XCTAssertEqual(localResult.fileURLs.count, 2) XCTAssertEqual(localResult.cacheType, .disk) } func testDownloadAndCacheLivePhotoWithEmptyResources() async throws { let result = try await manager.downloadAndCache(resources: [], options: .init([])) XCTAssertTrue(result.isEmpty) } func testDownloadAndCacheLivePhotoWithSingleResource() async throws { let resource = LivePhotoResource(downloadURL: LivePhotoURL.heic) stub(resource.downloadURL!, data: testImageData) let result = try await manager.downloadAndCache(resources: [resource], options: .init([])) XCTAssertEqual(result.count, 1) let t = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: "heic") XCTAssertEqual(t, .disk) } func testDownloadAndCacheLivePhotoWithSingleResourceGuessingUnsupportedExtension() async throws { let resource = LivePhotoResource(downloadURL: URL(string: "https://example.com")!) stub(resource.downloadURL!, data: testImageData) XCTAssertEqual(resource.referenceFileType, .other("")) let result = try await manager.downloadAndCache(resources: [resource], options: .init([])) XCTAssertEqual(result.count, 1) var cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: "heic") XCTAssertEqual(cacheType, .none) cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey) XCTAssertEqual(cacheType, .disk) } func testDownloadAndCacheLivePhotoWithSingleResourceExplicitSetExtension() async throws { let resource = LivePhotoResource(downloadURL: URL(string: "https://example.com")!, fileType: .heic) stub(resource.downloadURL!, data: testImageData) XCTAssertEqual(resource.referenceFileType, .heic) let result = try await manager.downloadAndCache(resources: [resource], options: .init([])) XCTAssertEqual(result.count, 1) var cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: "heic") XCTAssertEqual(cacheType, .disk) cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey) XCTAssertEqual(cacheType, .none) } func testDownloadAndCacheLivePhotoWithSingleResourceGuessingHEICExtension() async throws { let resource = LivePhotoResource(downloadURL: URL(string: "https://example.com")!) stub(resource.downloadURL!, data: partitalHEICData) XCTAssertEqual(resource.referenceFileType, .other("")) let result = try await manager.downloadAndCache(resources: [resource], options: .init([])) XCTAssertEqual(result.count, 1) var cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: "heic") XCTAssertEqual(cacheType, .disk) cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey) XCTAssertEqual(cacheType, .none) } func testDownloadAndCacheLivePhotoWithSingleResourceGuessingMOVExtension() async throws { let resource = LivePhotoResource(downloadURL: URL(string: "https://example.com")!) stub(resource.downloadURL!, data: partitalMOVData) XCTAssertEqual(resource.referenceFileType, .other("")) let result = try await manager.downloadAndCache(resources: [resource], options: .init([])) XCTAssertEqual(result.count, 1) var cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey, forcedExtension: "mov") XCTAssertEqual(cacheType, .disk) cacheType = manager.cache.imageCachedType(forKey: resource.cacheKey) XCTAssertEqual(cacheType, .none) } } private var imageCreatingOptionsKey: Void? extension KFCrossPlatformImage { var creatingOptions: ImageCreatingOptions? { get { return getAssociatedObject(self, &imageCreatingOptionsKey) } set { setRetainedAssociatedObject(self, &imageCreatingOptionsKey, newValue) } } } final class SimpleProcessor: ImageProcessor, @unchecked Sendable { public let identifier = "id" var processed = false /// Initialize a `DefaultImageProcessor` public init() {} /// Process an input `ImageProcessItem` item to an image for this processor. /// /// - parameter item: Input item which will be processed by `self` /// - parameter options: Options when processing the item. /// /// - returns: The processed image. /// /// - Note: See documentation of `ImageProcessor` protocol for more. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { processed = true switch item { case .image(let image): return image case .data(let data): let creatingOptions = options.imageCreatingOptions let image = KingfisherWrapper.image(data: data, options: creatingOptions) image?.creatingOptions = creatingOptions return image } } } final class FailingProcessor: ImageProcessor, @unchecked Sendable { public let identifier = "FailingProcessor" var processed = false public init() {} public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { processed = true return nil } } struct SimpleImageDataProvider: ImageDataProvider, @unchecked Sendable { let cacheKey: String let provider: () -> (Result) func data(handler: @escaping (Result) -> Void) { handler(provider()) } struct E: Error {} } actor ActorArray { var value: [Element] init(_ value: [Element]) { self.value = value } func append(_ newElement: Element) { value.append(newElement) } } ================================================ FILE: Tests/KingfisherTests/KingfisherOptionsInfoTests.swift ================================================ // // KingfisherOptionsInfoTests.swift // Kingfisher // // Created by Wei Wang on 16/1/4. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class KingfisherOptionsInfoTests: XCTestCase { func testEmptyOptionsShouldParseCorrectly() { let options = KingfisherParsedOptionsInfo(KingfisherOptionsInfo.empty) XCTAssertTrue(options.targetCache === nil) XCTAssertTrue(options.downloader === nil) #if os(iOS) || os(tvOS) || os(visionOS) switch options.transition { case .none: break default: XCTFail("The transition for empty option should be .None. But \(options.transition)") } #endif XCTAssertEqual(options.downloadPriority, URLSessionTask.defaultPriority) XCTAssertFalse(options.forceRefresh) XCTAssertFalse(options.fromMemoryCacheOrRefresh) XCTAssertFalse(options.cacheMemoryOnly) XCTAssertFalse(options.backgroundDecode) XCTAssertEqual(options.callbackQueue.queue.label, DispatchQueue.main.label) XCTAssertEqual(options.scaleFactor, 1.0) XCTAssertFalse(options.keepCurrentImageWhileLoading) XCTAssertFalse(options.onlyLoadFirstFrame) XCTAssertFalse(options.cacheOriginalImage) XCTAssertEqual(options.diskStoreWriteOptions, []) } func testSetOptionsShouldParseCorrectly() { let cache = ImageCache(name: "com.onevcat.Kingfisher.KingfisherOptionsInfoTests") let downloader = ImageDownloader(name: "com.onevcat.Kingfisher.KingfisherOptionsInfoTests") let queue = DispatchQueue.global(qos: .default) let testModifier = TestModifier() let testRedirectHandler = TestRedirectHandler() let processor = RoundCornerImageProcessor(cornerRadius: 20) let serializer = FormatIndicatedCacheSerializer.png let modifier = AnyImageModifier { i in return i } let alternativeSource = Source.network(URL(string: "https://onevcat.com")!) var options = KingfisherParsedOptionsInfo([ .targetCache(cache), .downloader(downloader), .originalCache(cache), .downloadPriority(0.8), .forceRefresh, .forceTransition, .fromMemoryCacheOrRefresh, .cacheMemoryOnly, .waitForCache, .onlyFromCache, .backgroundDecode, .callbackQueue(.dispatch(queue)), .scaleFactor(2.0), .preloadAllAnimationData, .requestModifier(testModifier), .redirectHandler(testRedirectHandler), .processor(processor), .cacheSerializer(serializer), .imageModifier(modifier), .keepCurrentImageWhileLoading, .onlyLoadFirstFrame, .cacheOriginalImage, .diskStoreWriteOptions([.atomic]), .alternativeSources([alternativeSource]), .retryStrategy(DelayRetryStrategy(maxRetryCount: 10)) ]) XCTAssertTrue(options.targetCache === cache) XCTAssertTrue(options.originalCache === cache) XCTAssertTrue(options.downloader === downloader) #if os(iOS) || os(tvOS) || os(visionOS) let transition = ImageTransition.fade(0.5) options.transition = transition switch options.transition { case .fade(let duration): XCTAssertEqual(duration, 0.5) default: XCTFail() } #endif XCTAssertEqual(options.downloadPriority, 0.8) XCTAssertTrue(options.forceRefresh) XCTAssertTrue(options.fromMemoryCacheOrRefresh) XCTAssertTrue(options.forceTransition) XCTAssertTrue(options.cacheMemoryOnly) XCTAssertTrue(options.waitForCache) XCTAssertTrue(options.onlyFromCache) XCTAssertTrue(options.backgroundDecode) XCTAssertEqual(options.callbackQueue.queue.label, queue.label) XCTAssertEqual(options.scaleFactor, 2.0) XCTAssertTrue(options.preloadAllAnimationData) XCTAssertTrue(options.requestModifier is TestModifier) XCTAssertTrue(options.redirectHandler is TestRedirectHandler) XCTAssertEqual(options.processor.identifier, processor.identifier) XCTAssertTrue(options.cacheSerializer is FormatIndicatedCacheSerializer) XCTAssertTrue(options.imageModifier is AnyImageModifier) XCTAssertTrue(options.keepCurrentImageWhileLoading) XCTAssertTrue(options.onlyLoadFirstFrame) XCTAssertTrue(options.cacheOriginalImage) XCTAssertEqual(options.diskStoreWriteOptions, [Data.WritingOptions.atomic]) XCTAssertEqual(options.alternativeSources?.count, 1) XCTAssertEqual(options.alternativeSources?.first?.url, alternativeSource.url) let retry = options.retryStrategy as? DelayRetryStrategy XCTAssertNotNil(retry) XCTAssertEqual(retry?.maxRetryCount, 10) } func testOptionCouldBeOverwritten() { var options = KingfisherParsedOptionsInfo([.downloadPriority(0.5), .onlyFromCache]) XCTAssertEqual(options.downloadPriority, 0.5) options = KingfisherParsedOptionsInfo([.downloadPriority(0.5), .onlyFromCache, .downloadPriority(0.8)]) XCTAssertEqual(options.downloadPriority, 0.8) } } final class TestModifier: ImageDownloadRequestModifier { func modified(for request: URLRequest) -> URLRequest? { return nil } } final class TestRedirectHandler: ImageDownloadRedirectHandler { func handleHTTPRedirection( for task: Kingfisher.SessionDataTask, response: HTTPURLResponse, newRequest: URLRequest ) async -> URLRequest? { newRequest } } ================================================ FILE: Tests/KingfisherTests/KingfisherTestHelper.swift ================================================ // // KingfisherTestHelper.swift // Kingfisher // // Created by Wei Wang on 15/4/10. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @testable import Kingfisher import CoreGraphics let testImageString = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAD8GlDQ1BJQ0MgUHJvZmlsZQAAOI2NVd1v21QUP4lvXKQWP6Cxjg4Vi69VU1u5GxqtxgZJk6XpQhq5zdgqpMl1bhpT1za2021V" + "n/YCbwz4A4CyBx6QeEIaDMT2su0BtElTQRXVJKQ9dNpAaJP2gqpwrq9Tu13GuJGvfznndz7v0TVAx1ea45hJGWDe8l01n5GPn5iWO1YhCc9BJ/RAp6Z7TrpcLgIuxoVH1sNfIcHeNwfa6/9z" + "dVappwMknkJsVz19HvFpgJSpO64PIN5G+fAp30Hc8TziHS4miFhheJbjLMMzHB8POFPqKGKWi6TXtSriJcT9MzH5bAzzHIK1I08t6hq6zHpRdu2aYdJYuk9Q/881bzZa8Xrx6fLmJo/iu4/V" + "XnfH1BB/rmu5ScQvI77m+BkmfxXxvcZcJY14L0DymZp7pML5yTcW61PvIN6JuGr4halQvmjNlCa4bXJ5zj6qhpxrujeKPYMXEd+q00KR5yNAlWZzrF+Ie+uNsdC/MO4tTOZafhbroyXuR3Df" + "08bLiHsQf+ja6gTPWVimZl7l/oUrjl8OcxDWLbNU5D6JRL2gxkDu16fGuC054OMhclsyXTOOFEL+kmMGs4i5kfNuQ62EnBuam8tzP+Q+tSqhz9SuqpZlvR1EfBiOJTSgYMMM7jpYsAEyqJCH" + "DL4dcFFTAwNMlFDUUpQYiadhDmXteeWAw3HEmA2s15k1RmnP4RHuhBybdBOF7MfnICmSQ2SYjIBM3iRvkcMki9IRcnDTthyLz2Ld2fTzPjTQK+Mdg8y5nkZfFO+se9LQr3/09xZr+5GcaSuf" + "eAfAww60mAPx+q8u/bAr8rFCLrx7s+vqEkw8qb+p26n11Aruq6m1iJH6PbWGv1VIY25mkNE8PkaQhxfLIF7DZXx80HD/A3l2jLclYs061xNpWCfoB6WHJTjbH0mV35Q/lRXlC+W8cndbl9t2" + "SfhU+Fb4UfhO+F74GWThknBZ+Em4InwjXIyd1ePnY/Psg3pb1TJNu15TMKWMtFt6ScpKL0ivSMXIn9QtDUlj0h7U7N48t3i8eC0GnMC91dX2sTivgloDTgUVeEGHLTizbf5Da9JLhkhh29QO" + "s1luMcScmBXTIIt7xRFxSBxnuJWfuAd1I7jntkyd/pgKaIwVr3MgmDo2q8x6IdB5QH162mcX7ajtnHGN2bov71OU1+U0fqqoXLD0wX5ZM005UHmySz3qLtDqILDvIL+iH6jB9y2x83ok898G" + "OPQX3lk3Itl0A+BrD6D7tUjWh3fis58BXDigN9yF8M5PJH4B8Gr79/F/XRm8m241mw/wvur4BGDj42bzn+Vmc+NL9L8GcMn8F1kAcXgSteGGAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlU" + "WHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9" + "Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJo" + "dHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8" + "L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAAKZklEQVR4Ae2ax28VyxLGywYMJuecgwgSIILIgg1pQRRJQrBkxZr9/RNYAhJiA0gEIbIE6JEzIggQIKLJOefod351+fzmzps5njke3wV2" + "SeM+Mx2qvq+qq3t6XNS1a9fyHz9+WE2V4poMHqcX11TPC3ctAWKippa1EVBTPS/cNT4C6oqJf7MsKiqKVVdeXh5bVx0V/woBcYCDYNVGpcAG2+hZlmW1EgAYrl+/ftnPnz+NTdenT5/s8+fP" + "sRgaN25sDRo0sLp161pxcbFfkFBdRFQLAQIO6G/fvtmHDx8cwMCBA61Pnz7WqVMna9GihQG2fv36Tsj79+/t5cuXdu/ePbt165ZdunTJGjVqZKWlpVZSUuJEQGTWkjkBeA1D8fKXL1+sd+/e" + "Nnr0aBs8eLADLqlfYqUNSq1evXru5Tp16nh0fP/+3cmiD6S9fv3azp07Z8eOHbNHjx45GZCFZBkNRR07dsws6wAe4wHfrVs3mzp1quH1Jk2aOHgig6iAIIU1pSJGIU9Ju48fPzoRZ86csT17" + "9tiLFy98LEjLKhoyIwCjAY7hs2fPtgkTJljLli09xAHJ/BdYvAjooFAnUTvyAO2IiocPHzoJu3fv9unDtMiChEwIwCPM39yrtc2ZM8dGjBhRARxCkDBggc1XihTGpz+55MCBA7ZlyxYnhRyi" + "8fONk6+uTi48/8rXoLI6jMM7Q4cOtUWLFnmJ5zBMniwEPHrpx4WnuVgdevToYW3atLGysjJ79eqVJ0kRVZmtUfVVIoCwx/NDhgxx8P369XMdGCvjo5SmfaaxGBdyO3fubK1bt7YbN24YqwfT" + "oVASCt4KA565iTHz5s3zbC9PAVC/CzVMJNFfY/GMyCLqhg0bZnPnznXg1ENSIVIQASjDMLIyCa9///5+L7AYyPpNyPKb56qTkXoWLFVHqed4nHEYD9IRSGAZZXmdNWuWL5PoKUQKmgIoe/Dg" + "gS1YsMCmTJnixikZUUeyun//vpcAYEODKC/wGzBctFeICzQlAKl7+/atPXnyxMnWpoh6xuKefMCe4erVq75EUpdGUhOAsWxa2rdvbwsXLrQOHTrY169fK7wDqPPnz9uyZcvs9u3bvi8ACBm7" + "YcOGDgwD6cPFNNK5JBsdLtozt69fv24HDx609evX27p162zcuHEOGPDooWzatKkTcfToUS/TEpB6J4hxZH3mfW4T5WRoSuBtDGcri5AgV61a5XmC+dqzZ08n5N27d/bmzRvf6AAeb0MQ22MA" + "IRcvXrQ1a9b4b8hG2BF2797dCWLeIwBmtzljxgzbsGGDL8Ui1BtU8icVAQDFY2T7AQMGeGiz+cEbGEI9Xn327Jk1a9bMM3aXLl382fLly313ePfu3bwmQQAXUwyCIZyxIQnSAAfRiKKRDRc7" + "zu3bt3vClEPyKvpdmYoAFON9Eg/Gse1FWVgwGA+pxHhWC8jhRUigwv0YC4CENsQFsz/TjvHCQh/ad89FxsSJE23Hjh2poiDVKqCwwzj29ygOEkA9iYn3AKaC6kQEwBGBpH/w4jl9NL/DgNu2" + "beuRgB6NTUk/6pgKaSUxARjPzmvs2LGeiGRsUCHGcImoYB2/ARQGlaZN+W/g6IgahxWBPKNpGR476j4VAbyd9erVy1cAPBclzF0yd7t27WKJiOoX9wygACZB/mf/fl8OiZCwYA/TC/ueP39e" + "sSqF24Xv/3+kcIvf92K8VatWnvyCYUidVgBeVlgFmPfqEzNk4seMw9Q6fvy4nTp1ynNJMI8o6pgGEI9EkRSlMBEBKJDH2ZVFCW1Y3o4cOVJhRFS7Qp5BAICaN29up0+f9uQbBEg9F05gOUXi" + "pmFYfyIC6AQBzDFePMICeHICyx8rA4JBWQo6EPYW7A6D46uOZ0QekikBKIAA2GWnFlTOb9Uz95Cgd/xBBn/Qg4dZDknGwSnI8NhAG+xjhQrXx5mQOAIYkHkntqMGDBITVV/VZ9Id5V3ppo2W" + "2yT6EhOAV4mCKOUoQnHU9EhiRNI2gESPwjyqH/YpX0XVh58lIgDFsMrmJmr3p3q2pPyOIymsPM09wMkzgEcPDkEXQkk9gn3YGaz3ipg/iQigLwTw/q8kp/FQjAHMTyVJCJBBapdFiWeDJ8wa" + "UzYAmhyB8DuJJGoFQM0rXobiwLFE8uGjOiKAMQHF+Ngi7wNSEUCE4H0kUwIYUKBZhjjwQEHQCJSzSvDOTpukBjB2ZSIPs/wNHz7cT4iIBtlEf/Q9ffrUL+6TOiFRBDCgwo9DCt7LFRHUYciv" + "n3+f2g4aNMhGjhzpn7iySopML3TOnDnTX3jC5GMD9tCGXSg7wmohgJ3YyZMnPReEw7Co+O+TIrbKHH5wZlCWO7qGBAyGpKDHMDpO1BYdJD3OEIisadOm+TacaIsai43YnTt3fNucOQEYq7Dm" + "hYdpECZBcxHwS5Ys8a9DkMBBBgYJWBxwnqsN7TlXBDznD4sXL/aXnTAwdGIHmzCO4JDg1PQHef6kPhNkp8Uc50SIUCPryhuUMkjv55wY40VI43CTJKr9epRdhDHvFETb5MmT/eB1/PjxfvYI" + "eJGsvtxj05UrV2zt2rX+vSANAalOhDCAtzKUcQrL6yfzk+dBEuQl6iGCiJg+fbq/p1+4cMH27dvnXtNcpi9Ecmi6dOlSf6dnRYEEPoAgIlp6eAbQejn9bI05QxRxTJGkkjgJakCSIaD4SPn4" + "8WP3bphxjOTSnoGTYyKGwwoyOUdjAq9xiRLqOHOgLaSRTwAu8GobLItz4U/i4ygMwrEvjaQmQCF38+ZNO3TokBuN8WESMAISeI5HAHbixAnbuXOnL2NhT0IIxu/du9cuX75ccVwuMsOgiDIS" + "LEsfEQVJYVLDfaLuUxPAICiH7c2bN9vZs2cdYD7lqiNJ8ZEEwjRNZBT35AZyBO0Arn5qoxJSGYNEvD93SsTFQUha7zNeQQRgAAZi8NatW/0jZT6DUaRIIPzjhDEgAG/GCeOQ9WnDP05s27at" + "IkHG9cn3vCACGBCP4U2WObLvtWvXXE94aYxSDtA4oS6qHuBcJF2mFP8+s2nTpsQ64/SlWgXCgxByZGmmATJ//nxPYMxNjMRgyT9A/e+xqv9RBvtRAdlMB8Bz4su54MaNG31DxkeUQkJfCguO" + "AA2Acr4TkBRXrlzp/8HBcoTBzFMEQICghIicL9U9tlQf2rPOI+w/du3aZatXr/YPovo2ETtIgooqRYDGhwSM4U1sxYoV/lFz0qRJ/vWItV0AICXfksZ48jZRBHg8ztdfljrAE218dmNc2lZV" + "MiFAhgOUHHD48GFPUKNGjbIxY8Z4ksKjgCdv8DtKeM4YJEKWN/YRZbkcw0kzGyjq+T6AjizAY0Mm/yQVBKO5DliAMB369u3r3wY5UAEYkRAlAKQ/SyxE4XXeB9gRQoz2G3EERo1Z2bPMCZBC" + "QGIonoIMkqIiRG3iStrifaYB3hZhWQKX7symgAZUqRDFeIBzJQVAtlcCZbyk/aQ7TVltBMiIQo0vtJ/0Ji2jJ2PS3n9Au1oC/gAnVglCbQRUib4/oHNtBPwBTqwShGI2HTVZ/gvZ53KpZJXY" + "DwAAAABJRU5ErkJggg==" var testImage = KFCrossPlatformImage(data: testImageData)! let testImageData = Data(base64Encoded: testImageString)! let partitalHEICData = Data(base64Encoded: "AAAALGZ0eXBoZWljAAAAAG1pZjFNaUhCTWlIRU1pUHI=")! let partitalMOVData = Data(base64Encoded: "AAAAFGZ0eXBxdCAgAAAAAHF0ICAAAAAId2lkZQAgJto=")! let testImagePNGData = testImage.kf.pngRepresentation()! let testImageJEPGData = testImage.kf.jpegRepresentation(compressionQuality: 1.0)! let testImageGIFData = Data(fileName: "dancing-banana.gif") let testImageSingleFrameGIFData = Data(fileName: "single-frame.gif") let testKeys = [ "http://stackoverflow.com/questions/11251340/convert-image-to-base64-string-in-ios-swift", "https://onevcat.com", "http://onevcat.com/content/images/2014/May/200.jpg", "http://onevcat.com/content/images/2014/May/200.jpg?fads#kj1asf" ] enum LivePhotoURL { static let mov = URL(string: "https://example.com/sample.mov")! static let heic = URL(string: "https://example.com/sample.heic")! } let testURLs = testKeys.map { URL(string: $0)! } func cleanDefaultCache() { let cache = KingfisherManager.shared.cache cache.clearMemoryCache() try? cache.diskStorage.removeAll() } func clearCaches(_ caches: [ImageCache]) { for c in caches { c.clearMemoryCache() try? c.diskStorage.removeAll(skipCreatingDirectory: true) } } func delay(_ time: Double, block: @escaping () -> Void) { DispatchQueue.main.asyncAfter(deadline: .now() + time) { block() } } extension KFCrossPlatformImage { func renderEqual(to image: KFCrossPlatformImage, withinTolerance tolerance: UInt8 = 3) -> Bool { guard size == image.size else { return false } guard let imageData1 = kf.pngRepresentation(), let imageData2 = image.kf.pngRepresentation() else { return false } guard let unifiedImage1 = KFCrossPlatformImage(data: imageData1), let unifiedImage2 = KFCrossPlatformImage(data: imageData2) else { return false } guard let rendered1 = unifiedImage1.rendered(), let rendered2 = unifiedImage2.rendered() else { return false } guard let data1 = rendered1.kf.cgImage?.dataProvider?.data, let data2 = rendered2.kf.cgImage?.dataProvider?.data else { return false } let length1 = CFDataGetLength(data1) let length2 = CFDataGetLength(data2) guard length1 == length2 else { return false } let dataPtr1: UnsafePointer = CFDataGetBytePtr(data1) let dataPtr2: UnsafePointer = CFDataGetBytePtr(data2) for index in 0.. KFCrossPlatformImage? { // Ignore non CG images guard let cgImage = kf.cgImage else { return nil } var bitmapInfo = cgImage.bitmapInfo let colorSpace = CGColorSpaceCreateDeviceRGB() let alpha = (bitmapInfo.rawValue & CGBitmapInfo.alphaInfoMask.rawValue) let w = cgImage.width let h = cgImage.height let size = CGSize(width: w, height: h) if alpha == CGImageAlphaInfo.none.rawValue { bitmapInfo.remove(.alphaInfoMask) bitmapInfo = CGBitmapInfo(rawValue: bitmapInfo.rawValue | CGImageAlphaInfo.noneSkipFirst.rawValue) } else if !(alpha == CGImageAlphaInfo.noneSkipFirst.rawValue) || !(alpha == CGImageAlphaInfo.noneSkipLast.rawValue) { bitmapInfo.remove(.alphaInfoMask) bitmapInfo = CGBitmapInfo(rawValue: bitmapInfo.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) } // Render the image guard let context = CGContext(data: nil, width: w, height: h, bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else { return nil } context.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: size)) #if os(macOS) return context.makeImage().flatMap { KFCrossPlatformImage(cgImage: $0, size: kf.size) } #else return context.makeImage().flatMap { KFCrossPlatformImage(cgImage: $0) } #endif } } #if os(iOS) || os(tvOS) || os(visionOS) import UIKit extension KFCrossPlatformImage { static func from(color: KFCrossPlatformColor, size: CGSize) -> KFCrossPlatformImage { let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) UIGraphicsBeginImageContext(rect.size) let context = UIGraphicsGetCurrentContext() context!.setFillColor(color.cgColor) context!.fill(rect) let img = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return img! } } #endif extension Data { init(fileName: String) { let comp = fileName.components(separatedBy: ".") guard comp.count == 2 else { fatalError() } self.init(named: comp[0], type: comp[1]) } init(named name: String, type: String) { guard let path = Bundle.test.path(forResource: name, ofType: type) else { fatalError() } try! self.init(contentsOf: URL(fileURLWithPath: path)) } } extension Bundle { static let test: Bundle = Bundle(for: ImageExtensionTests.self) } // Make tests happier with old Result type extension Result { var value: Success? { switch self { case .success(let success): return success case .failure: return nil } } var error: Failure? { switch self { case .success: return nil case .failure(let failure): return failure } } } ================================================ FILE: Tests/KingfisherTests/KingfisherTests-Bridging-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // #import "Nocilla.h" ================================================ FILE: Tests/KingfisherTests/LivePhotoSourceTests.swift ================================================ // // LivePhotoSourceTests.swift // Kingfisher // // Created by onevcat on 2024/10/01. // // Copyright (c) 2024 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class LivePhotoSourceTests: XCTestCase { func testLivePhotoResourceInitialization() { let url = URL(string: "https://example.com/photo.heic")! let resource = LivePhotoResource(downloadURL: url) XCTAssertEqual(resource.downloadURL, url) XCTAssertEqual(resource.referenceFileType, .heic) } func testLivePhotoResourceInitializationWithResource() { let url = URL(string: "https://example.com/photo.mov")! let imageResource = KF.ImageResource(downloadURL: url) let resource = LivePhotoResource(resource: imageResource) XCTAssertEqual(resource.downloadURL, url) XCTAssertEqual(resource.referenceFileType, .mov) } func testLivePhotoResourceFileExtensionByType() { let mov = LivePhotoResource.FileType.mov XCTAssertEqual(mov.determinedFileExtension(Data()), "mov") XCTAssertEqual(mov.fileExtension, "mov") let heic = LivePhotoResource.FileType.heic XCTAssertEqual(heic.determinedFileExtension(Data()), "heic") XCTAssertEqual(heic.fileExtension, "heic") let other = LivePhotoResource.FileType.other("exe") XCTAssertEqual(other.fileExtension, "exe") } func testLivePhotoResourceFileTypeDeterminationForHEIC() { let data = Data([0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63]) let fileType = LivePhotoResource.FileType.other("") let determinedExtension = fileType.determinedFileExtension(data) XCTAssertEqual(determinedExtension, "heic") } func testLivePhotoResourceFileTypeDeterminationForQT() { let data = Data([0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20]) let fileType = LivePhotoResource.FileType.other("") let determinedExtension = fileType.determinedFileExtension(data) XCTAssertEqual(determinedExtension, "mov") } func testLivePhotoResourceFileTypeDeterminationForExplicitFileType() { let data = Data([0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20]) let fileType = LivePhotoResource.FileType.other("ext") let determinedExtension = fileType.determinedFileExtension(data) XCTAssertEqual(determinedExtension, "ext") } func testLivePhotoResourceFileTypeDeterminationForUnknown() { let data = Data([0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x22]) let fileType = LivePhotoResource.FileType.other("") let determinedExtension = fileType.determinedFileExtension(data) XCTAssertEqual(determinedExtension, nil) } func testLivePhotoResourceFileTypeDeterminationForNonFYTP() { let data = Data([0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x71, 0x74, 0x20, 0x20]) let fileType = LivePhotoResource.FileType.other("") let determinedExtension = fileType.determinedFileExtension(data) XCTAssertEqual(determinedExtension, nil) } func testLivePhotoResourceFileTypeDeterminationForNotEnoughData() { let data = Data([0x00, 0x00, 0x00, 0x00]) let fileType = LivePhotoResource.FileType.other("") let determinedExtension = fileType.determinedFileExtension(data) XCTAssertEqual(determinedExtension, nil) } func testLivePhotoSourceInitializationWithResources() { let url1 = URL(string: "https://example.com/photo1.heic")! let url2 = URL(string: "https://example.com/photo2.mov")! let resources = [KF.ImageResource(downloadURL: url1), KF.ImageResource(downloadURL: url2)] let livePhotoSource = LivePhotoSource(resources: resources) XCTAssertEqual(livePhotoSource.resources.count, 2) XCTAssertEqual(livePhotoSource.resources[0].downloadURL, url1) XCTAssertEqual(livePhotoSource.resources[1].downloadURL, url2) } func testLivePhotoSourceInitializationWithURLs() { let url1 = URL(string: "https://example.com/photo1.heic")! let url2 = URL(string: "https://example.com/photo2.mov")! let livePhotoSource = LivePhotoSource(urls: [url1, url2]) XCTAssertEqual(livePhotoSource.resources.count, 2) XCTAssertEqual(livePhotoSource.resources[0].downloadURL, url1) XCTAssertEqual(livePhotoSource.resources[1].downloadURL, url2) } func testLivePhotoResourceInitializationWithCacheKey() { let url = URL(string: "https://example.com/photo.heic")! let cacheKey = "customCacheKey" let resource = LivePhotoResource(downloadURL: url, cacheKey: cacheKey) XCTAssertEqual(resource.downloadURL, url) XCTAssertEqual(resource.cacheKey, cacheKey) XCTAssertEqual(resource.referenceFileType, .heic) } func testLivePhotoResourceInitializationWithFileType() { let url = URL(string: "https://example.com/photo.unknown")! let resource = LivePhotoResource(downloadURL: url, fileType: .other("unknown")) XCTAssertEqual(resource.downloadURL, url) XCTAssertEqual(resource.referenceFileType, .other("unknown")) } func testLivePhotoResourceGuessedFileType() { let url1 = URL(string: "https://example.com/photo.heic")! let url2 = URL(string: "https://example.com/photo.mov")! let url3 = URL(string: "https://example.com/photo.unknown")! let resource1 = KF.ImageResource(downloadURL: url1) let resource2 = KF.ImageResource(downloadURL: url2) let resource3 = KF.ImageResource(downloadURL: url3) XCTAssertEqual(resource1.guessedFileType, .heic) XCTAssertEqual(resource2.guessedFileType, .mov) XCTAssertEqual(resource3.guessedFileType, .other("unknown")) } func testLivePhotoSourceInitializationWithMixedResources() { let url1 = URL(string: "https://example.com/photo1.heic")! let url2 = URL(string: "https://example.com/photo2.mov")! let url3 = URL(string: "https://example.com/photo3.unknown")! let resources = [ KF.ImageResource(downloadURL: url1), KF.ImageResource(downloadURL: url2), KF.ImageResource(downloadURL: url3) ] let livePhotoSource = LivePhotoSource(resources: resources) XCTAssertEqual(livePhotoSource.resources.count, 3) XCTAssertEqual(livePhotoSource.resources[0].downloadURL, url1) XCTAssertEqual(livePhotoSource.resources[1].downloadURL, url2) XCTAssertEqual(livePhotoSource.resources[2].downloadURL, url3) XCTAssertEqual(livePhotoSource.resources[0].referenceFileType, .heic) XCTAssertEqual(livePhotoSource.resources[1].referenceFileType, .mov) XCTAssertEqual(livePhotoSource.resources[2].referenceFileType, .other("unknown")) } } ================================================ FILE: Tests/KingfisherTests/MemoryStorageTests.swift ================================================ // // MemoryStorageTests.swift // Kingfisher // // Created by Wei Wang on 2018/11/12. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher extension Int { public var cacheCost: Int { return 1 } } #if compiler(>=6) extension Int: @retroactive CacheCostCalculable { } #else extension Int: CacheCostCalculable { } #endif class MemoryStorageTests: XCTestCase { var storage: MemoryStorage.Backend! override func setUp() { super.setUp() let config = MemoryStorage.Config(totalCostLimit: 3) storage = MemoryStorage.Backend(config: config) } override func tearDown() { storage = nil super.tearDown() } func testConfigSettingStorage() { XCTAssertEqual(storage.config.totalCostLimit, 3) XCTAssertEqual(storage.storage.totalCostLimit, 3) storage.config = MemoryStorage.Config(totalCostLimit: 10) XCTAssertEqual(storage.config.totalCostLimit, 10) XCTAssertEqual(storage.storage.totalCostLimit, 10) storage.config.countLimit = 100 XCTAssertEqual(storage.config.countLimit, 100) XCTAssertEqual(storage.storage.countLimit, 100) } func testStoreAndGetValue() { XCTAssertFalse(storage.isCached(forKey: "1")) storage.store(value: 1, forKey: "1") XCTAssertTrue(storage.isCached(forKey: "1")) XCTAssertEqual(storage.value(forKey: "1"), 1) } func testStoreValueOverwriting() { storage.store(value: 1, forKey: "1") XCTAssertEqual(storage.value(forKey: "1"), 1) storage.store(value: 100, forKey: "1") XCTAssertEqual(storage.value(forKey: "1"), 100) } func testRemoveValue() { XCTAssertFalse(storage.isCached(forKey: "1")) storage.store(value: 1, forKey: "1") XCTAssertTrue(storage.isCached(forKey: "1")) storage.remove(forKey: "1") XCTAssertFalse(storage.isCached(forKey: "1")) } func testRemoveAllValues() { storage.store(value: 1, forKey: "1") storage.store(value: 2, forKey: "2") XCTAssertTrue(storage.isCached(forKey: "1")) XCTAssertTrue(storage.isCached(forKey: "2")) storage.removeAll() XCTAssertFalse(storage.isCached(forKey: "1")) XCTAssertFalse(storage.isCached(forKey: "2")) } func testStoreWithExpiration() { let exp = expectation(description: #function) XCTAssertFalse(storage.isCached(forKey: "1")) storage.store(value: 1, forKey: "1", expiration: .seconds(0.1)) XCTAssertTrue(storage.isCached(forKey: "1")) XCTAssertFalse(storage.isCached(forKey: "2")) storage.store(value: 2, forKey: "2") XCTAssertTrue(storage.isCached(forKey: "2")) delay(0.2) { XCTAssertFalse(self.storage.isCached(forKey: "1")) XCTAssertTrue(self.storage.isCached(forKey: "2")) // But the object is still in underlying cache. let obj = self.storage.storage.object(forKey: "1") XCTAssertNotNil(obj) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testStoreWithConfigExpiration() { let exp = expectation(description: #function) storage.config.expiration = .seconds(0.1) XCTAssertFalse(storage.isCached(forKey: "1")) storage.store(value: 1, forKey: "1") XCTAssertTrue(storage.isCached(forKey: "1")) delay(0.2) { XCTAssertFalse(self.storage.isCached(forKey: "1")) // But the object is still in underlying cache. let obj = self.storage.storage.object(forKey: "1") XCTAssertNotNil(obj) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testStoreWithExpirationExtending() { let exp = expectation(description: #function) XCTAssertFalse(storage.isCached(forKey: "1")) storage.store(value: 1, forKey: "1", expiration: .seconds(1)) XCTAssertTrue(storage.isCached(forKey: "1")) delay(0.1) { let expirationDate1 = self.storage.storage.object(forKey: "1")?.estimatedExpiration XCTAssertNotNil(expirationDate1) // Request for the object to extend it's expiration date let obj = self.storage.value(forKey: "1", extendingExpiration: .expirationTime(.seconds(5))) XCTAssertNotNil(obj) let expirationDate2 = self.storage.storage.object(forKey: "1")?.estimatedExpiration XCTAssertNotNil(expirationDate2) XCTAssertNotEqual(expirationDate1!, expirationDate2!) XCTAssert(expirationDate1!.isPast(referenceDate: expirationDate2!)) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testStoreWithExpirationNotExtending() { let exp = expectation(description: #function) XCTAssertFalse(storage.isCached(forKey: "1")) storage.store(value: 1, forKey: "1", expiration: .seconds(1)) XCTAssertTrue(storage.isCached(forKey: "1")) delay(0.1) { let expirationDate1 = self.storage.storage.object(forKey: "1")?.estimatedExpiration XCTAssertNotNil(expirationDate1) // Request for the object to extend it's expiration date let obj = self.storage.value(forKey: "1", extendingExpiration: .none) XCTAssertNotNil(obj) let expirationDate2 = self.storage.storage.object(forKey: "1")?.estimatedExpiration XCTAssertNotNil(expirationDate2) XCTAssertEqual(expirationDate1, expirationDate2) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testRemoveExpired() { let exp = expectation(description: #function) XCTAssertFalse(storage.isCached(forKey: "1")) storage.store(value: 1, forKey: "1", expiration: .seconds(0.1)) XCTAssertTrue(storage.isCached(forKey: "1")) delay(0.2) { XCTAssertFalse(self.storage.isCached(forKey: "1")) // But the object is still in underlying cache. XCTAssertNotNil(self.storage.storage.object(forKey: "1")) self.storage.removeExpired() // It should be removed now. XCTAssertNil(self.storage.storage.object(forKey: "1")) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testExtendExpirationByAccessing() { let exp = expectation(description: #function) let expiration = StorageExpiration.seconds(0.5) storage.store(value: 1, forKey: "1", expiration: expiration) delay(0.3) { // This should extend the expiration to (0.3 + 0.5) from initially created. let v = self.storage.value(forKey: "1") XCTAssertEqual(v, 1) } delay(0.6) { // Accessing `isCached` does not extend expiration XCTAssertTrue(self.storage.isCached(forKey: "1")) } delay(1) { XCTAssertFalse(self.storage.isCached(forKey: "1")) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testAutoCleanExpiredMemory() { let exp = expectation(description: #function) let config = MemoryStorage.Config(totalCostLimit: 3, cleanInterval: 0.1) storage = MemoryStorage.Backend(config: config) storage.store(value: 1, forKey: "1", expiration: .seconds(0.1)) XCTAssertTrue(storage.isCached(forKey: "1")) XCTAssertEqual(self.storage.keys.count, 1) delay(0.2) { XCTAssertFalse(self.storage.isCached(forKey: "1")) XCTAssertNil(self.storage.storage.object(forKey: "1")) XCTAssertEqual(self.storage.keys.count, 0) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } func testStorageObject() { let now = Date() let obj = MemoryStorage.StorageObject(1, expiration: .seconds(1)) XCTAssertEqual(obj.value, 1) XCTAssertEqual( obj.estimatedExpiration.timeIntervalSince1970, now.addingTimeInterval(1).timeIntervalSince1970, accuracy: 0.3) let exp = expectation(description: #function) delay(0.5) { obj.extendExpiration() XCTAssertEqual( obj.estimatedExpiration.timeIntervalSince1970, now.addingTimeInterval(1.5).timeIntervalSince1970, accuracy: 0.3) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } } ================================================ FILE: Tests/KingfisherTests/NSButtonExtensionTests.swift ================================================ // // UIButtonExtensionTests.swift // Kingfisher // // Created by Wei Wang on 15/4/17. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit import XCTest @testable import Kingfisher class NSButtonExtensionTests: XCTestCase { var button: NSButton! override class func setUp() { super.setUp() LSNocilla.sharedInstance().start() } override class func tearDown() { LSNocilla.sharedInstance().stop() super.tearDown() } override func setUp() { super.setUp() button = NSButton() KingfisherManager.shared.downloader = ImageDownloader(name: "testDownloader") KingfisherManager.shared.defaultOptions = [.waitForCache] cleanDefaultCache() } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. LSNocilla.sharedInstance().clearStubs() button = nil cleanDefaultCache() KingfisherManager.shared.defaultOptions = .empty super.tearDown() } @MainActor func testDownloadAndSetImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) var progressBlockIsCalled = false button.kf.setImage(with: url, progressBlock: { _, _ in progressBlockIsCalled = true }) { result in XCTAssertTrue(progressBlockIsCalled) let image = result.value?.image XCTAssertNotNil(image) XCTAssertTrue(image!.renderEqual(to: testImage)) XCTAssertTrue(self.button.image!.renderEqual(to: testImage)) XCTAssertEqual(result.value!.cacheType, .none) exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testDownloadAndSetAlternateImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) var progressBlockIsCalled = false button.kf.setAlternateImage(with: url, progressBlock: { _, _ in progressBlockIsCalled = true }) { result in XCTAssertTrue(progressBlockIsCalled) let image = result.value?.image XCTAssertNotNil(image) XCTAssertTrue(image!.renderEqual(to: testImage)) XCTAssertTrue(self.button.alternateImage!.renderEqual(to: testImage)) XCTAssertEqual(result.value!.cacheType, .none) exp.fulfill() } waitForExpectations(timeout: 5, handler: nil) } @MainActor func testCancelImageTask() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) button.kf.setImage(with: url, completionHandler: { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) delay(0.1) { exp.fulfill() } }) self.button.kf.cancelImageDownloadTask() _ = stub.go() waitForExpectations(timeout: 3, handler: nil) } @MainActor func testCancelAlternateImageTask() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) button.kf.setAlternateImage(with: url, completionHandler: { result in XCTAssertNotNil(result.error) XCTAssertTrue(result.error!.isTaskCancelled) delay(0.1) { exp.fulfill() } }) self.button.kf.cancelAlternateImageDownloadTask() _ = stub.go() waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingNilURL() { let exp = expectation(description: #function) let url: URL? = nil button.kf.setAlternateImage(with: url, progressBlock: { _, _ in XCTFail() }) { result in XCTAssertNil(result.value) XCTAssertNotNil(result.error) guard case .imageSettingError(reason: .emptySource) = result.error! else { XCTFail() fatalError() } exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingNonWorkingImageWithFailureImage() { let expectation = self.expectation(description: "wait for downloading image") let url = testURLs[0] stub(url, errorCode: 404) button.kf.setImage(with: url, options: [.onFailureImage(testImage)], completionHandler: { result in XCTAssertNil(result.value) expectation.fulfill() }) XCTAssertNil(button.image) waitForExpectations(timeout: 5, handler: nil) XCTAssertEqual(testImage, button.image) } @MainActor func testSettingNonWorkingAlternateImageWithFailureImage() { let expectation = self.expectation(description: "wait for downloading image") let url = testURLs[0] stub(url, errorCode: 404) button.kf.setAlternateImage(with: url, options: [.onFailureImage(testImage)], completionHandler: { result in XCTAssertNil(result.value) expectation.fulfill() }) XCTAssertNil(button.alternateImage) waitForExpectations(timeout: 5, handler: nil) XCTAssertEqual(testImage, button.alternateImage) } } #endif ================================================ FILE: Tests/KingfisherTests/PixelFormatDecodingTests.swift ================================================ import Foundation import XCTest @testable import Kingfisher final class PixelFormatDecodingTests: XCTestCase { private struct Sample { let fileName: String let expectedBitsAfterDecoding: Int let expectedColorSpaceName: String? } private let samples: [Sample] = [ Sample(fileName: "gradient-8b-srgb-opaque.png", expectedBitsAfterDecoding: 8, expectedColorSpaceName: CGColorSpace.sRGB as String), Sample(fileName: "gradient-8b-srgb-alpha.png", expectedBitsAfterDecoding: 8, expectedColorSpaceName: CGColorSpace.sRGB as String), Sample(fileName: "gradient-8b-displayp3-alpha.png", expectedBitsAfterDecoding: 8, expectedColorSpaceName: CGColorSpace.displayP3 as String), Sample(fileName: "gradient-8b-gray.png", expectedBitsAfterDecoding: 8, expectedColorSpaceName: CGColorSpace.genericGrayGamma2_2 as String), Sample(fileName: "gradient-10b-srgb-opaque.heic", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.sRGB as String), Sample(fileName: "gradient-10b-srgb-alpha.heic", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.sRGB as String), Sample(fileName: "gradient-10b-displayp3-alpha.heic", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.displayP3 as String), Sample(fileName: "gradient-16b-srgb-alpha.png", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.sRGB as String), Sample(fileName: "gradient-16b-gray.png", expectedBitsAfterDecoding: 16, expectedColorSpaceName: CGColorSpace.genericGrayGamma2_2 as String) ] func testDecodingSupportsVariousPixelFormats() { for sample in samples { let data = Data(fileName: sample.fileName) let options = ImageCreatingOptions() guard let image = KingfisherWrapper.image(data: data, options: options) else { XCTFail("Failed to construct image for \(sample.fileName)") continue } let decoded = image.kf.decoded guard let cgImage = decoded.kf.cgImage else { XCTFail("Decoded image lost CGImage for \(sample.fileName)") continue } #if os(macOS) if sample.expectedBitsAfterDecoding > 8 { XCTAssertNotIdentical(decoded, image, "Decoding should redraw \(sample.fileName)") } XCTAssertEqual(cgImage.bitsPerComponent, sample.expectedBitsAfterDecoding, "Unexpected bitsPerComponent for \(sample.fileName)") if let expectedColorSpaceName = sample.expectedColorSpaceName { XCTAssertEqual(cgImage.colorSpace?.name as String?, expectedColorSpaceName, "Unexpected color space for \(sample.fileName)") } else { XCTFail("expectedColorSpaceName not existing, but needed for \(sample.fileName)") } #else // On iOS/tvOS/visionOS, `decoded` may go through `preparingForDisplay`, // which can keep 10-bit HEIC as 10 bpc or promote it to 16 bpc depending // on the runtime display/decode pipeline. if sample.fileName.contains("gradient-10b") { XCTAssertTrue( cgImage.bitsPerComponent == 10 || cgImage.bitsPerComponent == 16, "Unexpected bitsPerComponent for \(sample.fileName): \(cgImage.bitsPerComponent)" ) } else { XCTAssertEqual( cgImage.bitsPerComponent, sample.expectedBitsAfterDecoding, "Unexpected bitsPerComponent for \(sample.fileName)" ) } #endif } } } ================================================ FILE: Tests/KingfisherTests/RetryStrategyTests.swift ================================================ // // RetryStrategyTests.swift // Kingfisher // // Created by onevcat on 2020/05/06. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class RetryStrategyTests: XCTestCase { var manager: KingfisherManager! override class func setUp() { super.setUp() LSNocilla.sharedInstance().start() } override class func tearDown() { LSNocilla.sharedInstance().stop() super.tearDown() } override func setUpWithError() throws { try super.setUpWithError() let uuid = UUID() let downloader = ImageDownloader(name: "test.manager.\(uuid.uuidString)") let cache = ImageCache(name: "test.cache.\(uuid.uuidString)") manager = KingfisherManager(downloader: downloader, cache: cache) manager.defaultOptions = [.waitForCache] } override func tearDownWithError() throws { LSNocilla.sharedInstance().clearStubs() clearCaches([manager.cache]) cleanDefaultCache() manager = nil try super.tearDownWithError() } func testCanCreateRetryStrategy() { let strategy = DelayRetryStrategy(maxRetryCount: 10, retryInterval: .seconds(5)) XCTAssertEqual(strategy.maxRetryCount, 10) XCTAssertEqual(strategy.retryInterval.timeInterval(for: 0), 5) } func testDelayRetryIntervalCalculating() { let secondInternal = DelayRetryStrategy.Interval.seconds(10) XCTAssertEqual(secondInternal.timeInterval(for: 0), 10) let accumulatedInternal = DelayRetryStrategy.Interval.accumulated(3) XCTAssertEqual(accumulatedInternal.timeInterval(for: 0), 3) XCTAssertEqual(accumulatedInternal.timeInterval(for: 1), 6) XCTAssertEqual(accumulatedInternal.timeInterval(for: 2), 9) XCTAssertEqual(accumulatedInternal.timeInterval(for: 3), 12) let customInternal = DelayRetryStrategy.Interval.custom { TimeInterval($0 * 2) } XCTAssertEqual(customInternal.timeInterval(for: 0), 0) XCTAssertEqual(customInternal.timeInterval(for: 1), 2) XCTAssertEqual(customInternal.timeInterval(for: 2), 4) XCTAssertEqual(customInternal.timeInterval(for: 3), 6) } func testKingfisherManagerCanRetry() { let exp = expectation(description: #function) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) let retry = StubRetryStrategy() _ = manager.retrieveImage( with: .network(brokenURL), options: [.retryStrategy(retry)], completionHandler: { result in XCTAssertEqual(retry.count, 3) exp.fulfill() } ) waitForExpectations(timeout: 3, handler: nil) } func testImagePrefetcherCanRetry() { let exp = expectation(description: #function) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) let retry = StubRetryStrategy() let progressCount = LockIsolated(0) let prefetcher = ImagePrefetcher( urls: [brokenURL], options: [.retryStrategy(retry)], progressBlock: { _, _, _ in progressCount.withValue { $0 += 1 } }, completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(retry.count, 3) XCTAssertEqual(progressCount.value, 1, "Progress should be reported once per source, not per retry attempt.") XCTAssertEqual(skippedResources.count, 0) XCTAssertEqual(failedResources.count, 1) XCTAssertEqual(completedResources.count, 0) exp.fulfill() } ) prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } func testImagePrefetcherRetryStrategyStopDoesNotRetry() { let exp = expectation(description: #function) let brokenURL = URL(string: "brokenurl")! stub(brokenURL, data: Data()) let retry = ImmediateStopRetryStrategy() let prefetcher = ImagePrefetcher( urls: [brokenURL], options: [.retryStrategy(retry)], completionHandler: { skippedResources, failedResources, completedResources in XCTAssertEqual(retry.count, 1) XCTAssertEqual(skippedResources.count, 0) XCTAssertEqual(failedResources.count, 1) XCTAssertEqual(completedResources.count, 0) exp.fulfill() } ) prefetcher.start() waitForExpectations(timeout: 3, handler: nil) } // MARK: - DelayRetryStrategy Tests func testDelayRetryStrategyExceededCount() { let exp = expectation(description: #function) let blockCalled: ActorArray = ActorArray([]) let source = Source.network(URL(string: "url")!) let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0)) let group = DispatchGroup() group.enter() let context1 = RetryContext( source: source, error: .responseError(reason: .URLSessionError(error: E())) ) retry.retry(context: context1) { decision in guard case RetryDecision.retry(let userInfo) = decision else { XCTFail("The decision should be `retry`") return } XCTAssertNil(userInfo) Task { await blockCalled.append(true) group.leave() } } group.enter() let context2 = RetryContext( source: source, error: .responseError(reason: .URLSessionError(error: E())) ) context2.increaseRetryCount() // 1 context2.increaseRetryCount() // 2 context2.increaseRetryCount() // 3 retry.retry(context: context2) { decision in guard case RetryDecision.stop = decision else { XCTFail("The decision should be `stop`") return } Task { await blockCalled.append(true) group.leave() } } group.notify(queue: .main) { Task { let result = await blockCalled.value XCTAssertEqual(result.count, 2) XCTAssertTrue(result.allSatisfy { $0 }) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testDelayRetryStrategyNotRetryForErrorReason() { let exp = expectation(description: #function) // Only non-user cancel error && response error should be retied. let blockCalled: ActorArray = ActorArray([]) let source = Source.network(URL(string: "url")!) let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0)) let task = URLSession.shared.dataTask(with: URL(string: "url")!) let group = DispatchGroup() group.enter() let context1 = RetryContext( source: source, error: .requestError(reason: .taskCancelled(task: .init(task: task), token: .init())) ) retry.retry(context: context1) { decision in guard case RetryDecision.stop = decision else { XCTFail("The decision should be `stop` if user cancelled the task.") return } Task { await blockCalled.append(true) group.leave() } } group.enter() let context2 = RetryContext( source: source, error: .cacheError(reason: .imageNotExisting(key: "any_key")) ) retry.retry(context: context2) { decision in guard case RetryDecision.stop = decision else { XCTFail("The decision should be `stop` if the error type is not response error.") return } Task { await blockCalled.append(true) group.leave() } } group.notify(queue: .main) { Task { let result = await blockCalled.value XCTAssertEqual(result.count, 2) XCTAssertTrue(result.allSatisfy { $0 }) exp.fulfill() } } waitForExpectations(timeout: 3, handler: nil) } func testDelayRetryStrategyDidRetried() { let exp = expectation(description: #function) let source = Source.network(URL(string: "url")!) let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0)) let context = RetryContext( source: source, error: .responseError(reason: .URLSessionError(error: E())) ) retry.retry(context: context) { decision in guard case RetryDecision.retry = decision else { XCTFail("The decision should be `retry`.") return } exp.fulfill() } waitForExpectations(timeout: 3, handler: nil) } // MARK: - NetworkRetryStrategy Tests func testNetworkRetryStrategyRetriesImmediatelyWhenConnected() { let exp = expectation(description: #function) let source = Source.network(URL(string: "url")!) let networkMonitor = TestNetworkMonitor(isConnected: true) let retry = NetworkRetryStrategy( timeoutInterval: 30, networkMonitor: networkMonitor ) let context = RetryContext( source: source, error: .responseError(reason: .URLSessionError(error: E())) ) retry.retry(context: context) { decision in guard case RetryDecision.retry(let userInfo) = decision else { XCTFail("The decision should be `retry` when network is connected") return } XCTAssertNil(userInfo) exp.fulfill() } waitForExpectations(timeout: 1, handler: nil) } func testNetworkRetryStrategyStopsForTaskCancelled() { let exp = expectation(description: #function) let source = Source.network(URL(string: "url")!) let networkMonitor = TestNetworkMonitor(isConnected: true) let retry = NetworkRetryStrategy( timeoutInterval: 30, networkMonitor: networkMonitor ) let task = URLSession.shared.dataTask(with: URL(string: "url")!) let context = RetryContext( source: source, error: .requestError(reason: .taskCancelled(task: .init(task: task), token: .init())) ) retry.retry(context: context) { decision in guard case RetryDecision.stop = decision else { XCTFail("The decision should be `stop` if user cancelled the task") return } exp.fulfill() } waitForExpectations(timeout: 1, handler: nil) } func testNetworkRetryStrategyStopsForNonResponseError() { let exp = expectation(description: #function) let source = Source.network(URL(string: "url")!) let networkMonitor = TestNetworkMonitor(isConnected: true) let retry = NetworkRetryStrategy( timeoutInterval: 30, networkMonitor: networkMonitor ) let context = RetryContext( source: source, error: .cacheError(reason: .imageNotExisting(key: "any_key")) ) retry.retry(context: context) { decision in guard case RetryDecision.stop = decision else { XCTFail("The decision should be `stop` if the error type is not response error") return } exp.fulfill() } waitForExpectations(timeout: 1, handler: nil) } func testNetworkRetryStrategyWithTimeout() { let exp = expectation(description: #function) let source = Source.network(URL(string: "url")!) let networkMonitor = TestNetworkMonitor(isConnected: false) let retry = NetworkRetryStrategy(timeoutInterval: 0.1, networkMonitor: networkMonitor) let context = RetryContext( source: source, error: .responseError(reason: .URLSessionError(error: E())) ) // Test timeout behavior when network is disconnected retry.retry(context: context) { decision in guard case RetryDecision.stop = decision else { XCTFail("The decision should be `stop` after timeout") return } exp.fulfill() } waitForExpectations(timeout: 1, handler: nil) } func testNetworkRetryStrategyWaitsForReconnection() { let exp = expectation(description: #function) let source = Source.network(URL(string: "url")!) let networkMonitor = TestNetworkMonitor(isConnected: false) let retry = NetworkRetryStrategy( timeoutInterval: 30, networkMonitor: networkMonitor ) let context = RetryContext( source: source, error: .responseError(reason: .URLSessionError(error: E())) ) // Start retry when network is disconnected - should wait for reconnection retry.retry(context: context) { decision in guard case RetryDecision.retry(let userInfo) = decision else { XCTFail("The decision should be `retry` when network reconnects") return } XCTAssertNotNil(userInfo) // Should contain the observer exp.fulfill() } // Simulate network reconnection after a short delay DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { networkMonitor.simulateNetworkChange(isConnected: true) } waitForExpectations(timeout: 1, handler: nil) } func testNetworkRetryStrategyCancelsPreviousObserver() { let exp = expectation(description: #function) let source = Source.network(URL(string: "url")!) let networkMonitor = TestNetworkMonitor(isConnected: false) let retry = NetworkRetryStrategy( timeoutInterval: 30, networkMonitor: networkMonitor ) let context = RetryContext( source: source, error: .responseError(reason: .URLSessionError(error: E())) ) // First retry attempt - should create an observer retry.retry(context: context) { decision in // This should not be called since network is disconnected initially XCTFail("First callback should not be called immediately when network is disconnected") } // Second retry attempt - should cancel previous observer retry.retry(context: context) { decision in guard case RetryDecision.retry(let userInfo) = decision else { XCTFail("The second decision should be `retry`") return } XCTAssertNotNil(userInfo) exp.fulfill() } // Simulate network reconnection DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { networkMonitor.simulateNetworkChange(isConnected: true) } waitForExpectations(timeout: 1, handler: nil) } } private struct E: Error {} final class ImmediateStopRetryStrategy: RetryStrategy, @unchecked Sendable { let queue = DispatchQueue(label: "com.onevcat.KingfisherTests.ImmediateStopRetryStrategy") var _count = 0 var count: Int { get { queue.sync { _count } } set { queue.sync { _count = newValue } } } func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) { count += 1 retryHandler(.stop) } } final class StubRetryStrategy: RetryStrategy, @unchecked Sendable { let queue = DispatchQueue(label: "com.onevcat.KingfisherTests.StubRetryStrategy") var _count = 0 var count: Int { get { queue.sync { _count } } set { queue.sync { _count = newValue } } } func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) { if count == 0 { XCTAssertNil(context.userInfo) } else { XCTAssertEqual(context.userInfo as! Int, count) } XCTAssertEqual(context.retriedCount, count) count += 1 if count == 3 { retryHandler(.stop) } else { retryHandler(.retry(userInfo: count)) } } } // MARK: - Test Network Monitoring Implementations /// A test implementation of NetworkMonitoring that allows controlling network state for testing. final class TestNetworkMonitor: @unchecked Sendable, NetworkMonitoring { private let queue = DispatchQueue(label: "com.onevcat.KingfisherTests.TestNetworkMonitor", attributes: .concurrent) private var _isConnected: Bool private var observers: [TestNetworkObserver] = [] var isConnected: Bool { get { queue.sync { _isConnected } } set { queue.sync(flags: .barrier) { _isConnected = newValue } } } init(isConnected: Bool = true) { self._isConnected = isConnected } func observeConnectivity(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void) -> NetworkObserver { let observer = TestNetworkObserver( timeoutInterval: timeoutInterval, callback: callback, monitor: self ) queue.sync(flags: .barrier) { observers.append(observer) } return observer } /// Simulates network state change and notifies all observers. func simulateNetworkChange(isConnected: Bool) { queue.sync(flags: .barrier) { _isConnected = isConnected let activeObservers = observers observers.removeAll() DispatchQueue.main.async { activeObservers.forEach { $0.notify(isConnected: isConnected) } } } } /// Removes an observer from the list. func removeObserver(_ observer: TestNetworkObserver) { queue.sync(flags: .barrier) { observers.removeAll { $0 === observer } } } } /// Test implementation of NetworkObserver for testing purposes. final class TestNetworkObserver: @unchecked Sendable, NetworkObserver { let timeoutInterval: TimeInterval? let callback: @Sendable (Bool) -> Void private weak var monitor: TestNetworkMonitor? private var timeoutWorkItem: DispatchWorkItem? private let queue = DispatchQueue(label: "com.onevcat.KingfisherTests.TestNetworkObserver", qos: .utility) init(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void, monitor: TestNetworkMonitor) { self.timeoutInterval = timeoutInterval self.callback = callback self.monitor = monitor // Set up timeout if specified if let timeoutInterval = timeoutInterval { let workItem = DispatchWorkItem { [weak self] in self?.notify(isConnected: false) } timeoutWorkItem = workItem queue.asyncAfter(deadline: .now() + timeoutInterval, execute: workItem) } } func notify(isConnected: Bool) { queue.async { [weak self] in guard let self else { return } // Cancel timeout if we're notifying timeoutWorkItem?.cancel() timeoutWorkItem = nil // Remove from monitor monitor?.removeObserver(self) // Call the callback DispatchQueue.main.async { self.callback(isConnected) } } } func cancel() { queue.async { [weak self] in guard let self else { return } // Cancel timeout timeoutWorkItem?.cancel() timeoutWorkItem = nil // Remove from monitor monitor?.removeObserver(self) } } } ================================================ FILE: Tests/KingfisherTests/StorageExpirationTests.swift ================================================ // // StorageExpirationTests.swift // Kingfisher // // Created by onevcat on 2018/11/12. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Kingfisher class StorageExpirationTests: XCTestCase { func testExpirationNever() { let e = StorageExpiration.never XCTAssertEqual(e.estimatedExpirationSinceNow, .distantFuture) XCTAssertEqual(e.timeInterval, .infinity) XCTAssertFalse(e.isExpired) } func testExpirationSeconds() { let e = StorageExpiration.seconds(100) XCTAssertEqual( e.estimatedExpirationSinceNow.timeIntervalSince1970, Date().timeIntervalSince1970 + 100, accuracy: 0.1) XCTAssertEqual(e.timeInterval, 100) XCTAssertFalse(e.isExpired) } func testExpirationDays() { let e = StorageExpiration.days(1) let oneDayInSecond = TimeInterval(TimeConstants.secondsInOneDay) XCTAssertEqual( e.estimatedExpirationSinceNow.timeIntervalSince1970, Date().timeIntervalSince1970 + oneDayInSecond, accuracy: 0.1) XCTAssertEqual(e.timeInterval, oneDayInSecond, accuracy: 0.1) XCTAssertFalse(e.isExpired) } func testExpirationDate() { let oneDayInSecond = TimeInterval(TimeConstants.secondsInOneDay) let targetDate = Date().addingTimeInterval(oneDayInSecond) let e = StorageExpiration.date(targetDate) XCTAssertEqual( e.estimatedExpirationSinceNow.timeIntervalSince1970, Date().timeIntervalSince1970 + oneDayInSecond, accuracy: 0.1) XCTAssertEqual(e.timeInterval, oneDayInSecond, accuracy: 0.1) XCTAssertFalse(e.isExpired) } func testAlreadyExpired() { let e = StorageExpiration.expired XCTAssertTrue(e.isExpired) XCTAssertEqual(e.estimatedExpirationSinceNow, .distantPast) } } ================================================ FILE: Tests/KingfisherTests/StringExtensionTests.swift ================================================ // // StringExtensionTests.swift // Kingfisher // // Created by Wei Wang on 16/8/14. // Copyright © 2019 Wei Wang. All rights reserved. // import XCTest @testable import Kingfisher class StringExtensionTests: XCTestCase { func testStringSHA256() { let s = "hello" XCTAssertEqual(s.kf.sha256, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824") } } ================================================ FILE: Tests/KingfisherTests/UIButtonExtensionTests.swift ================================================ // // UIButtonExtensionTests.swift // Kingfisher // // Created by Wei Wang on 15/4/17. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(UIKit) import UIKit import XCTest @testable import Kingfisher class UIButtonExtensionTests: XCTestCase { var button: UIButton! override class func setUp() { super.setUp() LSNocilla.sharedInstance().start() } override class func tearDown() { LSNocilla.sharedInstance().stop() super.tearDown() } override func setUp() { super.setUp() button = UIButton() KingfisherManager.shared.downloader = ImageDownloader(name: "testDownloader") KingfisherManager.shared.defaultOptions = [.waitForCache] cleanDefaultCache() } override func tearDown() { LSNocilla.sharedInstance().clearStubs() button = nil cleanDefaultCache() KingfisherManager.shared.defaultOptions = .empty super.tearDown() } @MainActor func testDownloadAndSetImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) var progressBlockIsCalled = false KF.url(url) .onProgress { _, _ in progressBlockIsCalled = true } .onSuccess { result in XCTAssertTrue(progressBlockIsCalled) XCTAssertTrue(result.image.renderEqual(to: testImage)) XCTAssertTrue(self.button.image(for: .normal)!.renderEqual(to: testImage)) XCTAssertEqual(result.cacheType, .none) exp.fulfill() } .set(to: button, for: .normal) waitForExpectations(timeout: 3, handler: nil) } @MainActor func testDownloadAndSetBackgroundImage() { let exp = expectation(description: #function) let url = testURLs[0] stub(url, data: testImageData, length: 123) var progressBlockIsCalled = false KF.url(url) .onProgress { _, _ in progressBlockIsCalled = true } .onSuccess { result in XCTAssertTrue(progressBlockIsCalled) XCTAssertTrue(result.image.renderEqual(to: testImage)) XCTAssertTrue(self.button.backgroundImage(for: .normal)!.renderEqual(to: testImage)) XCTAssertEqual(result.cacheType, .none) exp.fulfill() } .setBackground(to: button, for: .normal) waitForExpectations(timeout: 3, handler: nil) } @MainActor func testCancelImageTask() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) KF.url(url) .onFailure { error in XCTAssertTrue(error.isTaskCancelled) delay(0.1) { exp.fulfill() } } .set(to: button, for: .highlighted) self.button.kf.cancelImageDownloadTask() _ = stub.go() waitForExpectations(timeout: 3, handler: nil) } @MainActor func testCancelBackgroundImageTask() { let exp = expectation(description: #function) let url = testURLs[0] let stub = delayedStub(url, data: testImageData) KF.url(url) .onFailure { error in XCTAssertTrue(error.isTaskCancelled) delay(0.1) { exp.fulfill() } } .setBackground(to: button, for: .highlighted) self.button.kf.cancelBackgroundImageDownloadTask() _ = stub.go() waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingNilURL() { let exp = expectation(description: #function) let url: URL? = nil button.kf.setBackgroundImage(with: url, for: .normal, completionHandler: { result in XCTAssertNil(result.value) XCTAssertNotNil(result.error) guard case .imageSettingError(reason: .emptySource) = result.error! else { XCTFail() return } exp.fulfill() }) waitForExpectations(timeout: 3, handler: nil) } @MainActor func testSettingNonWorkingImageWithFailureImage() { let expectation = self.expectation(description: "wait for downloading image") let url = testURLs[0] stub(url, errorCode: 404) let state = UIControl.State() KF.url(url) .onFailureImage(testImage) .onFailure { error in XCTAssertEqual(testImage, self.button.image(for: state)) expectation.fulfill() } .set(to: button, for: state) XCTAssertNil(button.image(for: state)) waitForExpectations(timeout: 5, handler: nil) } @MainActor func testSettingNonWorkingBackgroundImageWithFailureImage() { let expectation = self.expectation(description: "wait for downloading image") let url = testURLs[0] stub(url, errorCode: 404) let state = UIControl.State() KF.url(url) .onFailureImage(testImage) .onFailure { error in XCTAssertEqual(testImage, self.button.backgroundImage(for: state)) expectation.fulfill() } .setBackground(to: button, for: state) XCTAssertNil(button.backgroundImage(for: state)) waitForExpectations(timeout: 5, handler: nil) } } #endif ================================================ FILE: Tests/KingfisherTests/Utils/StubHelpers.swift ================================================ // // StubHelpers.swift // Kingfisher // // Created by Wei Wang on 2018/10/12. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @discardableResult func stub(_ url: URL, data: Data, statusCode: Int = 200, length: Int? = nil, headers: [String: String] = [:] ) -> LSStubResponseDSL { var stubResult = stubRequest("GET", url.absoluteString as NSString) .andReturn(statusCode)? .withHeaders(headers)? .withBody(data as NSData) if let length = length { stubResult = stubResult?.withHeader("Content-Length", "\(length)") } return stubResult! } func delayedStub(_ url: URL, data: Data, statusCode: Int = 200, length: Int? = nil, headers: [String: String] = [:] ) -> LSStubResponseDSL { let result = stub(url, data: data, statusCode: statusCode, length: length, headers: headers) return result.delay()! } func stub(_ url: URL, errorCode: Int) { let error = NSError(domain: "stubError", code: errorCode, userInfo: nil) stub(url, error: error) } func stub(_ url: URL, error: any Error) { return stubRequest("GET", url.absoluteString as NSString).andFailWithError(error) } ================================================ FILE: docs/architecture.md ================================================ # Kingfisher Architecture Documentation ## High-Level System Organization Kingfisher 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. The 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. Cross-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. ## Component Map ### Core Components | Component | Location | Purpose | |-----------|----------|---------| | **KingfisherManager** | `Sources/General/KingfisherManager.swift` | Central coordinator managing image retrieval, caching, and processing workflows | | **ImageDownloader** | `Sources/Networking/ImageDownloader.swift` | Handles all network operations for downloading images | | **ImageCache** | `Sources/Cache/ImageCache.swift` | Dual-layer caching system with memory and disk storage | | **ImageProcessor** | `Sources/Image/ImageProcessor.swift` | Protocol and implementations for image transformation pipeline | | **KF** | `Sources/General/KF.swift` | Builder pattern entry point for fluent API | | **Source** | `Sources/General/ImageSource/Source.swift` | Represents image data sources (network/provider) | | **Resource** | `Sources/General/ImageSource/Resource.swift` | Protocol for cacheable resources with key/URL | ### Storage Layer | Component | Location | Purpose | |-----------|----------|---------| | **MemoryStorage** | `Sources/Cache/MemoryStorage.swift` | In-memory cache implementation with LRU eviction | | **DiskStorage** | `Sources/Cache/DiskStorage.swift` | File-based cache with expiration and size limits | | **CacheSerializer** | `Sources/Cache/CacheSerializer.swift` | Handles image data serialization for cache storage | ### Networking Layer | Component | Location | Purpose | |-----------|----------|---------| | **SessionDelegate** | `Sources/Networking/SessionDelegate.swift` | URLSession delegate for download management | | **SessionDataTask** | `Sources/Networking/SessionDataTask.swift` | Wrapper for URLSessionDataTask with cancellation | | **ImagePrefetcher** | `Sources/Networking/ImagePrefetcher.swift` | Preloads images for improved performance | | **RequestModifier** | `Sources/Networking/RequestModifier.swift` | Protocol for modifying URL requests | | **RetryStrategy** | `Sources/Networking/RetryStrategy.swift` | Configurable retry logic for failed downloads | ### UI Integration | Component | Location | Purpose | |-----------|----------|---------| | **ImageView+Kingfisher** | `Sources/Extensions/ImageView+Kingfisher.swift` | UIImageView/NSImageView extensions | | **KFImage** | `Sources/SwiftUI/KFImage.swift` | SwiftUI image component | | **KFAnimatedImage** | `Sources/SwiftUI/KFAnimatedImage.swift` | SwiftUI animated image support | | **AnimatedImageView** | `Sources/Views/AnimatedImageView.swift` | GIF animation support view | ## Key Files ### KingfisherManager.swift (Lines 107-420) The heart of the library, containing: - `shared` singleton instance (line 113) - `retrieveImage()` main entry point (lines 196-210, 233-248) - Cache lookup logic (lines 400-403) - Download coordination (lines 415-418) - Retry and alternative source handling (lines 306-385) ### ImageDownloader.swift (Lines 35-150) Network layer implementation: - `ImageLoadingResult` struct for download results (lines 36-58) - `DownloadTask` class for cancellable downloads (lines 65-102) - URLSession management and request handling ### ImageCache.swift (Lines 52-200) Caching infrastructure: - `CacheType` enum defining cache levels (lines 52-72) - Memory and disk cache coordination - Cache key generation and expiration logic ### KF.swift (Lines 50-100) Builder pattern implementation: - Static factory methods for creating builders (lines 56-99) - Fluent API entry points for different source types ### ImageProcessor.swift (Lines 37-100) Processing pipeline: - `ImageProcessItem` enum for input types (lines 37-46) - `ImageProcessor` protocol definition (lines 49-76) - Processor chaining via `append()` (lines 85-95) ### KingfisherOptionsInfo.swift (Lines 43-250) Configuration system: - Option items enumeration with associated values - Cache, downloader, and processor configuration - Transition and placeholder settings ## Data Flow ### 1. Image Request Initiation ``` UIImageView.kf.setImage(with: url) │ └─> ImageView+Kingfisher.swift (line 77-87) │ └─> KingfisherManager.retrieveImage() Sources/General/KingfisherManager.swift (line 196) ``` ### 2. Cache Lookup ``` KingfisherManager.retrieveImage() │ └─> retrieveImageFromCache() (line 400) │ ├─> ImageCache.retrieveImage() │ Sources/Cache/ImageCache.swift │ │ │ ├─> MemoryStorage.value(forKey:) │ │ Sources/Cache/MemoryStorage.swift │ │ │ └─> DiskStorage.value(forKey:) │ Sources/Cache/DiskStorage.swift │ └─> [Cache Hit] → completionHandler(.success) [Cache Miss] → Continue to download ``` ### 3. Network Download ``` KingfisherManager.loadAndCacheImage() (line 415) │ └─> ImageDownloader.downloadImage() Sources/Networking/ImageDownloader.swift │ ├─> SessionDelegate.downloadTask() │ Sources/Networking/SessionDelegate.swift │ └─> URLSession.dataTask() │ └─> CompletionHandler with ImageLoadingResult ``` ### 4. Image Processing ``` Downloaded Data │ └─> ImageProcessor.process() (line 434) Sources/Image/ImageProcessor.swift │ ├─> DefaultImageProcessor (if none specified) │ └─> Custom processors chain │ └─> Processed KFCrossPlatformImage ``` ### 5. Cache Storage ``` Processed Image │ └─> KingfisherManager.cacheImage() (line 459) │ ├─> ImageCache.store() (line 482) │ │ │ ├─> MemoryStorage.store() │ │ In-memory cache with cost calculation │ │ │ └─> DiskStorage.store() │ File system with expiration │ └─> completionHandler(.success(RetrieveImageResult)) │ └─> UI Update on main queue ``` ### 6. Error Handling and Retry ``` Download/Processing Error │ └─> RetryStrategy.retry() (line 362) Sources/Networking/RetryStrategy.swift │ ├─> [Retry] → startNewRetrieveTask() (line 306) │ └─> [No Retry] → Check alternative sources (line 334) │ ├─> [Alternative exists] → Start new task │ └─> [No alternatives] → completionHandler(.failure) ``` This 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. ================================================ FILE: docs/build-system.md ================================================ # Kingfisher Build System Documentation ## Overview Kingfisher uses a dual build system approach supporting both Swift Package Manager and Fastlane/CocoaPods for maximum flexibility and distribution options. ### Primary Build Tools - **Swift Package Manager** (`Package.swift`) - Modern dependency management and building - **Fastlane** (`fastlane/Fastfile`) - Automated testing, building, and release workflows - **CocoaPods** (`Kingfisher.podspec`) - Legacy distribution and integration - **GitHub Actions** (`.github/workflows/`) - Continuous integration and testing ### Key Configuration Files ``` . ├── Package.swift # Swift Package Manager configuration ├── Kingfisher.podspec # CocoaPods specification ├── Gemfile # Ruby dependencies for Fastlane/CocoaPods ├── fastlane/ │ ├── Fastfile # Fastlane automation workflows │ └── actions/ # Custom Fastlane actions └── .github/workflows/ ├── build.yaml # CI build workflow └── test.yaml # CI test workflow ``` ## Build Workflows ### Building with Swift Package Manager ```bash # Build for default platform swift build # Build with specific Swift version swift build -Xswiftc -swift-version -Xswiftc 5 # Build for release swift build -c release # Build and run tests swift test # Generate Xcode project (if needed) swift package generate-xcodeproj ``` ### Building with Fastlane First, install dependencies: ```bash # Install Ruby dependencies bundle install ``` Common build commands: ```bash # Run all platform tests (iOS, macOS, tvOS, watchOS) bundle exec fastlane tests # Build for CI with specific destination DESTINATION="platform=iOS Simulator,name=iPhone 15,OS=17.5" bundle exec fastlane build_ci # Test for CI with specific destination DESTINATION="platform=macOS" bundle exec fastlane test_ci # Build specific platform bundle exec fastlane build destination:"platform=iOS Simulator,name=iPhone 15" # Lint both CocoaPods and SPM bundle exec fastlane lint ``` ### Release Process The release workflow automates versioning, tagging, and distribution: ```bash # Full release (tests, lint, version bump, GitHub release, CocoaPods push) bundle exec fastlane release version:X.X.X # Skip tests during release bundle exec fastlane release version:X.X.X skip_tests:true # Create XCFramework for distribution bundle exec fastlane xcframework version:X.X.X ``` Release steps performed: 1. Ensures clean git state and correct branch 2. Runs all tests (unless skipped) 3. Lints CocoaPods spec and SPM package 4. Updates version in all configuration files 5. Extracts and updates changelog 6. Creates signed git tag 7. Builds XCFramework for all platforms 8. Creates GitHub release with assets 9. Pushes to CocoaPods trunk ## Platform-specific Setup ### Supported Platforms From `Package.swift` and `Kingfisher.podspec`: - **iOS**: 13.0+ - **macOS**: 10.15+ - **tvOS**: 13.0+ - **watchOS**: 6.0+ - **visionOS**: 1.0+ ### CI Test Matrix From `.github/workflows/test.yaml`: - **Destinations**: macOS, iOS Simulator, tvOS Simulator, watchOS Simulator - **Xcode Versions**: 15.4, 16.2 ### Platform Build Commands ```bash # macOS bundle exec fastlane test destination:"platform=macOS" # iOS Simulator bundle exec fastlane test destination:"platform=iOS Simulator,name=iPhone 15,OS=17.5" # tvOS Simulator bundle exec fastlane test destination:"platform=tvOS Simulator,name=Apple TV,OS=17.5" # watchOS Simulator (build only, no test) bundle exec fastlane build destination:"platform=watchOS Simulator,name=Apple Watch Series 9 (41mm),OS=10.5" ``` ## Reference ### Build Targets From `Package.swift`: - **Library**: `Kingfisher` (single library product) - **Target**: `Kingfisher` (sources in `Sources/` directory) ### Fastlane Lanes Available lanes in `fastlane/Fastfile`: | Lane | Description | Parameters | |------|-------------|------------| | `tests` | Run tests on all platforms | None | | `test` | Run tests on specific platform | `destination` | | `build` | Build for specific platform | `destination` | | `test_ci` | CI test lane (builds watchOS) | Uses `ENV["DESTINATION"]` | | `build_ci` | CI build lane | Uses `ENV["DESTINATION"]` | | `lint` | Lint CocoaPods spec and SPM | None | | `release` | Full release workflow | `version`, `skip_tests` (optional) | | `xcframework` | Build XCFramework | `version`, `swift_version`, `xcode_version` | ### Environment Variables | Variable | Description | Used By | |----------|-------------|---------| | `DESTINATION` | Build/test destination | CI workflows | | `XCODE_VERSION` | Xcode version to use | CI workflows, Fastlane | | `GITHUB_TOKEN` | GitHub API token | Release workflow | ### Custom Fastlane Actions Located in `fastlane/actions/`: - `extract_current_change_log.rb` - Extract changelog for version - `git_commit_all.rb` - Commit all changes - `sync_build_number_to_git.rb` - Sync build number with git - `update_change_log.rb` - Update changelog file ### Troubleshooting #### Common Issues 1. **Bundle install fails** ```bash # Update bundler gem install bundler # Install with specific bundler version bundle _2.x.x_ install ``` 2. **Xcode version mismatch** ```bash # Set Xcode version explicitly XCODE_VERSION=16.2 bundle exec fastlane tests # Or use xcode-select sudo xcode-select -s /Applications/Xcode_16.2.app ``` 3. **Simulator not found** ```bash # List available simulators xcrun simctl list devices # Update destination string accordingly ``` 4. **CocoaPods push fails** ```bash # Verify pod spec locally first pod lib lint Kingfisher.podspec # Register session if needed pod trunk register email@example.com ``` 5. **Swift version issues** ```bash # Override Swift version in build bundle exec fastlane build xcargs:"SWIFT_VERSION=5.9" ``` ### Build Settings Key build settings used: - `BUILD_LIBRARY_FOR_DISTRIBUTION`: YES (for XCFramework) - `SKIP_INSTALL`: NO (for archiving) - `SWIFT_VERSION`: 5.0 (default, can be overridden) ### Distribution Artifacts Release builds generate: - `Kingfisher-{version}.xcframework.zip` - All platforms XCFramework - `Kingfisher-iOS-{version}.xcframework.zip` - iOS-only XCFramework Both are code-signed with Apple Distribution certificate and uploaded to GitHub releases. ================================================ FILE: docs/deployment.md ================================================ # Deployment Guide This document provides comprehensive guidance for deploying Kingfisher across different platforms and distribution channels. ## Overview Kingfisher supports multiple deployment strategies: - **CocoaPods**: Traditional CocoaPods spec deployment - **Swift Package Manager**: Native Swift package distribution - **XCFramework**: Pre-built universal frameworks - **GitHub Releases**: Automated release management The deployment process is fully automated using Fastlane with comprehensive platform coverage across iOS, macOS, tvOS, watchOS, and visionOS. ## Package Types ### CocoaPods Distribution **Configuration File**: `/Users/onevcat/Sync/github/Kingfisher/Kingfisher.podspec` **Build Targets**: - iOS 13.0+ - macOS 10.15+ - tvOS 13.0+ - watchOS 6.0+ - visionOS 1.0+ **Key Features**: - Module stability enabled (`BUILD_LIBRARY_FOR_DISTRIBUTION`) - Privacy manifest included (`PrivacyInfo.xcprivacy`) - Weak framework dependencies (SwiftUI, Combine) - Required frameworks (CFNetwork, Accelerate) ### Swift Package Manager **Configuration File**: `/Users/onevcat/Sync/github/Kingfisher/Package.swift` **Build Targets**: - Single library target: `Kingfisher` - Source path: `Sources/` - Minimum Swift tools version: 5.1 **Platform Support**: - iOS 13.0+ - macOS 10.15+ - tvOS 13.0+ - watchOS 6.0+ ### XCFramework Distribution **Output Locations**: ``` build/ ├── Kingfisher-{version}.xcframework.zip # All platforms ├── Kingfisher-iOS-{version}.xcframework.zip # iOS only ├── Kingfisher-{version}/ │ └── Kingfisher.xcframework/ │ ├── ios-arm64/ │ ├── ios-arm64_x86_64-simulator/ │ ├── macos-arm64_x86_64/ │ ├── tvos-arm64/ │ ├── tvos-arm64_x86_64-simulator/ │ ├── watchos-arm64_arm64_32_armv7k/ │ ├── watchos-arm64_i386_x86_64-simulator/ │ ├── xros-arm64/ │ └── xros-arm64_x86_64-simulator/ ``` **Platform-Specific Archives**: ``` build/ ├── Kingfisher-iphoneos.xcarchive/ ├── Kingfisher-iphonesimulator.xcarchive/ ├── Kingfisher-macosx.xcarchive/ ├── Kingfisher-appletvos.xcarchive/ ├── Kingfisher-appletvsimulator.xcarchive/ ├── Kingfisher-watchos.xcarchive/ ├── Kingfisher-watchsimulator.xcarchive/ ├── Kingfisher-xros.xcarchive/ └── Kingfisher-xrsimulator.xcarchive/ ``` ## Platform-Specific Deployment ### iOS Deployment - **Device**: `iphoneos` SDK - **Simulator**: `iphonesimulator` SDK - **Architectures**: arm64, x86_64 (simulator) - **Framework Path**: `build/Kingfisher-iOS-{version}.xcframework.zip` ### macOS Deployment - **SDK**: `macosx` - **Architectures**: arm64, x86_64 (universal) - **Framework Structure**: Traditional bundle format with versioning ### tvOS Deployment - **Device**: `appletvos` SDK - **Simulator**: `appletvsimulator` SDK - **Architectures**: arm64, x86_64 (simulator) ### watchOS Deployment - **Device**: `watchos` SDK - **Simulator**: `watchsimulator` SDK - **Architectures**: arm64, arm64_32, armv7k (device), i386, x86_64, arm64 (simulator) ### visionOS Deployment - **Device**: `xros` SDK - **Simulator**: `xrsimulator` SDK - **Architectures**: arm64, x86_64 (simulator) ## Deployment Commands ### Complete Release Process ```bash # Full release workflow bundle exec fastlane release version:X.X.X # Skip tests during release (not recommended) bundle exec fastlane release version:X.X.X skip_tests:true ``` ### Individual Components ```bash # Create XCFramework only bundle exec fastlane xcframework version:X.X.X # Run linting bundle exec fastlane lint # Build for specific platform bundle exec fastlane build_ci ``` ### CocoaPods Deployment ```bash # Lint podspec pod lib lint Kingfisher.podspec # Push to CocoaPods trunk (automated in release) pod trunk push Kingfisher.podspec ``` ### Swift Package Manager ```bash # Build with SPM swift build # Test with SPM swift test # Validate package swift package resolve ``` ## Continuous Integration ### GitHub Actions Workflows **Build Workflow**: `.github/workflows/build.yaml` - Builds across multiple Xcode versions (15.2, 15.3, 16.0, 16.1) - Tests all platforms in matrix configuration - Uses self-hosted runners **Test Workflow**: `.github/workflows/test.yaml` - Runs tests on Xcode 15.4 and 16.2 - Covers all platform destinations - Concurrent execution with cancellation **Matrix Configuration**: ```yaml destination: [ 'macOS', 'iOS Simulator,name=iPhone 15,OS=17.5', 'tvOS Simulator,name=Apple TV,OS=17.5', 'watchOS Simulator,name=Apple Watch Series 9 (41mm),OS=10.5' ] ``` ## Release Management ### Automated Release Process The release process handles: 1. **Pre-release Validation**: - Git branch verification - Clean git status check - Comprehensive testing across platforms - Podspec and SPM linting 2. **Version Management**: - Build number synchronization - Version number increment - Podspec version update - Changelog extraction 3. **Build Artifacts**: - XCFramework creation for all platforms - Code signing with Apple Distribution certificate - ZIP archive creation 4. **Distribution**: - Git tag creation with signing - GitHub release creation - Asset upload (both full and iOS-only XCFrameworks) - CocoaPods trunk push ### Changelog Management **Configuration**: `/Users/onevcat/Sync/github/Kingfisher/pre-change.yml` **Structure**: ```yaml version: X.X.X name: Release Name fix: - Bug fix descriptions with issue links add: - New feature descriptions ``` ### Version Tagging - **Format**: Semantic versioning (X.X.X) - **Signing**: GPG signed tags - **Automation**: Integrated with release workflow ## Reference ### Deployment Scripts | Script Location | Purpose | |----------------|---------| | `fastlane/Fastfile` | Main deployment automation | | `.github/workflows/build.yaml` | CI build workflow | | `.github/workflows/test.yaml` | CI test workflow | | `Gemfile` | Ruby dependencies | ### Build Output Locations | Package Type | Location | |-------------|----------| | CocoaPods | Published to CocoaPods trunk | | Swift Package Manager | Git repository tags | | XCFramework (All) | `build/Kingfisher-{version}.xcframework.zip` | | XCFramework (iOS) | `build/Kingfisher-iOS-{version}.xcframework.zip` | | GitHub Release Assets | Attached to release tags | ### Environment Variables | Variable | Purpose | Required | |----------|---------|----------| | `GITHUB_TOKEN` | GitHub API authentication | Yes (release) | | `DESTINATION` | CI build destination | Yes (CI) | | `XCODE_VERSION` | Xcode version selection | Yes (CI) | ### Code Signing - **Certificate**: Apple Distribution: Wei Wang (A4YJ9MRZ66) - **Timestamp**: Enabled for all XCFramework builds - **Verification**: Automated signature verification ### Server Configurations **GitHub Actions**: - **Runner Type**: Self-hosted - **Concurrency**: Group-based with cancellation - **Shell**: `bash -leo pipefail {0}` **Ruby Environment**: - **Fastlane**: Latest version - **CocoaPods**: Latest version - **xcodes**: For Xcode version management This deployment system ensures reliable, automated distribution across all supported Apple platforms with comprehensive testing and validation at each step. ================================================ FILE: docs/development.md ================================================ # Development Guide ## Overview Kingfisher 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. The 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. Testing 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. ## Code Style Conventions ### File Headers and Documentation All source files must include the standard license header: ```swift // // FileName.swift // Kingfisher // // Created by [Author] on [Date]. // // Copyright (c) 2019 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software")... ``` ### Naming Conventions - **Files**: Use PascalCase with descriptive names (`ImageProcessor.swift`, `KingfisherManager.swift`) - **Types**: PascalCase for classes, structs, protocols, and enums - **Methods/Properties**: camelCase starting with lowercase - **Constants**: Use `static let` for type constants, `let` for instance constants - **Protocols**: Use descriptive names, often ending with `-able` or describing capability Example from `/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift`: ```swift public protocol KingfisherCompatible: AnyObject { } public protocol KingfisherCompatibleValue {} ``` ### Cross-Platform Type Aliases Kingfisher uses consistent cross-platform type aliases defined in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift`: ```swift #if os(macOS) public typealias KFCrossPlatformImage = NSImage public typealias KFCrossPlatformView = NSView public typealias KFCrossPlatformColor = NSColor public typealias KFCrossPlatformImageView = NSImageView public typealias KFCrossPlatformButton = NSButton #else public typealias KFCrossPlatformImage = UIImage public typealias KFCrossPlatformColor = UIColor // ... additional platform-specific definitions #endif ``` ### Sendable Compliance Modern Swift concurrency is enforced throughout the codebase: ```swift public struct KingfisherWrapper: @unchecked Sendable { public let base: Base public init(_ base: Base) { self.base = base } } ``` ## Common Implementation Patterns ### 1. Namespace Wrapper Pattern The core pattern used throughout Kingfisher, implemented in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift`: ```swift /// Wrapper for Kingfisher compatible types public struct KingfisherWrapper: @unchecked Sendable { public let base: Base public init(_ base: Base) { self.base = base } } /// Protocol for types that can use .kf namespace public protocol KingfisherCompatible: AnyObject { } extension KingfisherCompatible { /// Gets a namespace holder for Kingfisher compatible types public var kf: KingfisherWrapper { get { return KingfisherWrapper(self) } set { } } } // Usage in extensions extension KFCrossPlatformImage: KingfisherCompatible { } ``` ### 2. Builder Pattern Fluent API implementation in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/KF.swift`: ```swift public enum KF { /// Creates a builder for a given URL public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder { source(url?.convertToSource(overrideCacheKey: cacheKey)) } } extension KF { public class Builder: @unchecked Sendable { private let source: Source? private var _options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions) // Fluent configuration methods public func placeholder(_ image: KFCrossPlatformImage?) -> Self { self.placeholder = image return self } } } ``` ### 3. Options Pattern Comprehensive options system in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/KingfisherOptionsInfo.swift`: ```swift /// Represents the available option items public enum KingfisherOptionsInfoItem: Sendable { case targetCache(ImageCache) case downloader(ImageDownloader) case transition(ImageTransition) case downloadPriority(Float) case forceRefresh case processor(any ImageProcessor) // ... many more options } /// Parsed options for internal use public struct KingfisherParsedOptionsInfo: Sendable { public var targetCache: ImageCache? = nil public var downloader: ImageDownloader? = nil public var transition: ImageTransition = .none // ... corresponding properties public init(_ info: KingfisherOptionsInfo?) { guard let info = info else { return } for option in info { switch option { case .targetCache(let value): targetCache = value case .downloader(let value): downloader = value // ... handle all options } } } } ``` ### 4. Protocol-Oriented Design Example from `/Users/onevcat/Sync/github/Kingfisher/Sources/Image/ImageProcessor.swift`: ```swift /// Protocol for image processing public protocol ImageProcessor: Sendable { /// Identifier for caching and retrieval var identifier: String { get } /// Process the input item func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? } extension ImageProcessor { /// Append processors in pipeline public func append(another: any ImageProcessor) -> any ImageProcessor { let newIdentifier = identifier.appending("|>\(another.identifier)") return GeneralProcessor(identifier: newIdentifier) { item, options in if let image = self.process(item: item, options: options) { return another.process(item: .image(image), options: options) } else { return nil } } } } ``` ### 5. Fluent Configuration with KFOptionSetter Protocol-based fluent API in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/KFOptionsSetter.swift`: ```swift @MainActor public protocol KFOptionSetter { var options: KingfisherParsedOptionsInfo { get nonmutating set } var onFailureDelegate: Delegate { get } var onSuccessDelegate: Delegate { get } var onProgressDelegate: Delegate<(Int64, Int64), Void> { get } } extension KFOptionSetter { public func targetCache(_ cache: ImageCache) -> Self { options.targetCache = cache return self } public func downloader(_ downloader: ImageDownloader) -> Self { options.downloader = downloader return self } } ``` ## Development Workflows ### Setting Up Images for UI Components **Common pattern for UIImageView/NSImageView** (file: `/Users/onevcat/Sync/github/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift`): ```swift // Basic usage imageView.kf.setImage(with: url) // With configuration imageView.kf.setImage( with: url, placeholder: placeholderImage, options: [.transition(.fade(0.2)), .cacheMemoryOnly], completionHandler: { result in // Handle result } ) ``` **Builder pattern approach**: ```swift KF.url(imageURL) .placeholder(placeholderImage) .fade(duration: 0.2) .cacheMemoryOnly() .onSuccess { result in print("Image loaded: \(result.image)") } .set(to: imageView) ``` ### Adding New Image Processors 1. **Create processor conforming to ImageProcessor protocol**: ```swift struct CustomProcessor: ImageProcessor { var identifier: String { "com.example.custom" } func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { // Implementation } } ``` 2. **Add convenience method to KFOptionSetter** (file: `/Users/onevcat/Sync/github/Kingfisher/Sources/General/KFOptionsSetter.swift`): ```swift extension KFOptionSetter { public func customEffect() -> Self { appendProcessor(CustomProcessor()) } } ``` ### Extending Platform Support **Add platform-specific extensions** (pattern from existing platform extensions): 1. **Update type aliases** in `/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift` 2. **Add compatibility conformance**: ```swift #if os(newOS) extension NewOSImageView: KingfisherCompatible { } #endif ``` 3. **Implement platform-specific extensions** following the pattern in existing platform files ### Cache Management Tasks **Working with ImageCache** (main class: `/Users/onevcat/Sync/github/Kingfisher/Sources/Cache/ImageCache.swift`): ```swift // Configure cache let cache = ImageCache(name: "custom") cache.memoryStorage.config.totalCostLimit = 50 * 1024 * 1024 // 50MB cache.diskStorage.config.sizeLimit = 200 * 1024 * 1024 // 200MB // Use with options imageView.kf.setImage(with: url, options: [.targetCache(cache)]) // Manual cache operations cache.store(image, forKey: key) cache.retrieveImage(forKey: key) { result in // Handle cached image } ``` ## Reference ### File Organization ``` Sources/ ├── General/ # Core managers, options, data providers │ ├── KingfisherManager.swift # Central coordinator │ ├── KF.swift # Builder pattern API │ ├── Kingfisher.swift # Core protocols and wrappers │ ├── KingfisherOptionsInfo.swift # Options system │ └── ImageSource/ # Data source abstractions ├── Networking/ # Download, prefetch, session management │ ├── ImageDownloader.swift # Network layer │ ├── ImagePrefetcher.swift # Batch prefetching │ └── RetryStrategy.swift # Retry logic ├── Cache/ # Multi-layer caching system │ ├── ImageCache.swift # Main cache interface │ ├── MemoryStorage.swift # Memory cache backend │ └── DiskStorage.swift # Disk cache backend ├── Image/ # Processing, filters, formats, transitions │ ├── ImageProcessor.swift # Processing protocols │ ├── Filter.swift # Built-in processors │ └── ImageTransition.swift # UI transition effects ├── Extensions/ # UIKit/AppKit integration │ ├── ImageView+Kingfisher.swift # Main UI extensions │ └── UIButton+Kingfisher.swift # Button extensions ├── SwiftUI/ # SwiftUI-specific components │ ├── KFImage.swift # SwiftUI image component │ └── KFAnimatedImage.swift # Animated SwiftUI component ├── Utility/ # Helper utilities and extensions └── Views/ # Custom UI components ``` ### Naming Conventions - **Manager classes**: `*Manager` (e.g., `KingfisherManager`) - **Data providers**: `*Provider` or `*DataProvider` (e.g., `ImageDataProvider`) - **Processors**: `*Processor` or `*ImageProcessor` (e.g., `BlurImageProcessor`) - **Extensions**: `Type+Kingfisher.swift` (e.g., `UIButton+Kingfisher.swift`) - **Protocols**: Descriptive names often with `-able` suffix (`KingfisherCompatible`) - **Internal utilities**: Plain descriptive names (`CallbackQueue`, `Result`) ### Common Issues and Solutions **Thread Safety**: All public APIs are designed to be thread-safe. Use `@MainActor` for UI-related operations and `@unchecked Sendable` for wrapper types. **Memory Management**: Kingfisher uses both memory and disk caching. Configure limits appropriately: ```swift ImageCache.default.memoryStorage.config.totalCostLimit = 50 * 1024 * 1024 ImageCache.default.diskStorage.config.sizeLimit = 200 * 1024 * 1024 ``` **Platform Differences**: Use platform-specific compilation directives and the provided cross-platform type aliases to ensure compatibility. **Testing**: Use the testing utilities in `/Users/onevcat/Sync/github/Kingfisher/Tests/KingfisherTests/KingfisherTestHelper.swift` and follow the mocking patterns established in existing tests. **Performance**: For large images, prefer `DownsamplingImageProcessor` over `ResizingImageProcessor` for better memory efficiency. ================================================ FILE: docs/files.md ================================================ # File Catalog - Kingfisher ## Overview Kingfisher 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. The 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. ## Core Source Files ### Primary Framework Components - **`Sources/General/KingfisherManager.swift`** - Central coordinator managing image loading workflow - **`Sources/General/KF.swift`** - Main entry point providing builder pattern API for image tasks - **`Sources/General/Kingfisher.swift`** - Core framework module with KingfisherCompatible protocol - **`Sources/General/KingfisherOptionsInfo.swift`** - Configuration options container for all operations - **`Sources/General/KingfisherError.swift`** - Error handling system with detailed error types - **`Sources/General/KFOptionsSetter.swift`** - Options configuration builder for method chaining ### Image Source and Data Providers - **`Sources/General/ImageSource/Resource.swift`** - URL resource definitions and transformations - **`Sources/General/ImageSource/Source.swift`** - Abstract image source protocols and implementations - **`Sources/General/ImageSource/ImageDataProvider.swift`** - Data provider protocol for custom image sources - **`Sources/General/ImageSource/AVAssetImageDataProvider.swift`** - AVAsset image frame extraction - **`Sources/General/ImageSource/PHPickerResultImageDataProvider.swift`** - Photo picker integration - **`Sources/General/ImageSource/LivePhotoSource.swift`** - Live Photo support implementation ### Network Layer - **`Sources/Networking/ImageDownloader.swift`** - HTTP image downloading with session management - **`Sources/Networking/ImageDownloader+LivePhoto.swift`** - Live Photo downloading extensions - **`Sources/Networking/ImageDownloaderDelegate.swift`** - Download progress and completion delegation - **`Sources/Networking/SessionDelegate.swift`** - URLSession delegate handling authentication and redirects - **`Sources/Networking/SessionDataTask.swift`** - Custom data task wrapper with cancellation support - **`Sources/Networking/ImagePrefetcher.swift`** - Batch image prefetching for performance optimization - **`Sources/Networking/RequestModifier.swift`** - HTTP request modification protocols - **`Sources/Networking/ImageModifier.swift`** - Response image modification protocols - **`Sources/Networking/RedirectHandler.swift`** - HTTP redirect handling strategies - **`Sources/Networking/RetryStrategy.swift`** - Failed request retry logic and policies - **`Sources/Networking/AuthenticationChallengeResponsable.swift`** - Authentication challenge handling - **`Sources/Networking/ImageDataProcessor.swift`** - Raw image data processing pipeline ### Caching System - **`Sources/Cache/ImageCache.swift`** - Dual-layer (memory + disk) caching coordinator - **`Sources/Cache/MemoryStorage.swift`** - In-memory LRU cache with automatic cleanup - **`Sources/Cache/DiskStorage.swift`** - Persistent disk storage with expiration policies - **`Sources/Cache/Storage.swift`** - Abstract storage protocols and configurations - **`Sources/Cache/CacheSerializer.swift`** - Image serialization for disk persistence - **`Sources/Cache/FormatIndicatedCacheSerializer.swift`** - Format-aware image serialization ### Image Processing and Transformation - **`Sources/Image/Image.swift`** - Cross-platform image type definitions and utilities - **`Sources/Image/ImageProcessor.swift`** - Image transformation and processing pipeline - **`Sources/Image/Filter.swift`** - Built-in image filters (blur, tint, overlay, etc.) - **`Sources/Image/ImageFormat.swift`** - Image format detection and handling - **`Sources/Image/ImageDrawing.swift`** - Core graphics drawing utilities and extensions - **`Sources/Image/ImageTransition.swift`** - View transition animations for image loading - **`Sources/Image/ImageProgressive.swift`** - Progressive JPEG loading implementation - **`Sources/Image/GIFAnimatedImage.swift`** - GIF animation support and playback - **`Sources/Image/GraphicsContext.swift`** - Graphics context management and utilities - **`Sources/Image/Placeholder.swift`** - Placeholder image definitions and protocols ## Platform Implementation Files ### UIKit Extensions - **`Sources/Extensions/ImageView+Kingfisher.swift`** - UIImageView integration with .kf namespace - **`Sources/Extensions/UIButton+Kingfisher.swift`** - UIButton background/image loading support - **`Sources/Extensions/NSTextAttachment+Kingfisher.swift`** - Text attachment image loading ### AppKit Extensions - **`Sources/Extensions/NSButton+Kingfisher.swift`** - macOS NSButton image loading integration - **`Sources/Extensions/HasImageComponent+Kingfisher.swift`** - Generic image component protocol ### Cross-Platform Extensions - **`Sources/Extensions/PHLivePhotoView+Kingfisher.swift`** - Live Photo view integration - **`Sources/Extensions/CPListItem+Kingfisher.swift`** - CarPlay list item support ### SwiftUI Components - **`Sources/SwiftUI/KFImage.swift`** - Main SwiftUI image view component - **`Sources/SwiftUI/KFAnimatedImage.swift`** - Animated image support for SwiftUI - **`Sources/SwiftUI/KFImageOptions.swift`** - SwiftUI-specific configuration options - **`Sources/SwiftUI/KFImageProtocol.swift`** - Shared protocol for KF SwiftUI components - **`Sources/SwiftUI/KFImageRenderer.swift`** - SwiftUI view rendering and update logic - **`Sources/SwiftUI/ImageBinder.swift`** - Binding layer between SwiftUI and Kingfisher core - **`Sources/SwiftUI/ImageContext.swift`** - SwiftUI image loading context management ### Custom Views - **`Sources/Views/AnimatedImageView.swift`** - Custom animated image view for GIF playback - **`Sources/Views/Indicator.swift`** - Loading indicator views and protocols ## Build System Files ### Package Management - **`Package.swift`** - Swift Package Manager manifest with dependencies and targets - **`Package@swift-5.9.swift`** - Swift 5.9 compatibility package manifest - **`Kingfisher.podspec`** - CocoaPods specification for distribution - **`Gemfile`** - Ruby dependencies for Fastlane and build tools - **`Gemfile.lock`** - Locked Ruby gem versions for reproducible builds ### Fastlane Build Automation - **`fastlane/Fastfile`** - Main Fastlane configuration with lanes for testing, building, and releasing - **`fastlane/actions/extract_current_change_log.rb`** - Extracts current version changelog - **`fastlane/actions/git_commit_all.rb`** - Git commit automation with custom messages - **`fastlane/actions/sync_build_number_to_git.rb`** - Synchronizes build numbers with git commits - **`fastlane/actions/update_change_log.rb`** - Automated changelog management - **`fastlane/README.md`** - Fastlane documentation and usage instructions ### Xcode Project Files - **`Kingfisher.xcodeproj/`** - Main Xcode project with targets and build settings - **`Kingfisher.xcworkspace/`** - Xcode workspace for integrated development - **`Demo/Kingfisher-Demo.xcodeproj/`** - Demo application Xcode project ## Configuration Files ### Framework Configuration - **`Sources/Info.plist`** - Framework bundle information and version metadata - **`Sources/PrivacyInfo.xcprivacy`** - Privacy manifest for App Store compliance ### Development Assets - **`images/`** - Sample images for testing and demo applications - `kingfisher-1.jpg` through `kingfisher-10.jpg` - Test image assets - `logo.png` - Project logo and branding - **`Tests/KingfisherTests/dancing-banana.gif`** - GIF test asset for animation testing - **`Tests/KingfisherTests/single-frame.gif`** - Single-frame GIF for edge case testing ### Demo Applications - **`Demo/Demo/Kingfisher-Demo/`** - iOS demo app showcasing all features - `ViewControllers/` - Various demo screens (GIF, transitions, processors, etc.) - `SwiftUIViews/` - SwiftUI demonstration views and regression tests - **`Demo/Demo/Kingfisher-macOS-Demo/`** - macOS demo application - **`Demo/Demo/Kingfisher-tvOS-Demo/`** - Apple TV demo application - **`Demo/Demo/Kingfisher-watchOS-Demo/`** - watchOS demo application ### Git and CI Configuration - **`pre-change.yml`** - Pre-commit hook configuration for code quality - **`.gitignore`** - Git ignore patterns for build artifacts and dependencies ## Utility and Helper Files ### Core Utilities - **`Sources/Utility/ExtensionHelpers.swift`** - Cross-platform compatibility helpers - **`Sources/Utility/CallbackQueue.swift`** - Thread-safe callback queue management - **`Sources/Utility/Box.swift`** - Reference wrapper for value types - **`Sources/Utility/Result.swift`** - Result type utilities and extensions - **`Sources/Utility/Delegate.swift`** - Weak delegate wrapper to prevent retain cycles - **`Sources/Utility/Runtime.swift`** - Runtime reflection and dynamic dispatch utilities - **`Sources/Utility/DisplayLink.swift`** - Cross-platform display link abstraction - **`Sources/Utility/SizeExtensions.swift`** - CGSize manipulation and calculations - **`Sources/Utility/String+SHA256.swift`** - String hashing for cache key generation ### Test Infrastructure - **`Tests/KingfisherTests/KingfisherTestHelper.swift`** - Shared testing utilities and mocks - **`Tests/KingfisherTests/Utils/StubHelpers.swift`** - HTTP stubbing helpers for network tests - **`Tests/Dependency/Nocilla/`** - HTTP mocking framework for isolated testing ### Documentation System - **`Sources/Documentation.docc/`** - DocC documentation bundle - `Documentation.md` - Main documentation entry point - `GettingStarted.md` - Quick start guide for new users - `Tutorials/` - Step-by-step tutorials for UIKit and SwiftUI - `CommonTasks/` - Task-oriented documentation for common use cases - `Topics/` - Advanced topic guides (prefetching, indicators, etc.) - `Resources/` - Documentation assets, images, and code samples ## Reference ### File Organization Patterns - **Modular Structure**: Core functionality separated into logical modules (General, Networking, Cache, Image, etc.) - **Platform Abstraction**: Cross-platform code in core modules, platform-specific extensions separate - **Protocol-Oriented**: Heavy use of protocols for customization and testing - **Namespace Pattern**: All public APIs accessed through `.kf` property extension ### Naming Conventions - **Files**: PascalCase with descriptive names indicating functionality - **Protocols**: Suffix with `-able` for capabilities (e.g., `AuthenticationChallengeResponsable`) - **Extensions**: Platform prefix for clarity (e.g., `UIButton+Kingfisher.swift`) - **Tests**: Mirror source structure with `Tests` suffix ### Dependency Relationships - **Core Dependencies**: Foundation, UIKit/AppKit conditionally imported - **SwiftUI Module**: Depends on core modules but isolated for optional usage - **Extensions**: Depend on core but platform-specific - **Test Dependencies**: Isolated with Nocilla for HTTP mocking - **Build Dependencies**: Fastlane for automation, Ruby gems for tooling ### Key Integration Points - **KingfisherCompatible Protocol**: Entry point for all functionality via `.kf` namespace - **Options System**: Centralized configuration through `KingfisherOptionsInfo` - **Result Types**: Consistent error handling with `Result` - **Callback Queues**: Thread-safe completion handling across all async operations ================================================ FILE: docs/project-overview.md ================================================ # Kingfisher Project Overview ## Project Purpose Kingfisher 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. The 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. ## Main Entry Points ### Core Configuration - **Package.swift** - Swift Package Manager manifest defining library targets and platform requirements - **Kingfisher.podspec** - CocoaPods specification (version 8.3.2) - **Sources/General/Kingfisher.swift** - Core type definitions and protocol declarations - **Sources/General/KingfisherManager.swift** - Central coordinator managing download and cache operations ### Primary APIs - **Sources/General/KF.swift** - Builder pattern entry point for fluent API - **Sources/Extensions/ImageView+Kingfisher.swift** - UIImageView/NSImageView extensions - **Sources/SwiftUI/KFImage.swift** - SwiftUI image component - **Sources/SwiftUI/KFAnimatedImage.swift** - SwiftUI animated image support ## Technology Stack ### Core Components - **Image Downloading**: `Sources/Networking/ImageDownloader.swift` - URLSession-based networking layer - **Cache System**: - `Sources/Cache/ImageCache.swift` - Dual-layer cache coordinator - `Sources/Cache/MemoryStorage.swift` - In-memory cache implementation - `Sources/Cache/DiskStorage.swift` - Persistent disk storage - **Image Processing**: `Sources/Image/ImageProcessor.swift` - Transformation pipeline with filters - **Format Support**: `Sources/Image/ImageFormat.swift` - Multi-format detection (JPEG, PNG, GIF, WebP) ### Platform Integrations - **UIKit Extensions**: `Sources/Extensions/UIButton+Kingfisher.swift`, `Sources/Extensions/NSTextAttachment+Kingfisher.swift` - **SwiftUI Components**: `Sources/SwiftUI/KFImageProtocol.swift`, `Sources/SwiftUI/ImageBinder.swift` - **Specialized Views**: `Sources/Views/AnimatedImageView.swift`, `Sources/Extensions/PHLivePhotoView+Kingfisher.swift` ### Build & Testing - **Fastlane**: `fastlane/Fastfile` - Primary build automation - **Test Suite**: `Tests/KingfisherTests/` - XCTest-based unit tests with Nocilla HTTP stubbing - **Documentation**: `Sources/Documentation.docc/` - DocC integrated documentation ## Platform Support ### Minimum Requirements - **Swift**: 5.9+ (Swift 6 strict concurrency ready) - **UIKit/AppKit**: - iOS 13.0+ (`#if os(iOS)`) - macOS 10.15+ (`#if os(macOS)`) - tvOS 13.0+ (`#if os(tvOS)`) - watchOS 6.0+ (`#if os(watchOS)`) - visionOS 1.0+ (`#if os(visionOS)`) - **SwiftUI**: iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+ ### Platform-Specific Files - **macOS**: `Sources/Extensions/NSButton+Kingfisher.swift` - NSButton image loading - **iOS/tvOS**: `Sources/Extensions/UIButton+Kingfisher.swift` - UIButton extensions - **watchOS**: `Sources/Extensions/WKInterfaceImage+Kingfisher.swift` - WatchKit support - **CarPlay**: `Sources/Extensions/CPListItem+Kingfisher.swift` - CarPlay list items (iOS 14.0+) - **tvOS**: `Sources/Extensions/TVMonogramView+Kingfisher.swift` - Apple TV monogram views ================================================ FILE: docs/testing.md ================================================ # Kingfisher Testing Documentation ## Overview Kingfisher'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. ### Test Infrastructure - **Test Framework**: XCTest - **Network Mocking**: Nocilla (located at `Tests/Dependency/Nocilla/`) - **Test Helper**: `Tests/KingfisherTests/KingfisherTestHelper.swift` - **Stub Utilities**: `Tests/KingfisherTests/Utils/StubHelpers.swift` - **Test Assets**: - `Tests/KingfisherTests/dancing-banana.gif` - Animated GIF for testing - `Tests/KingfisherTests/single-frame.gif` - Single frame GIF for testing ## Test Categories ### 1. Core Component Tests **Cache Layer Tests** - `Tests/KingfisherTests/ImageCacheTests.swift` - Tests for the main ImageCache functionality - `Tests/KingfisherTests/MemoryStorageTests.swift` - Memory cache specific tests - `Tests/KingfisherTests/DiskStorageTests.swift` - Disk storage specific tests - `Tests/KingfisherTests/StorageExpirationTests.swift` - Cache expiration policy tests **Networking Tests** - `Tests/KingfisherTests/ImageDownloaderTests.swift` - Image downloading and session management - `Tests/KingfisherTests/ImagePrefetcherTests.swift` - Batch image prefetching functionality - `Tests/KingfisherTests/DataReceivingSideEffectTests.swift` - Data processing side effects **Manager Tests** - `Tests/KingfisherTests/KingfisherManagerTests.swift` - Central coordinator tests - `Tests/KingfisherTests/KingfisherOptionsInfoTests.swift` - Configuration options tests ### 2. Image Processing Tests - `Tests/KingfisherTests/ImageProcessorTests.swift` - Image transformation pipeline tests - `Tests/KingfisherTests/ImageDrawingTests.swift` - Image drawing and rendering tests - `Tests/KingfisherTests/ImageExtensionTests.swift` - Core image extensions tests - `Tests/KingfisherTests/ImageModifierTests.swift` - Request modifier tests ### 3. UI Integration Tests **UIKit Tests** - `Tests/KingfisherTests/ImageViewExtensionTests.swift` - UIImageView extension tests - `Tests/KingfisherTests/UIButtonExtensionTests.swift` - UIButton extension tests **AppKit Tests** - `Tests/KingfisherTests/NSButtonExtensionTests.swift` - NSButton extension tests (macOS) ### 4. Specialized Feature Tests - `Tests/KingfisherTests/LivePhotoSourceTests.swift` - Live Photo support tests - `Tests/KingfisherTests/ImageDataProviderTests.swift` - Custom data provider tests - `Tests/KingfisherTests/RetryStrategyTests.swift` - Network retry strategy tests - `Tests/KingfisherTests/StringExtensionTests.swift` - String utility extension tests ## Running Tests ### Using Fastlane ```bash # Install dependencies first bundle install # Run all tests across all platforms bundle exec fastlane tests # Expected output: # [08:30:00]: ------------------------------ # [08:30:00]: --- Step: default_platform --- # [08:30:00]: ------------------------------ # [08:30:00]: Driving the lane 'ios tests' 🚀 # [08:30:01]: ------------------ # [08:30:01]: --- Step: scan --- # [08:30:01]: ------------------ # [08:30:01]: Running Tests: ▸ Touching Kingfisher.framework # [08:30:45]: Test Succeeded # Run tests for specific platform bundle exec fastlane test destination:"platform=iOS Simulator,name=iPhone 15" bundle exec fastlane test destination:"platform=macOS" bundle exec fastlane test destination:"platform=tvOS Simulator,name=Apple TV" # CI-specific test command (used in continuous integration) DESTINATION="platform=iOS Simulator,name=iPhone 15,OS=17.5" bundle exec fastlane test_ci # Build only (for watchOS where full testing isn't supported) bundle exec fastlane build destination:"platform=watchOS Simulator,name=Apple Watch Series 9 (41mm)" ``` ### Using Xcode ```bash # Open workspace in Xcode open Kingfisher.xcworkspace # Then use Xcode's test navigator or press Cmd+U to run all tests # Or use xcodebuild directly: xcodebuild test -workspace Kingfisher.xcworkspace -scheme Kingfisher -destination "platform=iOS Simulator,name=iPhone 15" ``` ## Test File Organization Reference ### Directory Structure ``` Tests/ ├── Dependency/ │ └── Nocilla/ # HTTP stubbing framework │ ├── LICENSE │ ├── README.md │ └── Nocilla/ # Nocilla source files │ ├── Categories/ # NSData and NSString extensions │ ├── DSL/ # Domain-specific language for stubbing │ ├── Diff/ # Request diff utilities │ ├── Hooks/ # HTTP client hooks (NSURLSession, etc.) │ ├── Matchers/ # Request matching logic │ ├── Model/ # HTTP request/response models │ └── Stubs/ # Stub implementation └── KingfisherTests/ ├── Info.plist ├── KingfisherTestHelper.swift # Main test helper utilities ├── KingfisherTests-Bridging-Header.h # Objective-C bridging ├── Utils/ │ └── StubHelpers.swift # Network stubbing helpers ├── dancing-banana.gif # Animated test image ├── single-frame.gif # Static test image └── *Tests.swift # Individual test files ``` ### Build System Test Targets **Xcode Scheme**: `Kingfisher.xcscheme` - Configured for testing on all supported platforms - Includes code coverage collection - Uses parallel testing when available **Fastlane Configuration**: `fastlane/Fastfile` - `tests` lane: Runs tests on all platforms - `test_ci` lane: CI-specific testing with environment-based destination - `test` lane: Core test execution with scan action - `build` lane: Build-only verification (used for watchOS) **Platform Test Destinations**: - iOS: `platform=iOS Simulator,name=iPhone 15,OS=17.5` - macOS: `platform=macOS` - tvOS: `platform=tvOS Simulator,name=Apple TV,OS=17.5` - watchOS: `platform=watchOS Simulator,name=Apple Watch Series 9 (41mm),OS=10.5` (build only) ### Test Helper Utilities The `KingfisherTestHelper.swift` provides: - Pre-encoded test image data in various formats (PNG, JPEG, GIF, HEIC, MOV) - Test URLs and keys for stubbing - Cache cleanup utilities (`cleanDefaultCache`, `clearCaches`) - Image comparison with tolerance (`renderEqual`) - Timing utilities (`delay`) - Platform-specific test helpers The `StubHelpers.swift` provides: - `stub()` - Create HTTP response stubs with custom data and headers - `delayedStub()` - Create delayed response stubs for timing tests - Network error stubbing capabilities ================================================ FILE: fastlane/Fastfile ================================================ fastlane_version "1.37.0" default_platform :ios platform :ios do desc "Runs all the tests" lane :tests do test(destination: "platform=macOS") test(destination: "platform=iOS Simulator,name=iPhone 16,OS=18.5") test(destination: "platform=tvOS Simulator,name=Apple TV,OS=18.5") build(destination: "platform=watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.5") end lane :test_ci do if ENV["DESTINATION"].include? "watchOS" then build(destination: ENV["DESTINATION"]) else test(destination: ENV["DESTINATION"]) end end lane :build_ci do build(destination: ENV["DESTINATION"]) end lane :test do |options| scan( scheme: "Kingfisher", clean: true, xcargs: "SWIFT_VERSION=5.0", destination: options[:destination] ) end lane :build do |options| xcodebuild( workspace: "Kingfisher.xcworkspace", configuration: "Debug", scheme: "Kingfisher", destination: options[:destination], build: true, build_settings: { "SWIFT_VERSION" => "5.0" } ) end desc "Lint" lane :lint do pod_lib_lint spm end desc "Release new version" lane :release do |options| target_version = options[:version] raise "The version is missed. Use `fastlane release version:{version_number}`.`" if target_version.nil? ensure_git_branch ensure_git_status_clean skip_tests = options[:skip_tests] tests unless skip_tests lint sync_build_number_to_git increment_version_number(version_number: target_version) version_bump_podspec(path: "Kingfisher.podspec", version_number: target_version) log = extract_current_change_log(version: target_version) release_log = update_change_log(log: log) git_commit_all(message: "Bump version to #{target_version}") Actions.sh("git tag -s #{target_version} -m ''") push_to_git_remote xcframework(version: target_version) set_github_release( repository_name: "onevcat/Kingfisher", api_token: ENV['GITHUB_TOKEN'], name: release_log[:title], tag_name: target_version, description: release_log[:text], upload_assets: [ "build/Kingfisher-#{target_version}.xcframework.zip", "build/Kingfisher-iOS-#{target_version}.xcframework.zip" ] ) pod_push end lane :xcframework do |options| version = options[:version] swift_version = options[:swift_version] || "5.0" xcode_version = options[:xcode_version] || "26.2.0" xcodes(version: xcode_version, select_for_current_build_only: true) FileUtils.rm_rf '../build' # Define platform to SDKs mapping PLATFORM_SDKS = { all: [ "macosx", "iphoneos", "iphonesimulator", "appletvos", "appletvsimulator", "watchos", "watchsimulator", "xros", "xrsimulator" ], ios: ["iphoneos", "iphonesimulator"] } def create_archives(sdks, swift_version) frameworks = {} sdks.each do |sdk| archive_path = "build/Kingfisher-#{sdk}.xcarchive" xcodebuild( archive: true, archive_path: archive_path, scheme: "Kingfisher", sdk: sdk, build_settings: { "BUILD_LIBRARY_FOR_DISTRIBUTION" => "YES", "SKIP_INSTALL" => "NO", "SWIFT_VERSION" => swift_version } ) framework_path = "#{archive_path}/Products/Library/Frameworks/Kingfisher.framework" dsym_path = "#{Dir.pwd}/../#{archive_path}/dSYMs/Kingfisher.framework.dSYM" frameworks[framework_path] = { dsyms: dsym_path } end frameworks end def create_and_package_xcframework(frameworks, output_name, version) output_base_name = if output_name.empty? "Kingfisher-#{version}" else "Kingfisher-#{output_name}-#{version}" end output_xcframework_path = "build/#{output_base_name}/Kingfisher.xcframework" create_xcframework( frameworks_with_dsyms: frameworks, output: output_xcframework_path ) Actions.sh("codesign --timestamp -v --sign 'Apple Distribution: Wei Wang (A4YJ9MRZ66)' ../build/#{output_base_name}/Kingfisher.xcframework") zip( path: output_xcframework_path, output_path: "build/#{output_base_name}.xcframework.zip", symlinks: true ) end # Create full platform xcframework all_frameworks = create_archives(PLATFORM_SDKS[:all], swift_version) create_and_package_xcframework(all_frameworks, "", version) # Create iOS only xcframework ios_frameworks = create_archives(PLATFORM_SDKS[:ios], swift_version) create_and_package_xcframework(ios_frameworks, "iOS", version) end before_all do |lane| xcode_version = ENV["XCODE_VERSION"] || "26.2.0" xcodes(version: xcode_version, select_for_current_build_only: true) end after_all do |lane| end error do |lane, exception| end end ================================================ FILE: fastlane/actions/extract_current_change_log.rb ================================================ module Fastlane module Actions class ExtractCurrentChangeLogAction < Action require 'yaml' def self.run(params) yaml = File.read(params[:file]) data = YAML.load(yaml) version = data["version"] raise "The version should match in the input file".red unless (version and version == params[:version]) title = "#{version}" title = title + " - #{data["name"]}" if (data["name"] and not data["name"].empty?) return {:title => title, :version => version, :add => data["add"], :fix => data["fix"], :remove => data["remove"]} end ##################################################### # @!group Documentation ##################################################### def self.description "Extract change log information for a specified version." end def self.details "This action will check input version and change log. If everything goes well, the change log info will be returned." end def self.available_options [ FastlaneCore::ConfigItem.new(key: :version, env_name: "KF_EXTRACT_CURRENT_CHANGE_LOG_VERSION", description: "The target version which is needed to be extract", verify_block: proc do |value| raise "No version number is given, pass using `version: 'version_number'`".red unless (value and not value.empty?) end), FastlaneCore::ConfigItem.new(key: :file, env_name: "KF_EXTRACT_CURRENT_CHANGE_LOG_PRECHANGE_FILE", description: "Create a development certificate instead of a distribution one", default_value: "pre-change.yml") ] end def self.return_value "An object contains change log infomation. {version: }" end def self.is_supported?(platform) true end def self.authors ["onevcat"] end end end end ================================================ FILE: fastlane/actions/git_commit_all.rb ================================================ module Fastlane module Actions class GitCommitAllAction < Action def self.run(params) Action.sh "git add -A" Actions.sh "git commit -am \"#{params[:message]}\"" end ##################################################### # @!group Documentation ##################################################### def self.description "Commit all unsaved changes to git." end def self.available_options [ FastlaneCore::ConfigItem.new(key: :message, env_name: "FL_GIT_COMMIT_ALL", description: "The git message for the commit", is_string: true) ] end def self.authors # So no one will ever forget your contribution to fastlane :) You are awesome btw! ["onevcat"] end def self.is_supported?(platform) true end end end end ================================================ FILE: fastlane/actions/sync_build_number_to_git.rb ================================================ module Fastlane module Actions module SharedValues KF_BUILD_NUMBER = :BUILD_NUMBER end class SyncBuildNumberToGitAction < Action def self.is_git? Actions.sh 'git rev-parse HEAD' return true rescue return false end def self.run(params) if is_git? command = 'git rev-list HEAD --count' else raise "Not in a git repository." end build_number = (Actions.sh command).strip Fastlane::Actions::IncrementBuildNumberAction.run(build_number: build_number) Actions.lane_context[SharedValues::KF_BUILD_NUMBER] = build_number end def self.output [ ['KF_BUILD_NUMBER', 'The new build number'] ] end ##################################################### # @!group Documentation ##################################################### def self.description "Set the build version of your project to the same number of your total git commit count" end def self.authors ["onevcat"] end def self.is_supported?(platform) [:ios, :mac].include? platform end end end end ================================================ FILE: fastlane/actions/update_change_log.rb ================================================ module Fastlane module Actions class UpdateChangeLogAction < Action def self.run(params) log = params[:log] raise "Invalid log object".red unless !log[:title].empty? and !log[:version].empty? readme = File.read(params[:changelogfile]) log_text = "## [#{log[:title]}](https://github.com/onevcat/Kingfisher/releases/tag/#{log[:version]}) (#{Time.now.strftime("%Y-%m-%d")})\n\n" des = "" add = log[:add].map { |i| "* #{i}" }.join("\n") unless log[:add].nil? des = des + "#### Add\n#{add}\n\n" unless add.nil? or add.empty? fix = log[:fix].map { |i| "* #{i}" }.join("\n") unless log[:fix].nil? des = des + "#### Fix\n#{fix}\n\n" unless fix.nil? or fix.empty? remove = log[:remove].map { |i| "* #{i}" }.join("\n") unless log[:remove].nil? des = des + "#### Remove\n#{remove}\n\n" unless remove.nil? or remove.empty? log_text = log_text + des File.open(params[:changelogfile], 'w') { |file| file.write(readme.sub("-----", "-----\n\n#{log_text}---")) } return {:title => log[:title], :text => des} end ##################################################### # @!group Documentation ##################################################### def self.description "Update the change log file with the content of log" end def self.details "Generally speaking, the log is return value of extract_current_change_log action" end def self.available_options [ FastlaneCore::ConfigItem.new(key: :log, env_name: "KF_UPDATE_CHANGE_LOG_LOG", description: "Change log extracted by pre change log file", is_string: false ), FastlaneCore::ConfigItem.new(key: :changelogfile, env_name: "KF_UPDATE_CHANGE_LOG_CHANGE_LOG_FILE", description: "The change log file, if not set, CHANGELOG.md will be used", default_value: "CHANGELOG.md") ] end def self.authors ["onevcat"] end def self.is_supported?(platform) true end end end end