github kean/Nuke 14.0.0-beta.1
Nuke 14.0 (Beta 1)

pre-release2 hours ago

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 ImageCache taking 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, and LazyImageState/animatedImage to NukeUI, and ImageContainer/animation to Nuke – #958
  • ImageDecoders/Default parses every animated image it recognizes once, and attaches the result along with the encoded ImageContainer/data. ImagePipeline/Configuration/isAnimatedImageParsingEnabled turns it off – #958
  • A decoder registered with ImageDecoderRegistry can attach an AnimatedImageSource and an AnimatedImageFrameDecoding to 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 ImageLoader to NukeUI: it loads images into UIImageView and NSImageView, or into any object that draws its ImageLoader/State, and replaces NukeExtensions, 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_t allocations with OSAllocatedUnfairLock – #908
  • The internal TaskSubscription is now ~Copyable, so the compiler rejects unsubscribing twice – #961

API Changes

  • Remove previously deprecated APIs: ImagePipelineDelegate typealias, ImageRequest.imageId, ImageRequest.UserInfoKey.imageIdKey, ImageRequest.UserInfoKey.scaleKey, ImageRequest.UserInfoKey.thumbnailKey, ImagePipeline.Configuration.maximumDecodedImageSize, and ImageDecodingContext.maximumDecodedImageSize – #907
  • Remove the soft-deprecated userInfo parameter from the ImageRequest initializers; it's still available as a property – #909
  • ImageRequest.scale and ImageRequest.ThumbnailOptions.init(maxPixelSize:) now use CGFloat – #910
  • Remove Combine support: ImagePipeline.imagePublisher(with:) and FetchImage.load(_:) that takes a publisher. Use the Async/Await APIs instead – #911
  • Remove ImageTask/Event/started, which ImageTask/events never yielded, and add ImagePipeline/Delegate/imageTaskDidStart(_:pipeline:) in its place – #914
  • ImageTask now conforms to Identifiable – #922
  • DataCache is now Sendable instead of @unchecked Sendable. DataCache/flush() and DataCache/sweep() are now async, and DataCache/queue and flush(for:) are removed – #925
  • Add AssetType/avif and detect the avif and avis brands, which ImageContainer/type previously reported as nil – #926
  • DataLoader/Error now conforms to Sendable – #933
  • ImageProcessors/CoreImageFilter/Error is now Sendable instead of @unchecked Sendable: its cases carry a filter name and descriptions instead of the CIFilter, CIImage, and [String: Any] payloads – #934
  • ImageProcessors/CoreImageFilter now takes [String: any Sendable] filter parameters instead of [String: Any] – #934
  • ImageLoadingOptions and its nested ContentModes, TintColors, and Transition now conform to Sendable, and the ImageLoadingOptions/Transition/custom(_:) closure is now @MainActor @Sendable – #935
  • ImageDecodingContext/init(request:data:isCompleted:urlResponse:cacheType:previewPolicy:) now takes previewPolicy – #936
  • TaskQueue/maxConcurrentOperationCount and the matching initializer parameter are renamed to maxConcurrentTaskCount; the old names are deprecated – #942
  • ImagePipeline/Delegate/willCache(data:image:for:pipeline:) is now async and returns the data to store instead of taking a completion closure. Return nil to prevent caching – #943
  • NukeUI, NukeExtensions, and NukeVideo now re-export Nuke, so importing them is enough. The NukeUI.ImageRequest typealias is removed – #946
  • ImagePipeline/Delegate now declares the isolation of every method instead of documenting it: willLoadData, willCache, imageTaskDidStart, and imageTask(_:didReceiveEvent:pipeline:) run on ImagePipelineActor, and the rest are nonisolated – #945
  • NukeUI now surfaces the typed ImagePipeline/Error instead of any Error: FetchImage/result, LazyImageState/result, LazyImageState/error, and the onCompletion and onFailure callbacks – #949
  • FetchImage/progress and LazyImageState/progress now return ImageTask/Progress, which FetchImage publishes itself, instead of the nested FetchImage.Progress observable object that needed its own @ObservedObject. The old type is deprecated – #953
  • Add ImagePipeline/Error/isCancelled – #952
  • Nuke_ImageDisplaying is renamed to ImageDisplaying, is no longer @objc, and takes an ImageContainer: nuke_display(image:data:) becomes nuke_display(_:). A conformance declared in an extension can no longer be overridden by a subclass – #958
  • ImagePipeline/Configuration/imageDecodingQueue now runs 2 concurrent tasks instead of 1, matching imageDecompressingQueue. 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/rateLimiter rate goes from 80 to 100 requests per second – #960
  • ImageTask and ImageDecoders/Video are now Sendable instead of @unchecked Sendable – #965
  • Add TaskQueue/reservedTaskCount: the number of slots that the work with a priority lower than .normal can't take – #981
  • LazyImage/priority(_:) with nil now keeps the request's own priority instead of resetting it to .normal – #1034
  • LazyImage/processors(_:) with nil now keeps the request's own processors; pass [] to remove them. nil in processors(_:) and priority(_:) also takes back an earlier call – #1035
  • LazyImageView/imageTask is now read-only – #1039

