Nuke 14 plays animated images, records where every image task spent its time, and loads images into any view with ImageLoader. It also fixes more than 30 bugs and raises the minimum platforms by one major version.
Animated Images
LazyImage and LazyImageView play GIF, APNG, animated WebP, and HEIC and AVIF sequences with no setup. Nuke plays them itself because the pipeline has something to contribute in terms of performance, and most of it is about caching:
- The animation is parsed once, on the decoding queue, and cached with the image, so a cell that scrolls back is ready the moment it appears
- An animation that fits in memory is decoded once and replayed. One that doesn't plays out of a window of three frames. The shared frame pool takes a quarter of one memory budget, with
ImageCachetaking the rest, and holds the smallest animations whole first - Views showing the same animation share one decoder and one set of frames, at the same size or close to it, and start on the same frame
- Frames are decoded no larger than the view displays them, and a view nobody is watching holds two frames
WebKit, more than any other player, was the reference for how playback behaves. Frames are never skipped, so a stalled main thread stretches the animation instead of making it jump. A delay under 11 ms becomes 0.1 s, and a file with no loop count plays once. The clock is held to 30 Hz in Low Power Mode, for the same reasons WebKit does it. See the Animated Images article.
LazyImage(url: URL(string: "https://example.com/cat.gif"))The repository now includes a demo app: open Nuke.xcodeproj and run the NukeDemo scheme. Its Animated Images screen shows the frame buffer, decode times, and dropped frames over a live animation, and Animation Lab plays up to 36 animations at once against the pool's budget.
- Add
AnimatedImage,AnimatedImageView,AnimatedImagePlayer,AnimatedImageFramePool, andLazyImageState/animatedImagetoNukeUI, andImageContainer/animationtoNuke– #958 ImageDecoders/Defaultparses every animated image it recognizes once, and attaches the result along with the encodedImageContainer/data.ImagePipeline/Configuration/isAnimatedImageParsingEnabledturns it off – #958- A decoder registered with
ImageDecoderRegistrycan attach anAnimatedImageSourceand anAnimatedImageFrameDecodingto play a format Image I/O can't read – #958
Diagnostics
Set isDiagnosticsEnabled and every task finishes with an ImageTask/metrics record: where the image came from, how long each stage took and how long it waited for a queue, what the download cost, and whether another task shared the work. The record is Codable, and printing it gives a timeline of the load. While diagnostics are on, the pipeline also sends the tasks, jobs, and stages as os_signpost intervals. See the Performance Guide.
let pipeline = ImagePipeline {
$0.isDiagnosticsEnabled = true
}
let task = pipeline.imageTask(with: url)
let image = try await task.image
print(task.metrics!)A feed image, resized for the cell, on a host the app hadn't talked to yet:
ImageTask #2 "feed" · success · 140.9 ms · from network
url: https://cdn.example.com/photos/1024.jpg
processors: com.github.kean/nuke/resize?s=(900.0, 900.0),cm=.aspectFill,crop=false,upscale=false
priority: normal
image: 1350×900 · jpeg · 4.9 MB in memory
transfer: 317 KB
pipeline: 44A3F653
time: network 93.6 ms (66%) · process 42.5 ms (30%) · cache 0.5 ms · decode 0.3 ms · queue 0.1 ms · other 3.8 ms (3%)
pending <0.1 ms ▏ started at 17:24:14.677
j4 loadImage [resize] 139.5 ms ████████████████████ 99%
├─ memoryLookup <0.1 ms ▏ miss · key aa429747
├─ diskLookup 0.3 ms ▏ miss · key f6d8540b
├─ j5 loadImage 96.3 ms ██████████████ 68%
│ ├─ memoryLookup <0.1 ms ▏ miss · key 38c326ae
│ ├─ diskLookup 0.1 ms ▏ miss · key 19d1c77c
│ └─ j6 fetchOriginalImage 96.0 ms ██████████████ 68%
│ ├─ j7 fetchOriginalData 95.2 ms ██████████████ 68%
│ │ ├─ download 93.6 ms ██████████████ 66% network · 317 KB · HTTP 200 · first byte 50.3 ms · session #2
│ │ │ └─ networkLoad 93.1 ms ██████████████ 66% HTTP 200 · h2 · TLS 1.3 · 151.101.1.1 · sent 164 bytes · received 318 KB
│ │ │ ├─ blocked 1.4 ms ░ 1%
│ │ │ ├─ domainLookup 1.0 ms ▏ 1%
│ │ │ ├─ connect 10.0 ms █ 7%
│ │ │ ├─ secureConnection 21.0 ms ███ 15%
│ │ │ ├─ request <0.1 ms ▏
│ │ │ ├─ waiting 16.1 ms ░░ 11%
│ │ │ └─ response 43.3 ms ███████ 31%
│ │ └─ diskStore <0.1 ms ▏ 317 KB · key 19d1c77c
│ └─ decode 0.3 ms ▏ ImageDecoders.Default · jpeg 1440×960
├─ process 42.6 ms ██████ 30% jpeg 1350×900
└─ memoryStore 0.1 ms ▕ key aa429747
total 140.9 ms finished at 17:24:14.818
ImageLoader
ImageLoader in NukeUI loads images into UIImageView and NSImageView, or into any object that draws its state. It replaces NukeExtensions, which is now soft-deprecated.
ImageLoader.load(url, into: imageView)- Add
ImageLoadertoNukeUI: it loads images intoUIImageViewandNSImageView, or into any object that draws itsImageLoader/State, and replacesNukeExtensions, which is now soft-deprecated – #1039
Quality
The test suite more than doubled, and writing it uncovered many of the bugs fixed in this release. NukeUI went from 63% to 95% line coverage.
| Version | Source lines | Tests | Test lines | Coverage |
|---|---|---|---|---|
| Nuke 14 | 7,358 | 1,698 | 21,562 | 98.6% |
| Nuke 13.2 | 5,011 | 937 | 10,740 | 96.4% |
| NukeUI 14 | 3,006 | 540 | 7,600 | 94.7% |
| NukeUI 13.2 | 804 | 80 | 978 | 63.4% |
Measured on macOS. Source and test lines exclude blank lines and comments.
Requirements
- Minimum required platforms: iOS 16.0, tvOS 16.0, watchOS 9.0, macOS 13.0, visionOS 1.0 – #904
- Replace the hand-rolled
os_unfair_lock_tallocations withOSAllocatedUnfairLock– #908 - The internal
TaskSubscriptionis now~Copyable, so the compiler rejects unsubscribing twice – #961
API Changes
- Remove previously deprecated APIs:
ImagePipelineDelegatetypealias,ImageRequest.imageId,ImageRequest.UserInfoKey.imageIdKey,ImageRequest.UserInfoKey.scaleKey,ImageRequest.UserInfoKey.thumbnailKey,ImagePipeline.Configuration.maximumDecodedImageSize, andImageDecodingContext.maximumDecodedImageSize– #907 - Remove the soft-deprecated
userInfoparameter from theImageRequestinitializers; it's still available as a property – #909 ImageRequest.scaleandImageRequest.ThumbnailOptions.init(maxPixelSize:)now useCGFloat– #910- Remove Combine support:
ImagePipeline.imagePublisher(with:)andFetchImage.load(_:)that takes a publisher. Use the Async/Await APIs instead – #911 - Remove
ImageTask/Event/started, whichImageTask/eventsnever yielded, and addImagePipeline/Delegate/imageTaskDidStart(_:pipeline:)in its place – #914 ImageTasknow conforms toIdentifiable– #922DataCacheis nowSendableinstead of@unchecked Sendable.DataCache/flush()andDataCache/sweep()are nowasync, andDataCache/queueandflush(for:)are removed – #925- Add
AssetType/avifand detect theavifandavisbrands, whichImageContainer/typepreviously reported asnil– #926 DataLoader/Errornow conforms toSendable– #933ImageProcessors/CoreImageFilter/Erroris nowSendableinstead of@unchecked Sendable: its cases carry a filter name and descriptions instead of theCIFilter,CIImage, and[String: Any]payloads – #934ImageProcessors/CoreImageFilternow takes[String: any Sendable]filter parameters instead of[String: Any]– #934ImageLoadingOptionsand its nestedContentModes,TintColors, andTransitionnow conform toSendable, and theImageLoadingOptions/Transition/custom(_:)closure is now@MainActor @Sendable– #935ImageDecodingContext/init(request:data:isCompleted:urlResponse:cacheType:previewPolicy:)now takespreviewPolicy– #936TaskQueue/maxConcurrentOperationCountand the matching initializer parameter are renamed tomaxConcurrentTaskCount; the old names are deprecated – #942ImagePipeline/Delegate/willCache(data:image:for:pipeline:)is nowasyncand returns the data to store instead of taking a completion closure. Returnnilto prevent caching – #943NukeUI,NukeExtensions, andNukeVideonow re-exportNuke, so importing them is enough. TheNukeUI.ImageRequesttypealias is removed – #946ImagePipeline/Delegatenow declares the isolation of every method instead of documenting it:willLoadData,willCache,imageTaskDidStart, andimageTask(_:didReceiveEvent:pipeline:)run onImagePipelineActor, and the rest arenonisolated– #945NukeUInow surfaces the typedImagePipeline/Errorinstead ofany Error:FetchImage/result,LazyImageState/result,LazyImageState/error, and theonCompletionandonFailurecallbacks – #949FetchImage/progressandLazyImageState/progressnow returnImageTask/Progress, whichFetchImagepublishes itself, instead of the nestedFetchImage.Progressobservable object that needed its own@ObservedObject. The old type is deprecated – #953- Add
ImagePipeline/Error/isCancelled– #952 Nuke_ImageDisplayingis renamed toImageDisplaying, is no longer@objc, and takes anImageContainer:nuke_display(image:data:)becomesnuke_display(_:). A conformance declared in an extension can no longer be overridden by a subclass – #958ImagePipeline/Configuration/imageDecodingQueuenow runs 2 concurrent tasks instead of 1, matchingimageDecompressingQueue. It carries the pixel work of a thumbnail request, which decodes and downsamples in one step and skips decompression – #962- The default
ImagePipeline/Configuration-swift.struct/rateLimiterrate goes from 80 to 100 requests per second – #960 ImageTaskandImageDecoders/Videoare nowSendableinstead of@unchecked Sendable– #965- Add
TaskQueue/reservedTaskCount: the number of slots that the work with a priority lower than.normalcan't take – #981 LazyImage/priority(_:)withnilnow keeps the request's own priority instead of resetting it to.normal– #1034LazyImage/processors(_:)withnilnow keeps the request's own processors; pass[]to remove them.nilinprocessors(_:)andpriority(_:)also takes back an earlier call – #1035LazyImageView/imageTaskis now read-only – #1039
Performance
ImagePipeline/Configuration-swift.struct/dataLoadingQueueruns the requests below.normalpriority in at most 3 of its 6 slots to ensure there are always slots available regardless of the number of low-priority tasks, like prefetching – #981ImageCachelookups are 6% faster – #975- Loading an image with
ImagePipeline/Configuration-swift.struct/dataCacheset makes 10 fewer allocations per download – #971 ImagePipeline/Erroris now 8 bytes instead of 97, which shrinksImageTask/Eventfrom 99 bytes to 26 – #967LazyImagechecks whether its request changed up to 5× faster on every body update when it keeps the same request – #969- Subscribing to
ImageTask/eventsno longer spawns aTask, which makes it 1.2–1.9× faster – #980 - Operations that require comparing processors – cache lookups, coalsing, etc – are up to 2× faster – #972
LazyImagechecks whether a request with processors changed 1.7× faster – #978- Creating data cache keys is up to 3x faster – #979
- Decompression is up to 45% faster on iOS, tvOS, and visionOS:
ImagePipeline/Configuration-swift.struct/isUsingPrepareForDisplayis now enabled by default – #990
Bug Fixes
- Remove an unused
AVKitimport fromNuke, which linked AVKit, AVFoundation, and their dependencies into every app – #947 - Fix a data race on
ImagePipeline/Configuration/isSignpostLoggingEnabled– #901 - Fix progressive previews not being delivered on a resumed download – #903
- Fix
ImageTask/eventsproducing an empty stream when the task finishes before the subscription lands, as it does on a memory cache hit – #916 - Fix
ImageTask/priorityupdates being applied out of order, leaving the running task at a stale priority – #918 - Fix a data race on the
DataCacheconfiguration properties – #925 - Fix
DataCache.initreading and decoding its metadata file on the calling thread, typically the main thread during app launch – #925 - Fix
DataCacheupdating the entry access date on the calling thread on every disk cache hit – #927 - Fix a data race on
DataLoader/prefersIncrementalDelivery– #928 - Fix a data race on
ImagePrefetcher/didComplete– #929 - Fix
DataCachesweeping only once per launch instead of everyDataCache/sweepInterval– #930 - Fix
DataCache/sweep()not recording the sweep date, so the next scheduled sweep ran again withinDataCache/sweepInterval– #932 - Fix
DataCachewriting entries non-atomically, so a read that arrived while the same key was being overwritten could return a truncated file – #937 - Fix
VideoPlayerViewnot resuming a looping video on macOS when the view is added back to a window – #951 - Fix
ImagePrefetcher/didCompletenot being called when stopping prefetching cancels the last outstanding request – #968 - Fix
ImageTask/events,ImageTask/progress, andImageTask/previewsmissing previews and progress updates sent before the subscription lands – #980 - Fix
ImageRequest/Options/returnCacheDataDontLoadfailing requests with processors even when the original image is cached – #1007 - Fix
ImageRequest/Options/disableDiskCacheWritesbeing ignored when storing processed images in the data cache – #1008 - Fix an image stored in a full
ImageCachebeing evicted right away when every cached image had been read – #1011 - Fix
ImageCachekeeping the previous image when it is overwritten with an image over the entry cost limit – #1014 - Fix a resumed download that failed again discarding its resumable data, so the next attempt started from scratch – #1006
- Fix
ImageRequest/Options/skipDataLoadingQueuebeing ignored when another request for the same resource is already waiting in the queue – #1005 - Fix
LazyImagenot reloading when only the request'sthumbnailorscalechanges – #1015 - Fix
FetchImage/isLoadingremainingtrueaftercancel()– #1013 - Fix a data race on
DataLoader/delegate– #998 - Fix
DataCache/removeData(for:)deleting the whole cache directory, or its parent, when a customDataCache/FilenameGeneratorreturns""or".."– #997 - Fix
DataLoader/delegatenever receiving server trust, client certificate, NTLM, and Negotiate challenges in the session-levelurlSession(_:didReceive:completionHandler:), which skipped certificate pinning implemented there – #996 - Fix
LazyImagerestarting a running request when its priority changes or when the view reappears after.lowerPriority, discarding what it had downloaded – #1034 - Fix setting
FetchImage/priorityorLazyImageView/priorityback tonilleaving the running task at the overridden priority – #1034 - Fix
DataCacheskipping the scheduled sweeps when the recorded last sweep date is in the future, e.g. after the device clock was corrected – #1024 - Fix
LazyImagestarting a new request with thepipeline,onStart, andonCompletionfrom when the view last appeared, and ignoring a change of the pipeline alone – #1037 - Fix
FetchImage/isLoadingremainingtruewhen the task passed toonStartis cancelled directly – #1039