File: README.md
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
- Asynchronous image downloading and caching.
- Loading image from either
URLSession-based networking or local provided data. - Useful image processors and filters provided.
- Multiple-layer hybrid cache for both memory and disk.
- Fine control on cache behavior. Customizable expiration date and size limit.
- Opt-in async cache probing in
KingfisherManagerto avoid blocking the caller thread on disk cache checks. - Cancelable downloading and auto-reusing previous downloaded content to improve performance.
- Independent components. Use the downloader, caching system, and image processors separately as you need.
- Prefetching images and showing them from the cache to boost your app.
- Extensions for
UIImageView,NSImageView,NSButton,UIButton,NSTextAttachment,WKInterfaceImage,TVMonogramViewandCPListItemto directly set an image from a URL. - Built-in transition animation when setting images.
- Customizable placeholder and indicator while loading images.
- Extensible image processing and image format easily.
- Low Data Mode support.
- SwiftUI support.
- Swift 6 & Swift Concurrency (strict mode) prepared.
- Load & cache for Live Photo.
Kingfisher 101
The simplest use-case is setting an image to an image view with the UIImageView extension:
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:
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:
- Downloads a high-resolution image.
- Downsamples it to match the image view size.
- Makes it round cornered with a given radius.
- Shows a system indicator and a placeholder image while downloading.
- When prepared, it animates the small thumbnail image with a "fade in" effect.
- The original large image is also cached to disk for later use, to get rid of downloading it again in a detail view.
- A console log is printed when the task finishes, either for success or failure.
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:
// 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:
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:
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
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '13.0'
use_frameworks!
target 'MyApp' do
pod 'Kingfisher', '~> 8.0'
endPre-built Framework
- Open the release page, download the latest version of Kingfisher from the assets section.
- Drag the
Kingfisher.xcframeworkinto your project and add it to the target (usually the app target). - Select your target, in the "General" Tab, find the "Frameworks, Libraries, and Embedded Content" section, set the
Embed Without Signingto Kingfisher.
Documentation
Check the documentation and tutorials:
Migrating
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 (七巧板), 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 or Sina Weibo. If you find an issue, open a ticket. 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 with a link to your home page.
Become a sponsor through GitHub Sponsors. :heart:
Special thanks to:
License
Kingfisher is released under the MIT license. See LICENSE for details.
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:
sharedsingleton 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:
ImageLoadingResultstruct for download results (lines 36-58)DownloadTaskclass for cancellable downloads (lines 65-102)- URLSession management and request handling
ImageCache.swift (Lines 52-200)
Caching infrastructure:
CacheTypeenum 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:
ImageProcessItemenum for input types (lines 37-46)ImageProcessorprotocol 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 download3. Network Download
KingfisherManager.loadAndCacheImage() (line 415)
│
└─> ImageDownloader.downloadImage()
Sources/Networking/ImageDownloader.swift
│
├─> SessionDelegate.downloadTask()
│ Sources/Networking/SessionDelegate.swift
│
└─> URLSession.dataTask()
│
└─> CompletionHandler with ImageLoadingResult4. Image Processing
Downloaded Data
│
└─> ImageProcessor.process() (line 434)
Sources/Image/ImageProcessor.swift
│
├─> DefaultImageProcessor (if none specified)
│
└─> Custom processors chain
│
└─> Processed KFCrossPlatformImage5. 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 queue6. 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 workflowBuild Workflows
Building with Swift Package Manager
# 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-xcodeprojBuilding with Fastlane
First, install dependencies:
# Install Ruby dependencies
bundle installCommon build commands:
# 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 lintRelease Process
The release workflow automates versioning, tagging, and distribution:
# 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.XRelease steps performed:
- Ensures clean git state and correct branch
- Runs all tests (unless skipped)
- Lints CocoaPods spec and SPM package
- Updates version in all configuration files
- Extracts and updates changelog
- Creates signed git tag
- Builds XCFramework for all platforms
- Creates GitHub release with assets
- 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
# 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 inSources/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 versiongit_commit_all.rb- Commit all changessync_build_number_to_git.rb- Sync build number with gitupdate_change_log.rb- Update changelog file
Troubleshooting
Common Issues
Bundle install fails
bash# Update bundler gem install bundler # Install with specific bundler version bundle _2.x.x_ installXcode 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.appSimulator not found
bash# List available simulators xcrun simctl list devices # Update destination string accordinglyCocoaPods push fails
bash# Verify pod spec locally first pod lib lint Kingfisher.podspec # Register session if needed pod trunk register [email protected]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 XCFrameworkKingfisher-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:
iphoneosSDK - Simulator:
iphonesimulatorSDK - 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:
appletvosSDK - Simulator:
appletvsimulatorSDK - Architectures: arm64, x86_64 (simulator)
watchOS Deployment
- Device:
watchosSDK - Simulator:
watchsimulatorSDK - Architectures: arm64, arm64_32, armv7k (device), i386, x86_64, arm64 (simulator)
visionOS Deployment
- Device:
xrosSDK - Simulator:
xrsimulatorSDK - Architectures: arm64, x86_64 (simulator)
Deployment Commands
Complete Release Process
# 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:trueIndividual Components
# 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_ciCocoaPods Deployment
# Lint podspec
pod lib lint Kingfisher.podspec
# Push to CocoaPods trunk (automated in release)
pod trunk push Kingfisher.podspecSwift Package Manager
# Build with SPM
swift build
# Test with SPM
swift test
# Validate package
swift package resolveContinuous 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:
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:
Pre-release Validation:
- Git branch verification
- Clean git status check
- Comprehensive testing across platforms
- Podspec and SPM linting
Version Management:
- Build number synchronization
- Version number increment
- Podspec version update
- Changelog extraction
Build Artifacts:
- XCFramework creation for all platforms
- Code signing with Apple Distribution certificate
- ZIP archive creation
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:
version: X.X.X
name: Release Name
fix:
- Bug fix descriptions with issue links
add:
- New feature descriptionsVersion 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:
//
// FileName.swift
// Kingfisher
//
// Created by [Author] on [Date].
//
// Copyright (c) 2019 Wei Wang <[email protected]>
//
// 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 letfor type constants,letfor instance constants - Protocols: Use descriptive names, often ending with
-ableor describing capability
Example from /Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.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:
#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
#endifSendable Compliance
Modern Swift concurrency is enforced throughout the codebase:
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:
/// 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<Self> {
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:
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:
/// 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:
/// 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:
@MainActor
public protocol KFOptionSetter {
var options: KingfisherParsedOptionsInfo { get nonmutating set }
var onFailureDelegate: Delegate<KingfisherError, Void> { get }
var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { 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):
// 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:
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
- Create processor conforming to ImageProcessor protocol:
struct CustomProcessor: ImageProcessor {
var identifier: String { "com.example.custom" }
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
// Implementation
}
}- Add convenience method to KFOptionSetter (file:
/Users/onevcat/Sync/github/Kingfisher/Sources/General/KFOptionsSetter.swift):
extension KFOptionSetter {
public func customEffect() -> Self {
appendProcessor(CustomProcessor())
}
}Extending Platform Support
Add platform-specific extensions (pattern from existing platform extensions):
- Update type aliases in
/Users/onevcat/Sync/github/Kingfisher/Sources/General/Kingfisher.swift - Add compatibility conformance:
#if os(newOS)
extension NewOSImageView: KingfisherCompatible { }
#endif- 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):
// 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 componentsNaming Conventions
- Manager classes:
*Manager(e.g.,KingfisherManager) - Data providers:
*Provideror*DataProvider(e.g.,ImageDataProvider) - Processors:
*Processoror*ImageProcessor(e.g.,BlurImageProcessor) - Extensions:
Type+Kingfisher.swift(e.g.,UIButton+Kingfisher.swift) - Protocols: Descriptive names often with
-ablesuffix (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:
ImageCache.default.memoryStorage.config.totalCostLimit = 50 * 1024 * 1024
ImageCache.default.diskStorage.config.sizeLimit = 200 * 1024 * 1024Platform 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 workflowSources/General/KF.swift- Main entry point providing builder pattern API for image tasksSources/General/Kingfisher.swift- Core framework module with KingfisherCompatible protocolSources/General/KingfisherOptionsInfo.swift- Configuration options container for all operationsSources/General/KingfisherError.swift- Error handling system with detailed error typesSources/General/KFOptionsSetter.swift- Options configuration builder for method chaining
Image Source and Data Providers
Sources/General/ImageSource/Resource.swift- URL resource definitions and transformationsSources/General/ImageSource/Source.swift- Abstract image source protocols and implementationsSources/General/ImageSource/ImageDataProvider.swift- Data provider protocol for custom image sourcesSources/General/ImageSource/AVAssetImageDataProvider.swift- AVAsset image frame extractionSources/General/ImageSource/PHPickerResultImageDataProvider.swift- Photo picker integrationSources/General/ImageSource/LivePhotoSource.swift- Live Photo support implementation
Network Layer
Sources/Networking/ImageDownloader.swift- HTTP image downloading with session managementSources/Networking/ImageDownloader+LivePhoto.swift- Live Photo downloading extensionsSources/Networking/ImageDownloaderDelegate.swift- Download progress and completion delegationSources/Networking/SessionDelegate.swift- URLSession delegate handling authentication and redirectsSources/Networking/SessionDataTask.swift- Custom data task wrapper with cancellation supportSources/Networking/ImagePrefetcher.swift- Batch image prefetching for performance optimizationSources/Networking/RequestModifier.swift- HTTP request modification protocolsSources/Networking/ImageModifier.swift- Response image modification protocolsSources/Networking/RedirectHandler.swift- HTTP redirect handling strategiesSources/Networking/RetryStrategy.swift- Failed request retry logic and policiesSources/Networking/AuthenticationChallengeResponsable.swift- Authentication challenge handlingSources/Networking/ImageDataProcessor.swift- Raw image data processing pipeline
Caching System
Sources/Cache/ImageCache.swift- Dual-layer (memory + disk) caching coordinatorSources/Cache/MemoryStorage.swift- In-memory LRU cache with automatic cleanupSources/Cache/DiskStorage.swift- Persistent disk storage with expiration policiesSources/Cache/Storage.swift- Abstract storage protocols and configurationsSources/Cache/CacheSerializer.swift- Image serialization for disk persistenceSources/Cache/FormatIndicatedCacheSerializer.swift- Format-aware image serialization
Image Processing and Transformation
Sources/Image/Image.swift- Cross-platform image type definitions and utilitiesSources/Image/ImageProcessor.swift- Image transformation and processing pipelineSources/Image/Filter.swift- Built-in image filters (blur, tint, overlay, etc.)Sources/Image/ImageFormat.swift- Image format detection and handlingSources/Image/ImageDrawing.swift- Core graphics drawing utilities and extensionsSources/Image/ImageTransition.swift- View transition animations for image loadingSources/Image/ImageProgressive.swift- Progressive JPEG loading implementationSources/Image/GIFAnimatedImage.swift- GIF animation support and playbackSources/Image/GraphicsContext.swift- Graphics context management and utilitiesSources/Image/Placeholder.swift- Placeholder image definitions and protocols
Platform Implementation Files
UIKit Extensions
Sources/Extensions/ImageView+Kingfisher.swift- UIImageView integration with .kf namespaceSources/Extensions/UIButton+Kingfisher.swift- UIButton background/image loading supportSources/Extensions/NSTextAttachment+Kingfisher.swift- Text attachment image loading
AppKit Extensions
Sources/Extensions/NSButton+Kingfisher.swift- macOS NSButton image loading integrationSources/Extensions/HasImageComponent+Kingfisher.swift- Generic image component protocol
Cross-Platform Extensions
Sources/Extensions/PHLivePhotoView+Kingfisher.swift- Live Photo view integrationSources/Extensions/CPListItem+Kingfisher.swift- CarPlay list item support
SwiftUI Components
Sources/SwiftUI/KFImage.swift- Main SwiftUI image view componentSources/SwiftUI/KFAnimatedImage.swift- Animated image support for SwiftUISources/SwiftUI/KFImageOptions.swift- SwiftUI-specific configuration optionsSources/SwiftUI/KFImageProtocol.swift- Shared protocol for KF SwiftUI componentsSources/SwiftUI/KFImageRenderer.swift- SwiftUI view rendering and update logicSources/SwiftUI/ImageBinder.swift- Binding layer between SwiftUI and Kingfisher coreSources/SwiftUI/ImageContext.swift- SwiftUI image loading context management
Custom Views
Sources/Views/AnimatedImageView.swift- Custom animated image view for GIF playbackSources/Views/Indicator.swift- Loading indicator views and protocols
Build System Files
Package Management
Package.swift- Swift Package Manager manifest with dependencies and targets[email protected]- Swift 5.9 compatibility package manifestKingfisher.podspec- CocoaPods specification for distributionGemfile- Ruby dependencies for Fastlane and build toolsGemfile.lock- Locked Ruby gem versions for reproducible builds
Fastlane Build Automation
fastlane/Fastfile- Main Fastlane configuration with lanes for testing, building, and releasingfastlane/actions/extract_current_change_log.rb- Extracts current version changelogfastlane/actions/git_commit_all.rb- Git commit automation with custom messagesfastlane/actions/sync_build_number_to_git.rb- Synchronizes build numbers with git commitsfastlane/actions/update_change_log.rb- Automated changelog managementfastlane/README.md- Fastlane documentation and usage instructions
Xcode Project Files
Kingfisher.xcodeproj/- Main Xcode project with targets and build settingsKingfisher.xcworkspace/- Xcode workspace for integrated developmentDemo/Kingfisher-Demo.xcodeproj/- Demo application Xcode project
Configuration Files
Framework Configuration
Sources/Info.plist- Framework bundle information and version metadataSources/PrivacyInfo.xcprivacy- Privacy manifest for App Store compliance
Development Assets
images/- Sample images for testing and demo applicationskingfisher-1.jpgthroughkingfisher-10.jpg- Test image assetslogo.png- Project logo and branding
Tests/KingfisherTests/dancing-banana.gif- GIF test asset for animation testingTests/KingfisherTests/single-frame.gif- Single-frame GIF for edge case testing
Demo Applications
Demo/Demo/Kingfisher-Demo/- iOS demo app showcasing all featuresViewControllers/- Various demo screens (GIF, transitions, processors, etc.)SwiftUIViews/- SwiftUI demonstration views and regression tests
Demo/Demo/Kingfisher-macOS-Demo/- macOS demo applicationDemo/Demo/Kingfisher-tvOS-Demo/- Apple TV demo applicationDemo/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 helpersSources/Utility/CallbackQueue.swift- Thread-safe callback queue managementSources/Utility/Box.swift- Reference wrapper for value typesSources/Utility/Result.swift- Result type utilities and extensionsSources/Utility/Delegate.swift- Weak delegate wrapper to prevent retain cyclesSources/Utility/Runtime.swift- Runtime reflection and dynamic dispatch utilitiesSources/Utility/DisplayLink.swift- Cross-platform display link abstractionSources/Utility/SizeExtensions.swift- CGSize manipulation and calculationsSources/Utility/String+SHA256.swift- String hashing for cache key generation
Test Infrastructure
Tests/KingfisherTests/KingfisherTestHelper.swift- Shared testing utilities and mocksTests/KingfisherTests/Utils/StubHelpers.swift- HTTP stubbing helpers for network testsTests/Dependency/Nocilla/- HTTP mocking framework for isolated testing
Documentation System
Sources/Documentation.docc/- DocC documentation bundleDocumentation.md- Main documentation entry pointGettingStarted.md- Quick start guide for new usersTutorials/- Step-by-step tutorials for UIKit and SwiftUICommonTasks/- Task-oriented documentation for common use casesTopics/- 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
.kfproperty extension
Naming Conventions
- Files: PascalCase with descriptive names indicating functionality
- Protocols: Suffix with
-ablefor capabilities (e.g.,AuthenticationChallengeResponsable) - Extensions: Platform prefix for clarity (e.g.,
UIButton+Kingfisher.swift) - Tests: Mirror source structure with
Testssuffix
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
.kfnamespace - Options System: Centralized configuration through
KingfisherOptionsInfo - Result Types: Consistent error handling with
Result<Success, KingfisherError> - Callback Queues: Thread-safe completion handling across all async operations
File: docs/issue-2495-analysis.md
Issue #2495 Analysis: Disk Cache Deserialization Cannot Be Cancelled
1. Issue Overview
Issue: #2495 - Images loading from cache are not canceled
Reporter: FaizanDurrani
Status: Open
Symptom
When scrolling quickly through a large collection of images, memory usage explodes (>1 GB). The reporter uses KingfisherWebP and observes that ImageCache.retrieveImageInDiskCache is called for every displayed item, even after imageView.kf.cancelDownloadTask() is invoked.
Instruments Evidence
The attached Instruments Allocations screenshot shows:
- Memory peak ~630 MB during fast scrolling
- Heaviest allocation stack:
ImageCache.retrieveImageInDiskCache(forKey:options:callbackQueue:completionHandler:)— 218.55 MBKingfisherWrapper.image(webpData:options:)— 181.09 MB- WebP decode +
CGImageCreatecalls downstream
The majority of memory is consumed by WebP deserialization, not by disk I/O itself.
2. Root Cause Analysis
The problem has two layers:
Layer 1: Cache Retrieval Is Not Cancellable
cancelDownloadTask() calls DownloadTask.cancel(), which only cancels the network download (SessionDataTask.cancel(token:) in ImageDownloader.swift:121-124).
For images that hit the disk cache, KingfisherManager.retrieveImage returns nil at line 444 — there is no DownloadTask to cancel. Once retrieveImageInDiskCache dispatches its block to ioQueue, the entire chain (disk read → deserialization → memory promotion → callback) runs to completion with no interruption point.
Layer 2: Eager Disk-to-Memory Promotion
Kingfisher's cache architecture is designed as "eager read into memory":
ImageCache.retrieveImage(line 608-655) checks memory first, then falls back to disk.- On disk hit, the image is fully deserialized via
CacheSerializer.image(with:data:options:)onioQueue(line 738). - The deserialized image is unconditionally stored into memory cache via
store(image, toDisk: false)(line 641-645). - There is no lazy decode path, no thumbnail downsampling, and no option to skip memory promotion.
During fast scrolling, each reused cell triggers a new setImage call. The previous cell's disk cache retrieval is already enqueued on the serial ioQueue. With N cells scrolled through rapidly, N deserialization blocks queue up. WebP decoding is particularly expensive — each decode allocates significant memory. The combined allocations exceed NSCache's eviction speed, causing the memory spike.
Why Existing Mechanisms Don't Help
| Mechanism | Why It Doesn't Help |
|---|---|
cancelDownloadTask() |
Returns nil for cache hits — nothing to cancel |
taskIdentifier check in view completion (line 331) |
Only runs after deserialization completes — the damage is done |
fromMemoryCacheOrRefresh option |
Skips disk cache entirely — loses all disk caching, not a valid workaround |
NSCache eviction (totalCostLimit = 25% RAM) |
LRU eviction is reactive, can't keep up with burst allocations |
3. Proposed Solution
Approach: Source Task Identifier Check Before Deserialization
Leverage the existing Source.Identifier / taskIdentifier mechanism — which already works at the view layer — and extend it into the cache retrieval path. The check is inserted before the expensive deserialization step, skipping decode for tasks that are no longer relevant.
Key Properties
- No public API changes — all modifications are internal
- Backward compatible — direct
KingfisherManagercallers are unaffected (checker is nil) - Completion handler contract preserved — every
setImagecall produces exactly one completion callback - Worst case identical to current behavior — if the check doesn't fire in time, the decode proceeds as it does today
Changes Required
3.1 `KingfisherParsedOptionsInfo` — New Internal Property
Add a closure property that flows through the options chain:
var sourceTaskIdentifierChecker: (@Sendable () -> Bool)?Returns true if the task is still current, false if it has been superseded. This property is internal — not exposed in the public KingfisherOptionsInfoItem enum.
3.2 `KingfisherManager.retrieveImage` — Store Checker in Options
Currently referenceTaskIdentifierChecker (line 306) is only applied to onDataReceived side effects (line 329-332). Add one line to also store it in options:
if let checker = referenceTaskIdentifierChecker {
options.onDataReceived?.forEach {
$0.onShouldApply = checker
}
options.sourceTaskIdentifierChecker = checker // NEW
}This ensures the checker propagates to ImageCache via the options struct.
3.3 `ImageCache.retrieveImageInDiskCache` — Two Check Points
Insert checks before disk read and before deserialization:
/* Detailed source-code truncated for AI context efficiency. */Returning .success(nil) uses the same semantics as "image not found on disk", which is safely handled by all upstream callers.
3.4 `KingfisherManager.retrieveImageFromCache` — Prevent Spurious Download (Path 2 Only)
retrieveImageFromCache has two code paths:
Path 1 (line 639-697): Processed image lookup. On
.noneresult, callscompletionHandler(.failure(.imageNotExisting)). SinceretrieveImageFromCachealready returnedtruesynchronously, no download is triggered. No change needed.Path 2 (line 716-793): Original image fallback with custom processor. On
.noneresult, callsself.loadAndCacheImage(...)(line 732) — this does trigger a download. Add a check here:
guard let image = cacheResult.image else {
// NEW: Task is stale — report error instead of downloading.
if let checker = options.sourceTaskIdentifierChecker, !checker() {
let error = KingfisherError.cacheError(reason: .imageNotExisting(key: key))
options.callbackQueue.execute { completionHandler?(.failure(error)) }
return
}
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
}Summary of File Changes
| File | Change | Lines Affected |
|---|---|---|
Sources/General/KingfisherOptionsInfoItems.swift |
Add sourceTaskIdentifierChecker property |
~1 line |
Sources/General/KingfisherManager.swift |
Store checker in options | ~1 line |
Sources/Cache/ImageCache.swift |
Two cancellation checks in retrieveImageInDiskCache |
~10 lines |
Sources/General/KingfisherManager.swift |
Prevent spurious download in path 2 | ~5 lines |
Total: ~17 lines of effective code.
4. Timing and Behavior Simulation
Scenario A: Fast Scrolling (Primary Problem Case)
A cell is reused 5 times within one layout pass. All setImage calls complete on the main thread before the serial ioQueue processes any block.
Main thread (single runloop pass):
cellForItem → cell X: setImage(url_1), X.taskId = 101, dispatch block_1 to ioQueue
cellForItem → cell X: setImage(url_5), X.taskId = 102, dispatch block_2 to ioQueue
cellForItem → cell X: setImage(url_9), X.taskId = 103, dispatch block_3 to ioQueue
cellForItem → cell X: setImage(url_13), X.taskId = 104, dispatch block_4 to ioQueue
cellForItem → cell X: setImage(url_17), X.taskId = 105, dispatch block_5 to ioQueue
↕ main thread yields
ioQueue (serial, starts processing):
block_1: CHECK 1 → X.taskId==101? → 105≠101 → SKIP (no disk read, no decode)
block_2: CHECK 1 → X.taskId==102? → 105≠102 → SKIP
block_3: CHECK 1 → X.taskId==103? → 105≠103 → SKIP
block_4: CHECK 1 → X.taskId==104? → 105≠104 → SKIP
block_5: CHECK 1 → X.taskId==105? → 105==105 → read disk → CHECK 2 → still valid → DECODE ✓Result: 4 out of 5 decodes skipped. Only the actually-needed image is decoded.
Current behavior: All 5 images decoded, all promoted to memory, 4 results discarded at view layer.
Check hit rate: (N-1)/N → approaches 100% with faster scrolling.
Scenario B: Moderate Scrolling
User scrolls at a pace where each cell stays visible for ~200ms. Decode takes ~100ms per image.
Main thread:
t=0ms setImage(url_1), X.taskId = 101, dispatch block_1
ioQueue:
t=5ms block_1: CHECK 1 → 101==101 ✓ → read disk (10ms)
t=15ms CHECK 2 → 101==101 ✓ → decode (100ms)
t=115ms decode complete → store to memory → callback
Main thread:
t=200ms cell reuse, setImage(url_5), X.taskId = 102, dispatch block_2Result: Decode completes before cell reuse. Check does not fire.
Current behavior: Identical — image was needed and displayed.
Impact: None — and none needed, since one-at-a-time decode doesn't cause memory pressure.
Scenario C: Moderate Scrolling with Large File
Same pace as B, but a very large WebP file takes 300ms to read from disk.
Main thread:
t=0ms setImage(url_1), X.taskId = 101, dispatch block_1
ioQueue:
t=5ms block_1: CHECK 1 → 101==101 ✓ → start disk read (300ms)
Main thread:
t=200ms cell reuse, setImage(url_5), X.taskId = 102
ioQueue:
t=305ms disk read complete → CHECK 2 → 101==102? → 101≠102 → SKIP decode ✓Result: CHECK 2 catches the stale task after a slow disk read. Decode skipped.
Current behavior: Full decode runs (expensive), result discarded at view layer.
This is where the two-check design pays off — CHECK 1 was valid at dispatch time, but CHECK 2 catches the change.
Scenario D: Multiple Visible Cells (No Reuse)
10 cells appear simultaneously on initial load. Each cell has a different image, no cell reuse occurs.
Main thread:
cell A: setImage(url_1), A.taskId = 101
cell B: setImage(url_2), B.taskId = 102
...
cell J: setImage(url_10), J.taskId = 110
ioQueue:
block for cell A: CHECK → A.taskId==101? → 101==101 ✓ → decode
block for cell B: CHECK → B.taskId==102? → 102==102 ✓ → decode
...
block for cell J: CHECK → J.taskId==110? → 110==110 ✓ → decodeResult: All 10 images decoded. This is correct — all 10 are needed.
Current behavior: Identical.
Impact: None. The check is per-cell (each cell has its own taskIdentifier), so concurrent requests for different cells don't interfere with each other.
Scenario E: Fast Scrolling with Custom Processor (Path 2)
User has a custom ImageProcessor. Original image is cached on disk, processed version is not. Cell reuses rapidly.
Main thread:
setImage(url_1, processor: blur), X.taskId = 101
setImage(url_5, processor: blur), X.taskId = 102
retrieveImageFromCache:
Path 1: processed image not cached → validCache = false
Path 2: original image cached → canUseOriginalImageCache = true
originalCache.retrieveImage → ioQueue:
block_1: CHECK → 101==102? → SKIP decode → .success(nil)
Path 2 callback:
cacheResult.image == nil
→ CHECK: 101==102? → stale
→ completionHandler(.failure(.imageNotExisting)) ← no download triggered ✓
View layer:
issuedIdentifier(101) ≠ taskIdentifier(102) → .notCurrentSourceTask
completionHandler(.failure(.notCurrentSourceTask)) ← user gets callback ✓Without the path 2 fix: loadAndCacheImage would be called, triggering a network download that is ultimately discarded.
5. Completion Handler Contract Verification
Contract: Every setImage call produces exactly one completion callback.
For the "skipped" task (stale identifier):
retrieveImageInDiskCache → .success(nil) // skipped decode
↓
ImageCache.retrieveImage → .success(.none) // treated as "not found"
↓
retrieveImageFromCache:
Path 1 → completionHandler(.failure(.imageNotExisting)) ✅ called
Path 2 → checker prevents download,
completionHandler(.failure(.imageNotExisting)) ✅ called
↓
handler (line 386) → failCurrentSource → completionHandler(.failure(...))
↓
View extension (line 331):
issuedIdentifier ≠ taskIdentifier →
completionHandler(.failure(.notCurrentSourceTask)) ✅ user receives callbackFor the "current" task (valid identifier):
Behavior is identical to today — full decode, memory promotion, success callback. No changes in this path.
6. Risk Assessment
6.1 Thread Safety of `taskIdentifier` Access
Status: Pre-existing issue, not introduced by this change.
The checker closure { issuedIdentifier == self.taskIdentifier } reads taskIdentifier (associated object on UIImageView) from ioQueue, while writes happen on @MainActor. The associated object uses OBJC_ASSOCIATION_RETAIN_NONATOMIC.
Mitigation factors:
objc_getAssociatedObject/objc_setAssociatedObjectuse an internal lock in the ObjC runtime (AssociationsManagermutex). Memory access is serialized at the runtime level.- The value is wrapped in
Box. After reading the Box reference, we immediately extract theUIntvalue (copy semantics). No retained reference escapes. - The same pattern already exists in
onShouldApplyforImageProgressiveProvider(called from background decoder thread).
Worst case of torn read:
- Read old value (checker thinks task is still valid) → proceed with decode → same as current behavior
- Read new value (checker thinks task is stale) → skip a decode that was actually still needed → cell shows placeholder until the truly-current task completes
Recommendation: Consider changing OBJC_ASSOCIATION_RETAIN_NONATOMIC to OBJC_ASSOCIATION_RETAIN in a separate PR, independent of this optimization. This would make the contract explicit without affecting performance (associated object access is already locked).
6.2 Spurious `.imageNotExisting` Error Propagation
When a skipped task returns .success(nil), it becomes .imageNotExisting upstream. This error flows through handler → failCurrentSource (line 352-383).
failCurrentSource checks:
error.isTaskCancelled(line 354) — no, this is a cache error, not a cancellation error. Won't short-circuit.error.isLowDataModeConstrained(line 359) — no. Won't trigger low-data-mode fallback.retrievingContext.popAlternativeSource()(line 369) — if alternative sources are configured, the next source will be tried.
Impact: If a user configures alternativeSources, a skipped task may trigger retrieval from the next alternative source. This retrieval result will be discarded at the view layer (stale taskIdentifier), but it wastes a network request or cache lookup.
Severity: Low. alternativeSources is a rarely-used feature. The wasted work is bounded (one alternative attempt that gets discarded). The exact same waste occurs today when a network download completes for a stale task.
Possible future improvement: Introduce a dedicated CacheErrorReason.taskCancelled case so that failCurrentSource short-circuits at line 354. This would require adding a case to the public KingfisherError.CacheErrorReason enum — a source-breaking change if users have exhaustive switches. Could be considered for the next major version.
6.3 Already-Running Decode Cannot Be Interrupted
Once cacheSerializer.image(with:data:options:) begins execution, it runs to completion. There is no mid-decode cancellation.
Mitigating factor: The ioQueue is serial. At most one decode runs at any time. The memory consumed by a single in-flight decode is bounded and manageable. The issue in #2495 is caused by dozens of decodes queuing up and running sequentially while their decoded images accumulate in memory — our check prevents the queue from growing unboundedly.
6.4 `loadDiskFileSynchronously` Mode
When loadDiskFileSynchronously is true, the loading queue is .untouch (caller's thread, typically main). In this case:
- CHECK 1 runs on the main thread synchronously —
taskIdentifierwas just set, so the check always passes. - The disk read and decode run synchronously on the main thread, blocking the runloop. No opportunity for cell reuse.
Impact: No behavior change for synchronous mode. This is expected — synchronous mode is inherently non-cancellable.
6.5 Direct `KingfisherManager` / `ImageCache` API Users
Users who call KingfisherManager.shared.retrieveImage(with:options:completionHandler:) or ImageCache.default.retrieveImage(forKey:...) directly:
sourceTaskIdentifierCheckeris never set (it's only populated from the view extension path).- All checks evaluate to
if let checker = nil→ skip → original behavior.
Impact: None. Full backward compatibility.
7. Summary
What This Fix Achieves
- Skips expensive deserialization for disk-cached images when the requesting view has already moved on to a different image
- Prevents spurious network downloads when a cancelled cache lookup for a custom-processed image returns "not found"
- Preserves the completion handler contract — every
setImagealways results in exactly one completion callback
What This Fix Does NOT Achieve
- Cannot interrupt a decode that is already in progress (inherent limitation of
CacheSerializerprotocol) - Does not add a first-class cancellation API for cache lookups (would require public API changes)
- Does not address the
taskIdentifierdata race (pre-existing, should be fixed independently) - Does not change the eager disk-to-memory promotion design (a larger architectural topic)
Estimated Effectiveness
| Scrolling Speed | Decodes Prevented | Memory Pressure Reduction |
|---|---|---|
| Fast (>10 cells/sec) | ~90-100% of stale decodes | Significant |
| Moderate (~5 cells/sec) | ~50-70% of stale decodes | Moderate |
| Slow (<2 cells/sec) | ~0% (but no pressure) | N/A |
8. Implementation Plan
Step 1: Add `sourceTaskIdentifierChecker` to `KingfisherParsedOptionsInfo`
File: Sources/General/KingfisherOptionsInfo.swift
Location: Line 420, after onDataReceived (the other internal property)
Add:
var onDataReceived: [any DataReceivingSideEffect]? = nil
var sourceTaskIdentifierChecker: (@Sendable () -> Bool)? = nil // NEWThis is an internal property (no public modifier), matching the pattern of onDataReceived. It does not need handling in the init(_ info:) switch since it is never set from KingfisherOptionsInfoItem.
Verification: Build succeeds. No public API surface change.
Step 2: Store `referenceTaskIdentifierChecker` into options
File: Sources/General/KingfisherManager.swift
Location: Line 329-333, inside retrieveImage(with:options:...referenceTaskIdentifierChecker:...)
Change from:
if let checker = referenceTaskIdentifierChecker {
options.onDataReceived?.forEach {
$0.onShouldApply = checker
}
}To:
if let checker = referenceTaskIdentifierChecker {
options.onDataReceived?.forEach {
$0.onShouldApply = checker
}
options.sourceTaskIdentifierChecker = checker
}This one-line addition causes the checker to propagate through RetrievingContext.options into every downstream call that receives KingfisherParsedOptionsInfo, including ImageCache.retrieveImage and retrieveImageInDiskCache.
Verification: Build succeeds. Existing tests pass — no behavioral change yet.
Step 3: Insert cancellation checks in `retrieveImageInDiskCache`
File: Sources/Cache/ImageCache.swift
Location: Line 730-749, the loadingQueue.execute block in the internal retrieveImageInDiskCache
Change from:
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`.")
}
}To:
loadingQueue.execute {
// CHECK 1: For blocks queued behind others on the serial ioQueue,
// the task is likely already stale by the time execution begins.
if let checker = options.sourceTaskIdentifierChecker, !checker() {
callbackQueue.execute { completionHandler(.success(nil)) }
return
}
do {
var image: KFCrossPlatformImage? = nil
if let data = try self.diskStorage.value(
forKey: computedKey,
forcedExtension: options.forcedExtension,
extendingExpiration: options.diskCacheAccessExtendingExpiration
) {
// CHECK 2: Disk read completed, but deserialization has not started.
// Catches staleness that occurred during a slow disk read.
if let checker = options.sourceTaskIdentifierChecker, !checker() {
callbackQueue.execute { completionHandler(.success(nil)) }
return
}
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`.")
}
}Why two checks:
- CHECK 1 fires for queued blocks whose task became stale while waiting. Skips both disk I/O and decode. This is the high-hit-rate path during fast scrolling.
- CHECK 2 fires when disk read was slow (large file) and the task became stale during the read. Skips only decode.
Verification: Build succeeds. Test with a fast-scrolling collection view and Instruments to confirm decode skipping.
Step 4: Prevent spurious download in `retrieveImageFromCache` path 2
File: Sources/General/KingfisherManager.swift
Location: Line 724-738, inside the originalCache.retrieveImage callback in retrieveImageFromCache
Change from:
result.match(
onSuccess: { cacheResult in
guard let image = cacheResult.image else {
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
}To:
result.match(
onSuccess: { cacheResult in
guard let image = cacheResult.image else {
// If the task is stale (cancelled by a newer setImage call),
// report cache miss instead of triggering a network download.
if let checker = options.sourceTaskIdentifierChecker, !checker() {
let error = KingfisherError.cacheError(reason: .imageNotExisting(key: key))
options.callbackQueue.execute { completionHandler?(.failure(error)) }
return
}
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
}Why this is needed: Path 2 handles original-image fallback with custom processors. When the original image's decode is skipped (returning .none), the existing code falls through to loadAndCacheImage, triggering a network download. The check prevents this wasted request while still calling completionHandler to preserve the contract.
Verification: Build succeeds. Existing tests pass. For targeted testing: configure a custom ImageProcessor with disk-cached original images, fast-scroll, and verify no spurious network requests in Charles/Instruments.
Step 5: Testing
Unit Tests
Add test cases in Tests/KingfisherTests/:
Cache retrieval skip on stale identifier: Set up a disk-cached image. Simulate a stale
sourceTaskIdentifierChecker(returnsfalse). VerifyretrieveImageInDiskCachereturns.success(nil)without invokingCacheSerializer.image(with:).Completion handler contract: Call
setImage, then immediately callsetImageagain on the same view. Verify the first call's completion receives.notCurrentSourceTaskerror.Valid identifier proceeds normally: Set up a disk-cached image. Provide a checker that returns
true. Verify the image is decoded and returned normally.Path 2 download prevention: Configure a custom processor, cache only the original image on disk. Provide a stale checker. Verify
loadAndCacheImageis NOT called and completion receives.imageNotExisting.
Manual / Integration Testing
- Instruments: Profile a fast-scrolling collection view with WebP images. Compare memory peak before and after the change.
- Functional: Verify images display correctly at all scrolling speeds. No blank or stuck cells.
- Edge cases: Test with
loadDiskFileSynchronously = true,fromMemoryCacheOrRefresh = true, andalternativeSourcesconfigured.
Execution Order
Step 1 (add property)
└─ Step 2 (wire checker into options)
└─ Step 3 (cache-level checks) ── can be done in parallel ── Step 4 (path 2 guard)
│
Step 5 (tests)Steps 1→2 are prerequisites. Steps 3 and 4 are independent of each other. Step 5 should cover all changes.
Review From Codex
Overall Assessment
I agree with the root cause analysis in this document:
- The main issue is not
cancelDownloadTask()itself, but that the disk-cache retrieval path has no cancellation point once work is enqueued. - The current disk-hit behavior eagerly deserializes the image and then promotes it into memory cache, even when the requesting view has already moved on to another source.
- The existing
taskIdentifiercheck at the view layer happens too late. It prevents stale results from being applied to the view, but it does not prevent the expensive decode work from already happening.
These points are consistent with the current implementation in:
Sources/Extensions/ImageView+Kingfisher.swiftSources/General/KingfisherManager.swiftSources/Cache/ImageCache.swiftSources/Networking/ImageDownloader.swift
Main Concern With the Proposed Fix
I do not recommend shipping the proposed solution exactly as written.
The main problem is that it represents a stale request as .success(nil) inside retrieveImageInDiskCache, which then becomes a normal cache miss (.none / .imageNotExisting) upstream.
That encoding leaks the wrong semantics into higher layers:
ImageCache.retrieveImage(forKey:options:...)treatsnilas a normal disk miss.KingfisherManager.retrieveImageFromCachethen treats that result as an ordinary cache failure path.- At the manager level, ordinary failures can still flow into retry logic or
alternativeSources.
So the side effect is broader than only "path 2 may trigger a spurious download". Both retry and alternative-source logic can be activated by a stale request that should have been short-circuited as "no longer relevant", not "cache miss".
This is the strongest reason I would not merge the current proposal unchanged.
Thread-Safety Concern
The document's thread-safety discussion is somewhat optimistic.
Today, the checker closure reads taskIdentifier from a background queue, while writes happen on @MainActor. The storage behind taskIdentifier is implemented with associated objects and OBJC_ASSOCIATION_RETAIN_NONATOMIC.
Changing that association to atomic might reduce some risk, but it does not really solve the larger issue: a background queue is still observing UI-associated state across actor boundaries.
If this optimization is added, I suggest tightening this part of the design as well, instead of relying on the current associated-object pattern more heavily than it already is.
Additional Observation
The proposed check points are directionally correct, but there is one more cheap place worth checking:
- after disk read
- before serializer decode
- before
backgroundDecode
The dominant cost for WebP is likely already inside the serializer path, so this does not change the main conclusion. Still, adding a final stale check before backgroundDecode is nearly free and keeps the behavior more consistent.
Recommended Direction
I recommend keeping the overall approach, but changing the internal representation of the outcome.
Instead of encoding "stale request" as .success(nil) or .imageNotExisting, use an explicit internal stale state, for example an internal retrieval result such as:
enum DiskRetrievalResult {
case image(KFCrossPlatformImage)
case none
case stale
}Then:
ImageCachecan stop work early and report.staleinternally.KingfisherManagercan short-circuit.stalewithout entering retry logic.retrieveImageFromCachepath 2 can avoid download fallback for stale work.- completion behavior at the view layer remains unchanged, because the view will still eventually surface
.notCurrentSourceTaskfor the outdated request.
This preserves the optimization benefit without polluting existing cache-miss semantics.
Recommended Checker Design
I would also prefer that the checker read from a dedicated thread-safe token holder instead of reading the view's associated-object-backed taskIdentifier directly from a background queue.
Using sourceTaskIdentifierChecker as an internal option is fine. The weak point is not the propagation mechanism; it is the current state source used by the checker.
Memory-Model Note
This issue is primarily about avoiding stale decode work, and that is worth fixing first.
However, the current memory spike is amplified by another design choice as well: on disk hit, the decoded image is unconditionally promoted back into memory cache. If memory pressure reports continue even after stale decode skipping is added, the next place to revisit is whether disk-hit promotion should always be eager.
That is a larger architectural question and should probably remain separate from the narrow fix for #2495.
Documentation Note
One minor accuracy issue in the implementation plan: the file name should be Sources/General/KingfisherOptionsInfo.swift, not KingfisherOptionsInfoItems.swift.
Merge Recommendation
My recommendation is:
- Keep the idea of checking staleness before expensive disk-cache deserialization.
- Do not encode stale work as a normal cache miss.
- Introduce an internal explicit stale outcome and short-circuit it in
KingfisherManager. - Tighten the checker state source so it does not depend on reading UI-associated state from a background queue.
- Add regression tests for retry strategy and
alternativeSources, not only for path 2 fallback.
With those adjustments, the fix looks worthwhile and technically sound. Without them, the current proposal is likely to reduce memory pressure in the target scenario, but it also introduces semantic leakage into error handling and recovery paths.
Response From Claude
Overall
The Codex review is thorough and technically accurate. All five recommendations are accepted and should be incorporated into the final implementation.
On the Main Concern (`.success(nil)` Semantic Leakage)
This is the most valuable point in the review, and it identifies a flaw that our original analysis underestimated.
Section 6.2 of this document assessed the alternativeSources interaction as "Low" severity. That assessment was incomplete. The retry strategy interaction is worse: a stale request encoded as .imageNotExisting enters handler (KingfisherManager.swift:394), which consults retryStrategy. If the user has configured a retry strategy (e.g., DelayRetryStrategy(maxRetryCount: 3)), the stale request loops through startNewRetrieveTask → imageCachedType (still cached) → retrieveImage → ioQueue → checker (still stale) → .imageNotExisting → retry again, up to maxRetryCount times. Each cycle is individually cheap (the checker skips decode), but the loop is semantically wrong and wastes work.
The DiskRetrievalResult enum approach cleanly eliminates this. By distinguishing .stale from .none at the ImageCache boundary, KingfisherManager can short-circuit before entering retry or alternative-source logic. The additional complexity is minimal (~10 lines for the enum + ~10 lines for handling), and it makes the control flow explicit and correct.
On Thread Safety
The review correctly notes that our thread-safety discussion is optimistic. The practical risk is indeed low — objc_getAssociatedObject uses an internal runtime lock, and the Box value is immediately copied — but "probably safe due to runtime implementation details" is not a strong engineering argument for a library consumed by thousands of apps.
More importantly, we are not introducing this pattern — the existing onShouldApply in ImageProgressiveProvider already reads taskIdentifier from a background thread. Our change would amplify the reliance on this pattern (every disk cache retrieval instead of only progressive loads), which makes the case for fixing it stronger.
A CancellationToken (~15 lines) replaces the cross-actor read with a lock-protected Bool. The checker becomes { !token.isCancelled } instead of { issuedIdentifier == self.taskIdentifier }. This is self-evidently correct and does not depend on ObjC runtime internals. It should be included in the initial PR rather than deferred, since the incremental cost is small and the design becomes clean from the start.
On `backgroundDecode` Check
Agreed. decoded(scale:) forces bitmap rendering into a new context. The cost is secondary to WebP deserialization, but the check is nearly free and keeps the pattern consistent. Include it.
On Regression Tests
The original test plan (Section 8, Step 5) covered the basic cases but missed two important scenarios:
Retry strategy with stale request: Verify that a stale disk cache retrieval does not enter the retry loop. With
DiskRetrievalResult.stale, the handler should short-circuit before consultingretryStrategy.Alternative sources with stale request: Verify that a stale result does not trigger
popAlternativeSource(). The.staleresult should bypassfailCurrentSourceentirely.
These should be added to the test plan.
On the Documentation Note
Confirmed. Section 3.1 and the Summary table in Section 3 reference KingfisherOptionsInfoItems.swift. The correct file name is KingfisherOptionsInfo.swift. This will be corrected when the implementation plan is finalized.
Summary of Accepted Changes
All five recommendations are accepted for the initial PR:
| # | Recommendation | Rationale | Additional Code |
|---|---|---|---|
| 1 | Keep staleness checking before deserialization | Core optimization, already planned | 0 |
| 2 | Use DiskRetrievalResult instead of .success(nil) |
Prevents semantic leakage into retry and alternative-source logic | ~10 lines |
| 3 | Short-circuit .stale in KingfisherManager |
Avoids retry loops and spurious downloads for stale requests | ~10 lines |
| 4 | CancellationToken for thread-safe state |
Eliminates cross-actor associated-object reads from ioQueue | ~15 lines |
| 5 | Regression tests for retry and alternativeSources |
Covers the interaction that the original test plan missed | Test code |
Total additional production code over the original proposal: ~35 lines. The overall change remains small and focused.
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 coordinatorSources/Cache/MemoryStorage.swift- In-memory cache implementationSources/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))
- iOS 13.0+ (
- 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 testingTests/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 functionalityTests/KingfisherTests/MemoryStorageTests.swift- Memory cache specific testsTests/KingfisherTests/DiskStorageTests.swift- Disk storage specific testsTests/KingfisherTests/StorageExpirationTests.swift- Cache expiration policy tests
Networking Tests
Tests/KingfisherTests/ImageDownloaderTests.swift- Image downloading and session managementTests/KingfisherTests/ImagePrefetcherTests.swift- Batch image prefetching functionalityTests/KingfisherTests/DataReceivingSideEffectTests.swift- Data processing side effects
Manager Tests
Tests/KingfisherTests/KingfisherManagerTests.swift- Central coordinator testsTests/KingfisherTests/KingfisherOptionsInfoTests.swift- Configuration options tests
2. Image Processing Tests
Tests/KingfisherTests/ImageProcessorTests.swift- Image transformation pipeline testsTests/KingfisherTests/ImageDrawingTests.swift- Image drawing and rendering testsTests/KingfisherTests/ImageExtensionTests.swift- Core image extensions testsTests/KingfisherTests/ImageModifierTests.swift- Request modifier tests
3. UI Integration Tests
UIKit Tests
Tests/KingfisherTests/ImageViewExtensionTests.swift- UIImageView extension testsTests/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 testsTests/KingfisherTests/ImageDataProviderTests.swift- Custom data provider testsTests/KingfisherTests/RetryStrategyTests.swift- Network retry strategy testsTests/KingfisherTests/StringExtensionTests.swift- String utility extension tests
Running Tests
Using Fastlane
# 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
# 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 filesBuild 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
testslane: Runs tests on all platformstest_cilane: CI-specific testing with environment-based destinationtestlane: Core test execution with scan actionbuildlane: 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 headersdelayedStub()- Create delayed response stubs for timing tests- Network error stubbing capabilities
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 coordinatorSources/General/KF.swift- Builder pattern API (KF.url()...)Sources/Extensions/ImageView+Kingfisher.swift- UIKit/AppKit extensionsSources/SwiftUI/KFImage.swift- SwiftUI components
Essential Build Commands:
# 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.XDocumentation
For LLMs and Developers:
- Project Overview - What Kingfisher does, core purpose, technology stack, and platform support
- Architecture - System organization, component map, key files, and data flow with specific file references
- Build System - Swift Package Manager and Fastlane workflows, platform setup, and troubleshooting
- Testing - Test categories, running tests, and test infrastructure with file locations
- Development - Code style, implementation patterns, workflows, and common solutions
- Deployment - Package types, platform deployment, release management, and CI/CD
- File Catalog - Comprehensive file organization with specific file purposes and relationships
Configuration Files:
Package.swift- Swift Package Manager manifestKingfisher.podspec- CocoaPods specificationfastlane/Fastfile- Build automationSources/Documentation.docc/- DocC documentation
Key Patterns:
- Namespace wrapper (
.kfproperty) inSources/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+