Nuke 13 achieves Data Race Safety by migrating pipeline work to Swift Concurrency, replacing DispatchQueue and OperationQueue with a @globalActor-based synchronization model. It also ships over 10 new APIs, including progressive preview policies, a willLoadData auth hook, memory size limits, and type-safe ImageRequest properties.
The test suite was rewritten in Swift Testing with Swift 6 mode enabled and significantly expanded:
| Version | Source lines | Tests | Test lines | Coverage |
|---|---|---|---|---|
| Nuke 13.0 | 4,669 | 768 | 8,509 | 96.0% |
| Nuke 12.9 | 4,589 | 496 | 6,167 | 92.4% |
Requirements
- Minimum supported Xcode version: 26.0.
- Minimum required platforms: iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15.
Concurrency & Data Race Safety
- Replace the internal serial
DispatchQueuewith a@globalActor(ImagePipelineActor) for pipeline synchronization, making thread-safety compiler-enforced. The actor is public so that customImagePipeline.Delegateimplementations can use it when needed to reduce thread hops - Replace
OperationQueue-based scheduling with a customTaskQueuesynchronized onImagePipelineActor. Background operations like image processing and decoding now run on the default Swift Concurrency executors, eliminating unnecessary thread hops. The entire pipeline is now a good Swift Concurrency citizen - Replace callback-based
DataLoadingprotocol with async/await:loadData(with:)now returns(AsyncThrowingStream<Data, Error>, URLResponse). RemoveCancellableprotocol - Add typed throws (
throws(ImagePipeline.Error)) toImageTask.image,ImageTask.response,ImagePipeline.image(for:), andImagePipeline.data(for:). AddImagePipeline.Error.cancelledcase. Cancellation now throws this instead ofCancellationError. - Change
userInfotype from[UserInfoKey: Any]to[UserInfoKey: any Sendable]in bothImageRequestandImageContainer - Add
@MainActor @Sendableto completion-basedloadImage/loadDataclosure parameters - Add
@MainActor @Sendabletoprogressandcompletionclosures inNukeExtensionsloadImagefunctions - Add
@MainActor @Sendableto all callback closures inNukeUI:FetchImage.onStart/onCompletion,LazyImage.onStart/onCompletionmodifiers,LazyImageView.onStart/onPreview/onProgress/onSuccess/onFailure/onCompletion - Eliminate an actor hop during
ImageTaskstartup, reducing per-request overhead - Synchronize
ResumableDataStorageonImagePipelineActor, replacingNSLockwith actor isolation and removing@unchecked Sendable. - Convert unit tests to Swift Testing and enable Swift 6 mode for all tests
New Features
- Add
ImagePipeline.PreviewPolicy(.incremental,.thumbnail,.disabled) to control how progressive previews are generated per-request - Add
ImagePipelineDelegate.previewPolicy(for:pipeline:)for customizing the policy dynamically. Default policy:.incrementalfor progressive JPEGs and GIFs,.disabledfor everything else (baseline JPEGs, PNGs, etc.), restoring the original behavior beforeCGImageSourceCreateIncrementalwas adopted - Add
ImagePipeline.Delegate.willLoadData(for:urlRequest:pipeline:), an async, throwing hook that intercepts theURLRequestjust before data loading begins. Use it to inject auth tokens, sign requests, or perform any async pre-flight work. Throw to cancel with a meaningful error (e.g., when a token refresh fails). Default implementation returns the request unchanged — #774 - Add
ImageRequest.init(id:image:)that accepts an async closure returning anImageContainerdirectly. Use it to process images already in memory or to integrate with systems that provide pre-decoded images (e.g., Photos framework). The image skips data decoding entirely and is loaded inTaskFetchOriginalImage– #823 - Add type-safe
imageID,scale, andthumbnailproperties toImageRequest, replacing the previoususerInfodictionary-based approach. The new properties are more ergonomic and improve performance by eliminating dictionary lookups andAnyboxing. TheuserInfo[.imageIdKey],userInfo[.scaleKey], anduserInfo[.thumbnailKey]keys are deprecated. The newimageIDproperty replacesimageIdto follow idiomatic Swift naming (uppercase "ID") and is now also writable – #772 - Add
ImagePipeline.Configuration.progressiveDecodingInterval(default: 0.5s) to throttle progressive decoding attempts when data arrives faster than the interval - Add
ImagePipeline.Configuration.maximumResponseDataSize— downloads that exceed this limit are automatically cancelled. The default limit is based on the device's physical memory. Set tonilto disable — #738 - Add
ImagePipeline.Configuration.maximumDecodedImageSize— images whose decoded bitmap would exceed this limit are automatically downscaled during decoding. The default limit is calculated dynamically based on the device's physical memory. Set tonilto disable - Add
DataCache.isSweepEnabled(trueby default). Set it tofalsein targets that share a cache with the main app (e.g. a Notification Service Extension) so that only the main app enforces size limits via LRU sweeps - Add
AssetType.icowith magic-byte detection for ICO (Windows icon) images - Add
ImageTask.Event.started - Mark all public enums as
@frozen(except error enums and empty namespaces)
Performance
- Rewrite
ImageProcessors.GaussianBlurto use Accelerate (vImageBoxConvolve) instead of Core Image, fixing gray border artifacts and improving performance ~5.8x — #308 - Optimize data downloading by pre-allocating the buffer using the expected content size from the HTTP response, reducing memory reallocations during image downloads (this only applies when progressive decoding is on) — #738
- Update
ImageCache.defaultCostLimitto 15% of physical memory with no hard cap (previously 20% capped at 512 MB). The cache uses a custom LRU policy that enforces limits precisely, so 15% is effectively more generous than the previous capped value on modern devices – #838 - The storage cost limit of
ResumableDataStorageis now dynamic and varies depending on the available RAM. - Add
consumingtoLazyImagebuilder methods (processors,priority,pipeline,onStart,onDisappear,onCompletion) andImageContainer.map(_:)
API Changes
- Rename
ImagePipelineDelegatetoImagePipeline.Delegate. A deprecatedImagePipelineDelegatetypealias is provided for backward compatibility - Refactor
ImageDecoders.Defaultto fully delegate incremental decoding to Image I/O viaCGImageSourceCreateIncremental - Remove
queueparameter from completion-basedloadImage/loadDatamethods — callbacks now always run on the main queue - Remove
ImageTask.Event.cancelledin favor of.finished(.failure(.cancelled))— cancellation is now uniformly represented as a failure result - Remove
ImageRequest.init(id:dataPublisher:)and internalTaskFetchWithPublisher. UseImageRequest.init(id:data:)(async closure) instead — it is now handled directly byTaskFetchOriginalData - Remove soft-deprecated per-event
ImagePipelineDelegatemethods (imageTaskDidStart,didUpdateProgress,didReceivePreview,imageTaskDidCancel,didCompleteWithResult). UseimageTask(_:didReceiveEvent:pipeline:)instead - Remove previously deprecated APIs:
DataCache.isCompressionEnabled,ImageProcessors.Resize.ContentModetypealias,AsyncImageTasktypealias,ImagePipeline.Configuration.callbackQueue,ImagePipeline.Configuration.dataCachingQueue,ImagePipeline.loadData(with: URL), andImagePipeline.data(for: URL) - Soft-deprecate the
userInfoparameter inImageRequestinitializers in favor of dedicated type-safe properties
Bug Fixes
- Fix progressive JPEGs with large EXIF headers not producing previews —
CGImageSourceCreateIncrementalfails to recognize these files until fully downloaded. The decoder now falls back to generating a thumbnail from a non-incremental source. The issue was raised by and the initial fix provided by @theop-luma in #835 - Fix thumbnail requests re-downloading original image data when it is already stored in the disk cache — #837