Performance

  • ImagePipeline/Configuration-swift.struct/dataLoadingQueue runs the requests below .normal priority 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 – #981
  • ImageCache lookups are 6% faster – #975
  • Loading an image with ImagePipeline/Configuration-swift.struct/dataCache set makes 10 fewer allocations per download – #971
  • ImagePipeline/Error is now 8 bytes instead of 97, which shrinks ImageTask/Event from 99 bytes to 26 – #967
  • LazyImage checks whether its request changed up to 5× faster on every body update when it keeps the same request – #969
  • Subscribing to ImageTask/events no longer spawns a Task, which makes it 1.2–1.9× faster – #980
  • Operations that require comparing processors – cache lookups, coalsing, etc – are up to 2× faster – #972
  • LazyImage checks 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/isUsingPrepareForDisplay is now enabled by default – #990

Bug Fixes

  • Remove an unused AVKit import from Nuke, 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/events producing an empty stream when the task finishes before the subscription lands, as it does on a memory cache hit – #916
  • Fix ImageTask/priority updates being applied out of order, leaving the running task at a stale priority – #918
  • Fix a data race on the DataCache configuration properties – #925
  • Fix DataCache.init reading and decoding its metadata file on the calling thread, typically the main thread during app launch – #925
  • Fix DataCache updating 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 DataCache sweeping only once per launch instead of every DataCache/sweepInterval – #930
  • Fix DataCache/sweep() not recording the sweep date, so the next scheduled sweep ran again within DataCache/sweepInterval – #932
  • Fix DataCache writing entries non-atomically, so a read that arrived while the same key was being overwritten could return a truncated file – #937
  • Fix VideoPlayerView not resuming a looping video on macOS when the view is added back to a window – #951
  • Fix ImagePrefetcher/didComplete not being called when stopping prefetching cancels the last outstanding request – #968
  • Fix ImageTask/events, ImageTask/progress, and ImageTask/previews missing previews and progress updates sent before the subscription lands – #980
  • Fix ImageRequest/Options/returnCacheDataDontLoad failing requests with processors even when the original image is cached – #1007
  • Fix ImageRequest/Options/disableDiskCacheWrites being ignored when storing processed images in the data cache – #1008
  • Fix an image stored in a full ImageCache being evicted right away when every cached image had been read – #1011
  • Fix ImageCache keeping 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/skipDataLoadingQueue being ignored when another request for the same resource is already waiting in the queue – #1005
  • Fix LazyImage not reloading when only the request's thumbnail or scale changes – #1015
  • Fix FetchImage/isLoading remaining true after cancel() – #1013
  • Fix a data race on DataLoader/delegate – #998
  • Fix DataCache/removeData(for:) deleting the whole cache directory, or its parent, when a custom DataCache/FilenameGenerator returns "" or ".." – #997
  • Fix DataLoader/delegate never receiving server trust, client certificate, NTLM, and Negotiate challenges in the session-level urlSession(_:didReceive:completionHandler:), which skipped certificate pinning implemented there – #996
  • Fix LazyImage restarting a running request when its priority changes or when the view reappears after .lowerPriority, discarding what it had downloaded – #1034
  • Fix setting FetchImage/priority or LazyImageView/priority back to nil leaving the running task at the overridden priority – #1034
  • Fix DataCache skipping the scheduled sweeps when the recorded last sweep date is in the future, e.g. after the device clock was corrected – #1024
  • Fix LazyImage starting a new request with the pipeline, onStart, and onCompletion from when the view last appeared, and ignoring a change of the pipeline alone – #1037
  • Fix FetchImage/isLoading remaining true when the task passed to onStart is cancelled directly – #1039

Documentation

  • Add an article on using Nuke from Objective-C – #948
  • Fix the code samples in the SwiftUI, UIKit, Objective-C, and Performance articles, and compile them in CI – #955
  • Add an article on animated images and update the format documentation – #958

Don't miss a new Nuke release

NewReleases is sending notifications on new releases.