├── .scripts ├── validate.sh ├── lint.sh ├── install_swiftlint.sh ├── test.sh └── create-xcframeworks.sh ├── Tests ├── Resources │ ├── cat.gif │ ├── swift.png │ ├── video.mp4 │ ├── baseline.jpeg │ ├── baseline.webp │ ├── fixture.jpeg │ ├── fixture.png │ ├── image-p3.jpg │ ├── img_751.heic │ ├── grayscale.jpeg │ ├── fixture-tiny.jpeg │ ├── progressive.jpeg │ ├── Snapshots │ │ ├── s-circle.png │ │ ├── s-sepia.png │ │ ├── s-circle-border.png │ │ ├── s-rounded-corners.png │ │ ├── s-sepia-less-intense.png │ │ ├── s-crop-left-orientation.jpg │ │ ├── s-crop-left-orientation.png │ │ └── s-rounded-corners-border.png │ └── right-orientation.jpeg ├── NukeTests │ ├── DeprecationTests.swift │ ├── DataPublisherTests.swift │ ├── ImageProcessorsTests │ │ ├── AnonymousTests.swift │ │ ├── DecompressionTests.swift │ │ └── GaussianBlurTests.swift │ ├── ImagePipelineTests │ │ ├── ImagePipelineDecodingTests.swift │ │ ├── ImagePipelineFormatsTests.swift │ │ ├── ImagePipelineTaskDelegateTests.swift │ │ ├── ImagePipelineProcessorTests.swift │ │ └── ImagePipelineConfigurationTests.swift │ ├── ImageDecoderRegistryTests.swift │ ├── LinkedListTest.swift │ ├── ImageEncoderTests.swift │ └── RateLimiterTests.swift ├── Host │ ├── AppDelegate.swift │ ├── ViewController.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ └── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard ├── MockImageEncoder.swift ├── NukePerformanceTests │ ├── ImageRequestPerformanceTests.swift │ ├── ImageCachePerformanceTests.swift │ ├── DataCachePeformanceTests.swift │ ├── ImageViewPerformanceTests.swift │ ├── ImageProcessingPerformanceTests.swift │ └── ImagePipelinePerformanceTests.swift ├── Info.plist ├── MockDataCache.swift ├── MockImageCache.swift ├── MockImageDecoder.swift ├── NukeExtensionsTests │ └── NukeExtensionsTestsHelpers.swift ├── CombineExtensions.swift ├── MockProgressiveDataLoader.swift ├── MockDataLoader.swift ├── NukeExtensions.swift ├── MockImageProcessor.swift └── ImagePipelineObserver.swift ├── Documentation ├── Nuke.docc │ ├── Resources │ │ ├── bench-01.png │ │ └── bench-02.png │ ├── Extensions │ │ ├── ImageResponse-Extension.md │ │ ├── DataLoader-Extension.md │ │ ├── ImageTask-Extension.md │ │ ├── ImagePiplelineCache-Extension.md │ │ ├── ImagePipelineDelegate-Extension.md │ │ ├── ImageRequest-Extension.md │ │ └── ImagePipelineConfiguration-Extension.md │ ├── Performance │ │ └── Caching │ │ │ └── caching.md │ └── Customization │ │ ├── ImageFormats │ │ ├── image-formats.md │ │ └── image-encoding.md │ │ └── ImageProcessing │ │ └── image-processing.md ├── NukeUI.docc │ ├── Resources │ │ └── nukeui-preview.png │ ├── Extensions │ │ ├── Image-Extension.md │ │ ├── LazyImageView-Extensions.md │ │ ├── FetchImage-Extensions.md │ │ └── LazyImage-Extensions.md │ └── NukeUI.md ├── NukeExtensions.docc │ ├── Resources │ │ └── pjpeg_demo.mp4 │ └── NukeExtensions.md └── Migrations │ ├── Nuke 6 Migration Guide.md │ ├── Nuke 7 Migration Guide.md │ ├── Nuke 11 Migration Guide.md │ ├── Nuke 9 Migration Guide.md │ └── Nuke 5 Migration Guide.md ├── Nuke.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── NukeUI Unit Tests.xcscheme │ ├── Nuke Performance Tests.xcscheme │ ├── NukeExtensions Tests.xcscheme │ ├── Nuke Unit Tests.xcscheme │ ├── Nuke Thread Safety Tests.xcscheme │ ├── NukeVideo.xcscheme │ ├── NukeUI.xcscheme │ ├── NukeExtensions.xcscheme │ └── Nuke Tests Host.xcscheme ├── .github └── FUNDING.yml ├── Sources ├── Nuke │ ├── Encoding │ │ ├── ImageEncoders.swift │ │ ├── ImageEncoding.swift │ │ ├── ImageEncoders+Default.swift │ │ └── ImageEncoders+ImageIO.swift │ ├── Loading │ │ └── DataLoading.swift │ ├── Caching │ │ ├── DataCaching.swift │ │ └── ImageCaching.swift │ ├── Processing │ │ ├── ImageProcessors+Anonymous.swift │ │ ├── ImageProcessors+Circle.swift │ │ ├── ImageDecompression.swift │ │ ├── ImageProcessors+GaussianBlur.swift │ │ ├── ImageProcessors+RoundedCorners.swift │ │ ├── ImageProcessors+Composition.swift │ │ └── ImageProcessingOptions.swift │ ├── Internal │ │ ├── Atomic.swift │ │ ├── Log.swift │ │ ├── Extensions.swift │ │ ├── DataPublisher.swift │ │ ├── LinkedList.swift │ │ ├── Operation.swift │ │ ├── ImagePublisher.swift │ │ └── ImageRequestKeys.swift │ ├── Decoding │ │ ├── ImageDecoders+Empty.swift │ │ ├── ImageDecoderRegistry.swift │ │ ├── ImageDecoding.swift │ │ └── AssetType.swift │ ├── Tasks │ │ ├── TaskLoadData.swift │ │ ├── AsyncPipelineTask.swift │ │ ├── TaskFetchOriginalImage.swift │ │ └── TaskFetchWithPublisher.swift │ ├── ImageResponse.swift │ └── Pipeline │ │ └── ImagePipeline+Error.swift ├── NukeUI │ └── LazyImageState.swift └── NukeVideo │ ├── ImageDecoders+Video.swift │ └── AVDataAsset.swift ├── Package.swift ├── .swiftlint.yml ├── LICENSE └── .gitignore /.scripts/validate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./temp/swiftlint lint --strict 4 | -------------------------------------------------------------------------------- /Tests/Resources/cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/cat.gif -------------------------------------------------------------------------------- /Tests/Resources/swift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/swift.png -------------------------------------------------------------------------------- /Tests/Resources/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/video.mp4 -------------------------------------------------------------------------------- /Tests/Resources/baseline.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/baseline.jpeg -------------------------------------------------------------------------------- /Tests/Resources/baseline.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/baseline.webp -------------------------------------------------------------------------------- /Tests/Resources/fixture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/fixture.jpeg -------------------------------------------------------------------------------- /Tests/Resources/fixture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/fixture.png -------------------------------------------------------------------------------- /Tests/Resources/image-p3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/image-p3.jpg -------------------------------------------------------------------------------- /Tests/Resources/img_751.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/img_751.heic -------------------------------------------------------------------------------- /Tests/Resources/grayscale.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/grayscale.jpeg -------------------------------------------------------------------------------- /Tests/Resources/fixture-tiny.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/fixture-tiny.jpeg -------------------------------------------------------------------------------- /Tests/Resources/progressive.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/progressive.jpeg -------------------------------------------------------------------------------- /Tests/Resources/Snapshots/s-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/Snapshots/s-circle.png -------------------------------------------------------------------------------- /Tests/Resources/Snapshots/s-sepia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/Snapshots/s-sepia.png -------------------------------------------------------------------------------- /Tests/Resources/right-orientation.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/right-orientation.jpeg -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Resources/bench-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Documentation/Nuke.docc/Resources/bench-01.png -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Resources/bench-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Documentation/Nuke.docc/Resources/bench-02.png -------------------------------------------------------------------------------- /Tests/Resources/Snapshots/s-circle-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/Snapshots/s-circle-border.png -------------------------------------------------------------------------------- /Tests/Resources/Snapshots/s-rounded-corners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/Snapshots/s-rounded-corners.png -------------------------------------------------------------------------------- /.scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if which swiftlint >/dev/null; then 4 | swiftlint 5 | else 6 | echo "SwiftLint not installed" 7 | fi 8 | -------------------------------------------------------------------------------- /Tests/Resources/Snapshots/s-sepia-less-intense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/Snapshots/s-sepia-less-intense.png -------------------------------------------------------------------------------- /Documentation/NukeUI.docc/Resources/nukeui-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Documentation/NukeUI.docc/Resources/nukeui-preview.png -------------------------------------------------------------------------------- /Tests/Resources/Snapshots/s-crop-left-orientation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/Snapshots/s-crop-left-orientation.jpg -------------------------------------------------------------------------------- /Tests/Resources/Snapshots/s-crop-left-orientation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/Snapshots/s-crop-left-orientation.png -------------------------------------------------------------------------------- /Tests/Resources/Snapshots/s-rounded-corners-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Tests/Resources/Snapshots/s-rounded-corners-border.png -------------------------------------------------------------------------------- /Documentation/NukeExtensions.docc/Resources/pjpeg_demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/Nuke/HEAD/Documentation/NukeExtensions.docc/Resources/pjpeg_demo.mp4 -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Extensions/ImageResponse-Extension.md: -------------------------------------------------------------------------------- 1 | # ``Nuke/ImageResponse`` 2 | 3 | ## Topics 4 | 5 | ### Related Types 6 | 7 | - ``ImageContainer`` 8 | -------------------------------------------------------------------------------- /Tests/NukeTests/DeprecationTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Documentation/NukeExtensions.docc/NukeExtensions.md: -------------------------------------------------------------------------------- 1 | # ``NukeExtensions`` 2 | 3 | Nuke provides convenience extension for image views with multiple display options. 4 | 5 | ## Overview 6 | 7 | In this guide, you'll learn about these extensions and all of the available options. 8 | -------------------------------------------------------------------------------- /Tests/Host/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | 7 | @UIApplicationMain 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | 10 | var window: UIWindow? 11 | } 12 | -------------------------------------------------------------------------------- /.scripts/install_swiftlint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # -L to enable redirects 4 | echo "Installing SwiftLint by downloading a pre-compiled binary" 5 | curl -L 'https://github.com/realm/SwiftLint/releases/download/0.47.1/portable_swiftlint.zip' -o swiftlint.zip 6 | mkdir temp 7 | unzip swiftlint.zip -d temp 8 | rm -f swiftlint.zip 9 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Extensions/DataLoader-Extension.md: -------------------------------------------------------------------------------- 1 | # ``Nuke/DataLoader`` 2 | 3 | 4 | ## Topics 5 | 6 | ### Initializers 7 | 8 | - ``init(configuration:validate:)`` 9 | 10 | ### Loading Data 11 | 12 | - ``loadData(with:didReceiveData:completion:)`` 13 | 14 | ### Observing Events 15 | 16 | - ``delegate`` 17 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Documentation/NukeUI.docc/Extensions/Image-Extension.md: -------------------------------------------------------------------------------- 1 | # ``NukeUI/Image`` 2 | 3 | ## Topics 4 | 5 | ### Initializers 6 | 7 | - ``init(_:)`` 8 | - ``init(_:onCreated:)`` 9 | 10 | ### Configuration 11 | 12 | - ``resizingMode(_:)`` 13 | - ``videoRenderingEnabled(_:)`` 14 | - ``videoLoopingEnabled(_:)`` 15 | - ``animatedImageRenderingEnabled(_:)`` 16 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Extensions/ImageTask-Extension.md: -------------------------------------------------------------------------------- 1 | # ``Nuke/ImageTask`` 2 | 3 | ## Topics 4 | 5 | ### Controlling the Task State 6 | 7 | - ``cancel()`` 8 | - ``state-swift.property`` 9 | - ``State-swift.enum`` 10 | - ``priority`` 11 | - ``ImageRequest/Priority-swift.enum`` 12 | 13 | ### Task Progress 14 | 15 | - ``progress-swift.property`` 16 | - ``Progress-swift.struct`` 17 | 18 | ### General Task Information 19 | 20 | - ``request`` 21 | - ``taskId`` 22 | - ``description`` 23 | -------------------------------------------------------------------------------- /Tests/MockImageEncoder.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Nuke 7 | 8 | final class MockImageEncoder: ImageEncoding, @unchecked Sendable { 9 | let result: Data? 10 | var encodeCount = 0 11 | 12 | init(result: Data?) { 13 | self.result = result 14 | } 15 | 16 | func encode(_ image: PlatformImage) -> Data? { 17 | encodeCount += 1 18 | return result 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Host/ViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | 7 | class ViewController: UIViewController { 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | // Do any additional setup after loading the view, typically from a nib. 11 | } 12 | 13 | override func didReceiveMemoryWarning() { 14 | super.didReceiveMemoryWarning() 15 | // Dispose of any resources that can be recreated. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kean 4 | patreon: # 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Extensions/ImagePiplelineCache-Extension.md: -------------------------------------------------------------------------------- 1 | # ``Nuke/ImagePipeline/Cache-swift.struct`` 2 | 3 | ## Topics 4 | 5 | ### Accessing Cached Images 6 | 7 | - ``cachedImage(for:caches:)`` 8 | - ``storeCachedImage(_:for:caches:)`` 9 | - ``removeCachedImage(for:caches:)`` 10 | - ``containsCachedImage(for:caches:)`` 11 | 12 | ### Accessing Cached Data 13 | 14 | - ``cachedData(for:)`` 15 | - ``storeCachedData(_:for:)`` 16 | - ``removeCachedData(for:)`` 17 | - ``containsData(for:)`` 18 | 19 | ### Removing All 20 | 21 | - ``removeAll(caches:)`` 22 | 23 | ### Cache Keys 24 | 25 | - ``makeImageCacheKey(for:)`` 26 | - ``makeDataCacheKey(for:)`` 27 | -------------------------------------------------------------------------------- /Tests/NukePerformanceTests/ImageRequestPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | import Nuke 7 | 8 | class ImageRequestPerformanceTests: XCTestCase { 9 | func testStoringRequestInCollections() { 10 | let urls = (0..<200_000).map { _ in return URL(string: "http://test.com/\(rnd(200))")! } 11 | let requests = urls.map { ImageRequest(url: $0) } 12 | 13 | measure { 14 | var array = [ImageRequest]() 15 | for request in requests { 16 | array.append(request) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Extensions/ImagePipelineDelegate-Extension.md: -------------------------------------------------------------------------------- 1 | # ``Nuke/ImagePipelineDelegate`` 2 | 3 | ## Topics 4 | 5 | ### Data Loading 6 | 7 | - ``dataLoader(for:pipeline:)-7xolj`` 8 | 9 | ### Decoding and Encoding 10 | 11 | - ``imageDecoder(for:pipeline:)-2rbkl`` 12 | - ``imageEncoder(for:pipeline:)-6uxsr`` 13 | 14 | ### Caching 15 | 16 | - ``imageCache(for:pipeline:)-1i8cv`` 17 | - ``dataCache(for:pipeline:)-2lnae`` 18 | - ``cacheKey(for:pipeline:)-8k9a4`` 19 | - ``willCache(data:image:for:pipeline:completion:)-7eg0n`` 20 | 21 | ### Decompression 22 | 23 | - ``shouldDecompress(response:for:pipeline:)-3cw2f`` 24 | - ``decompress(response:request:pipeline:)-lbbz`` 25 | -------------------------------------------------------------------------------- /.scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eo pipefail 4 | 5 | scheme="Nuke" 6 | 7 | while getopts "s:d:" opt; do 8 | case $opt in 9 | s) scheme=${OPTARG};; 10 | d) destinations+=("$OPTARG");; 11 | #... 12 | esac 13 | done 14 | shift $((OPTIND -1)) 15 | 16 | echo "scheme = ${scheme}" 17 | echo "destinations = ${destinations[@]}" 18 | 19 | xcodebuild -version 20 | 21 | xcodebuild build-for-testing -scheme "$scheme" -destination "${destinations[0]}" 22 | 23 | for destination in "${destinations[@]}"; 24 | do 25 | echo "\nRunning tests for destination: $destination" 26 | xcodebuild test-without-building -scheme "$scheme" -destination "$destination" 27 | done 28 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Performance/Caching/caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | Learn about cache layers in Nuke and how to configure them. 4 | 5 | ## Overview 6 | 7 | Nuke has three cache layers that you can configure to precisely match your app needs. The pipeline uses these caches when you request an image. Your app has advanced control over how images are stored and retrieved and direct access to all cache layers. 8 | 9 | ## Topics 10 | 11 | ### Overview 12 | 13 | - 14 | - 15 | 16 | ### Memory Cache 17 | 18 | - ``ImageCaching`` 19 | - ``ImageCache`` 20 | - ``ImageCacheKey`` 21 | 22 | ### Disk Cache 23 | 24 | - ``DataCaching`` 25 | - ``DataCache`` 26 | 27 | ### Composite Cache 28 | 29 | - ``ImagePipeline/Cache-swift.struct`` 30 | -------------------------------------------------------------------------------- /Sources/Nuke/Encoding/ImageEncoders.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// A namespace with all available encoders. 8 | public enum ImageEncoders {} 9 | 10 | extension ImageEncoding where Self == ImageEncoders.Default { 11 | public static func `default`(compressionQuality: Float = 0.8) -> ImageEncoders.Default { 12 | ImageEncoders.Default(compressionQuality: compressionQuality) 13 | } 14 | } 15 | 16 | extension ImageEncoding where Self == ImageEncoders.ImageIO { 17 | public static func imageIO(type: AssetType, compressionRatio: Float = 0.8) -> ImageEncoders.ImageIO { 18 | ImageEncoders.ImageIO(type: type, compressionRatio: compressionRatio) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Documentation/NukeUI.docc/NukeUI.md: -------------------------------------------------------------------------------- 1 | # ``NukeUI`` 2 | 3 | Image loading for SwiftUI, UIKit, and AppKit views. 4 | 5 | ## Overview 6 | 7 | There are two main views provided by the framework: 8 | 9 | - ``LazyImage`` for SwiftUI 10 | - ``LazyImageView`` for UIKit and AppKit 11 | 12 | ``LazyImage`` is designed similar to the native [`AsyncImage`](https://developer.apple.com/documentation/SwiftUI/AsyncImage), but it uses [Nuke](https://github.com/kean/Nuke) for loading images. You can take advantage of all of its features, such as caching, prefetching, task coalescing, smart background decompression, request priorities, and more. 13 | 14 | ![nukeui demo](nukeui-preview) 15 | 16 | ## Topics 17 | 18 | ### Essentials 19 | 20 | - ``LazyImage`` 21 | - ``LazyImageView`` 22 | 23 | ### Helpers 24 | 25 | - ``LazyImageState`` 26 | - ``FetchImage`` 27 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Nuke", 6 | platforms: [ 7 | .iOS(.v13), 8 | .tvOS(.v13), 9 | .macOS(.v10_15), 10 | .watchOS(.v6), 11 | .visionOS(.v1), 12 | ], 13 | products: [ 14 | .library(name: "Nuke", targets: ["Nuke"]), 15 | .library(name: "NukeUI", targets: ["NukeUI"]), 16 | .library(name: "NukeVideo", targets: ["NukeVideo"]), 17 | .library(name: "NukeExtensions", targets: ["NukeExtensions"]) 18 | ], 19 | targets: [ 20 | .target(name: "Nuke"), 21 | .target(name: "NukeUI", dependencies: ["Nuke"]), 22 | .target(name: "NukeVideo", dependencies: ["Nuke"]), 23 | .target(name: "NukeExtensions", dependencies: ["Nuke"]) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Nuke/Loading/DataLoading.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// Fetches original image data. 8 | public protocol DataLoading: Sendable { 9 | /// - parameter didReceiveData: Can be called multiple times if streaming 10 | /// is supported. 11 | /// - parameter completion: Must be called once after all (or none in case 12 | /// of an error) `didReceiveData` closures have been called. 13 | func loadData(with request: URLRequest, 14 | didReceiveData: @escaping (Data, URLResponse) -> Void, 15 | completion: @escaping (Error?) -> Void) -> any Cancellable 16 | } 17 | 18 | /// A unit of work that can be cancelled. 19 | public protocol Cancellable: AnyObject, Sendable { 20 | func cancel() 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Nuke/Caching/DataCaching.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// Data cache. 8 | /// 9 | /// - important: The implementation must be thread safe. 10 | public protocol DataCaching: Sendable { 11 | /// Retrieves data from cache for the given key. 12 | func cachedData(for key: String) -> Data? 13 | 14 | /// Returns `true` if the cache contains data for the given key. 15 | func containsData(for key: String) -> Bool 16 | 17 | /// Stores data for the given key. 18 | /// - note: The implementation must return immediately and store data 19 | /// asynchronously. 20 | func storeData(_ data: Data, for key: String) 21 | 22 | /// Removes data for the given key. 23 | func removeData(for key: String) 24 | 25 | /// Removes all items. 26 | func removeAll() 27 | } 28 | -------------------------------------------------------------------------------- /Documentation/NukeUI.docc/Extensions/LazyImageView-Extensions.md: -------------------------------------------------------------------------------- 1 | # ``NukeUI/LazyImageView`` 2 | 3 | ## Topics 4 | 5 | ### Initializers 6 | 7 | - ``init(frame:)`` 8 | - ``init(coder:)`` 9 | 10 | ### Loading Images 11 | 12 | - ``url`` 13 | - ``request`` 14 | - ``cancel()`` 15 | - ``reset()`` 16 | 17 | ### Request Options 18 | 19 | - ``priority`` 20 | - ``processors`` 21 | - ``pipeline`` 22 | 23 | ### Displaying Images 24 | 25 | - ``placeholderImage`` 26 | - ``placeholderView`` 27 | - ``placeholderViewPosition`` 28 | - ``failureImage`` 29 | - ``failureView`` 30 | - ``failureViewPosition`` 31 | - ``isProgressiveImageRenderingEnabled`` 32 | - ``isResetEnabled`` 33 | - ``transition-swift.property`` 34 | 35 | ### Callbacks 36 | 37 | - ``onStart`` 38 | - ``onProgress`` 39 | - ``onPreview`` 40 | - ``onSuccess`` 41 | - ``onFailure`` 42 | - ``onCompletion`` 43 | 44 | ### Accessing Underlying Views 45 | 46 | - ``imageView`` 47 | -------------------------------------------------------------------------------- /Tests/MockDataCache.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Nuke 7 | 8 | final class MockDataCache: DataCaching, @unchecked Sendable { 9 | var store = [String: Data]() 10 | var readCount = 0 11 | var writeCount = 0 12 | 13 | func resetCounters() { 14 | readCount = 0 15 | writeCount = 0 16 | } 17 | 18 | func cachedData(for key: String) -> Data? { 19 | readCount += 1 20 | return store[key] 21 | } 22 | 23 | func containsData(for key: String) -> Bool { 24 | store[key] != nil 25 | } 26 | 27 | func storeData(_ data: Data, for key: String) { 28 | writeCount += 1 29 | store[key] = data 30 | } 31 | 32 | func removeData(for key: String) { 33 | store[key] = nil 34 | } 35 | 36 | func removeAll() { 37 | store.removeAll() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - closure_spacing 3 | - convenience_type 4 | - empty_count 5 | - empty_string 6 | - explicit_init 7 | - fatal_error_message 8 | - first_where 9 | - identical_operands 10 | - joined_default_parameter 11 | - modifier_order 12 | - operator_usage_whitespace 13 | - overridden_super_call 14 | - pattern_matching_keywords 15 | - prohibited_super_call 16 | - toggle_bool 17 | - unavailable_function 18 | - vertical_parameter_alignment_on_call 19 | 20 | disabled_rules: 21 | - line_length 22 | - identifier_name 23 | - type_name 24 | 25 | nesting: 26 | type_level: 27 | warning: 2 28 | 29 | included: 30 | - Sources/ 31 | 32 | file_length: 33 | warning: 1000 34 | error: 1500 35 | 36 | type_body_length: 37 | warning: 600 38 | error: 1000 39 | 40 | identifier_name: 41 | min_length: 42 | warning: 1 43 | 44 | reporter: "xcode" 45 | -------------------------------------------------------------------------------- /Documentation/Migrations/Nuke 6 Migration Guide.md: -------------------------------------------------------------------------------- 1 | # Nuke 6 Migration Guide 2 | 3 | This guide is provided in order to ease the transition of existing applications using Nuke 5.x to the latest APIs, as well as explain the design and structure of new and changed functionality. 4 | 5 | ## Requirements 6 | 7 | - iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0 8 | - Xcode 9 9 | - Swift 4 10 | 11 | ## Overview 12 | 13 | Nuke 6 has a relatively small number of changes in the public API, chances are most of them are not going to affect your projects. Most of the deprecated APIs are kept in the project to ease the transition, however, they are going to be removed fairly soon. 14 | 15 | There were a lot of implementation details leaking into the public API in Nuke 5 (e.g. `Deduplicator` class, scheduling infrastructure) which were all made private in Nuke 6. If you were using any of those APIs you can always ping me with your questions on [Twitter](https://twitter.com/a_grebenyuk). 16 | -------------------------------------------------------------------------------- /Tests/Host/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /Sources/Nuke/Processing/ImageProcessors+Anonymous.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | #if !os(macOS) 8 | import UIKit 9 | #else 10 | import AppKit 11 | #endif 12 | 13 | extension ImageProcessors { 14 | /// Processed an image using a specified closure. 15 | public struct Anonymous: ImageProcessing, CustomStringConvertible { 16 | public let identifier: String 17 | private let closure: @Sendable (PlatformImage) -> PlatformImage? 18 | 19 | public init(id: String, _ closure: @Sendable @escaping (PlatformImage) -> PlatformImage?) { 20 | self.identifier = id 21 | self.closure = closure 22 | } 23 | 24 | public func process(_ image: PlatformImage) -> PlatformImage? { 25 | closure(image) 26 | } 27 | 28 | public var description: String { 29 | "AnonymousProcessor(identifier: \(identifier)" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Customization/ImageFormats/image-formats.md: -------------------------------------------------------------------------------- 1 | # Image Formats 2 | 3 | Learn about image formats supported in Nuke and how to extend them. 4 | 5 | ## Overview 6 | 7 | Nuke has built-in support for basic image formats like `jpeg`, `png`, and `heif`. It also has the infrastructure for supporting a variety of custom image formats. 8 | 9 | Nuke can drive progressive decoding, animated image rendering, progressive animated image rendering, drawing vector images directly or converting them to bitmaps, parsing thumbnails included in the image containers, and more. 10 | 11 | ## Topics 12 | 13 | ### Supported Images 14 | 15 | - 16 | - ``PlatformImage`` 17 | - ``AssetType`` 18 | 19 | ### Decoding 20 | 21 | - 22 | - ``ImageDecoding`` 23 | - ``ImageDecoders`` 24 | - ``ImageDecodingError`` 25 | - ``ImageDecodingContext`` 26 | - ``ImageDecoderRegistry`` 27 | 28 | ### Encoding 29 | 30 | - 31 | - ``ImageEncoding`` 32 | - ``ImageEncoders`` 33 | - ``ImageEncodingContext`` 34 | -------------------------------------------------------------------------------- /Sources/Nuke/Internal/Atomic.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | final class Atomic: @unchecked Sendable { 8 | private var _value: T 9 | private let lock: os_unfair_lock_t 10 | 11 | init(value: T) { 12 | self._value = value 13 | self.lock = .allocate(capacity: 1) 14 | self.lock.initialize(to: os_unfair_lock()) 15 | } 16 | 17 | deinit { 18 | lock.deinitialize(count: 1) 19 | lock.deallocate() 20 | } 21 | 22 | var value: T { 23 | get { 24 | os_unfair_lock_lock(lock) 25 | defer { os_unfair_lock_unlock(lock) } 26 | return _value 27 | } 28 | set { 29 | os_unfair_lock_lock(lock) 30 | defer { os_unfair_lock_unlock(lock) } 31 | _value = newValue 32 | } 33 | } 34 | 35 | func withLock(_ closure: (inout T) -> U) -> U { 36 | os_unfair_lock_lock(lock) 37 | defer { os_unfair_lock_unlock(lock) } 38 | return closure(&_value) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Extensions/ImageRequest-Extension.md: -------------------------------------------------------------------------------- 1 | # ``Nuke/ImageRequest`` 2 | 3 | ## Image Processing 4 | 5 | Set ``ImageRequest/processors`` to apply one of the built-in processors that can be found in ``ImageProcessors`` namespace or a custom one. 6 | 7 | ```swift 8 | request.processors = [.resize(width: 320)] 9 | ``` 10 | 11 | > Tip: See for more information on image processing. 12 | 13 | ## Topics 14 | 15 | ### Initializers 16 | 17 | - ``init(url:processors:priority:options:userInfo:)`` 18 | - ``init(urlRequest:processors:priority:options:userInfo:)`` 19 | - ``init(id:data:processors:priority:options:userInfo:)`` 20 | - ``init(id:dataPublisher:processors:priority:options:userInfo:)`` 21 | - ``init(stringLiteral:)`` 22 | 23 | ### Options 24 | 25 | - ``processors`` 26 | - ``priority-swift.property`` 27 | - ``options-swift.property`` 28 | - ``userInfo`` 29 | 30 | ### Nested Types 31 | 32 | - ``Priority-swift.enum`` 33 | - ``Options-swift.struct`` 34 | - ``UserInfoKey`` 35 | - ``ThumbnailOptions`` 36 | 37 | ### Instance Properties 38 | 39 | - ``urlRequest`` 40 | - ``url`` 41 | - ``imageId`` 42 | - ``description`` 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2024 Alexander Grebenyuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Extensions/ImagePipelineConfiguration-Extension.md: -------------------------------------------------------------------------------- 1 | # ``Nuke/ImagePipeline/Configuration-swift.struct`` 2 | 3 | ## Topics 4 | 5 | ### Initializers 6 | 7 | - ``init(dataLoader:)`` 8 | 9 | ### Predefined Configurations 10 | 11 | To learn more about caching, see . 12 | 13 | - ``withDataCache`` 14 | - ``withDataCache(name:sizeLimit:)`` 15 | - ``withURLCache`` 16 | 17 | ### Dependencies 18 | 19 | - ``dataLoader`` 20 | - ``dataCache`` 21 | - ``imageCache`` 22 | - ``makeImageDecoder`` 23 | - ``makeImageEncoder`` 24 | 25 | ### Caching Options 26 | 27 | - ``dataCachePolicy`` 28 | - ``ImagePipeline/DataCachePolicy`` 29 | - ``isStoringPreviewsInMemoryCache`` 30 | 31 | ### Other Options 32 | 33 | - ``isDecompressionEnabled`` 34 | - ``isTaskCoalescingEnabled`` 35 | - ``isRateLimiterEnabled`` 36 | - ``isProgressiveDecodingEnabled`` 37 | - ``isResumableDataEnabled`` 38 | - ``callbackQueue`` 39 | 40 | ### Global Options 41 | 42 | - ``isSignpostLoggingEnabled`` 43 | 44 | ### Operation Queues 45 | 46 | - ``dataLoadingQueue`` 47 | - ``dataCachingQueue`` 48 | - ``imageProcessingQueue`` 49 | - ``imageDecompressingQueue`` 50 | - ``imageDecodingQueue`` 51 | - ``imageEncodingQueue`` 52 | -------------------------------------------------------------------------------- /Sources/Nuke/Caching/ImageCaching.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// In-memory image cache. 8 | /// 9 | /// The implementation must be thread safe. 10 | public protocol ImageCaching: AnyObject, Sendable { 11 | /// Access the image cached for the given request. 12 | subscript(key: ImageCacheKey) -> ImageContainer? { get set } 13 | 14 | /// Removes all caches items. 15 | func removeAll() 16 | } 17 | 18 | /// An opaque container that acts as a cache key. 19 | /// 20 | /// In general, you don't construct it directly, and use ``ImagePipeline`` or ``ImagePipeline/Cache-swift.struct`` APIs. 21 | public struct ImageCacheKey: Hashable, Sendable { 22 | let key: Inner 23 | 24 | // This is faster than using AnyHashable (and it shows in performance tests). 25 | enum Inner: Hashable, Sendable { 26 | case custom(String) 27 | case `default`(MemoryCacheKey) 28 | } 29 | 30 | public init(key: String) { 31 | self.key = .custom(key) 32 | } 33 | 34 | public init(request: ImageRequest) { 35 | self.key = .default(MemoryCacheKey(request)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/MockImageCache.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | @testable import Nuke 7 | 8 | class MockImageCache: ImageCaching, @unchecked Sendable { 9 | let queue = DispatchQueue(label: "com.github.Nuke.MockCache") 10 | var enabled = true 11 | var images = [AnyHashable: ImageContainer]() 12 | var readCount = 0 13 | var writeCount = 0 14 | 15 | init() {} 16 | 17 | func resetCounters() { 18 | readCount = 0 19 | writeCount = 0 20 | } 21 | 22 | subscript(key: ImageCacheKey) -> ImageContainer? { 23 | get { 24 | queue.sync { 25 | readCount += 1 26 | return enabled ? images[key] : nil 27 | } 28 | } 29 | set { 30 | queue.sync { 31 | writeCount += 1 32 | if let image = newValue { 33 | if enabled { images[key] = image } 34 | } else { 35 | images[key] = nil 36 | } 37 | } 38 | } 39 | } 40 | 41 | func removeAll() { 42 | images.removeAll() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Nuke/Encoding/ImageEncoding.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | #if canImport(UIKit) 6 | import UIKit 7 | #endif 8 | 9 | #if canImport(AppKit) 10 | import AppKit 11 | #endif 12 | 13 | import ImageIO 14 | 15 | // MARK: - ImageEncoding 16 | 17 | /// An image encoder. 18 | public protocol ImageEncoding: Sendable { 19 | /// Encodes the given image. 20 | func encode(_ image: PlatformImage) -> Data? 21 | 22 | /// An optional method which encodes the given image container. 23 | func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data? 24 | } 25 | 26 | extension ImageEncoding { 27 | public func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data? { 28 | if container.type == .gif { 29 | return container.data 30 | } 31 | return self.encode(container.image) 32 | } 33 | } 34 | 35 | /// Image encoding context used when selecting which encoder to use. 36 | public struct ImageEncodingContext: @unchecked Sendable { 37 | public let request: ImageRequest 38 | public let image: PlatformImage 39 | public let urlResponse: URLResponse? 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## System 2 | .DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | Nuke.xcodeproj/xcshareddata/xcbaselines/ 8 | .swiftpm/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | 21 | ## Other 22 | *.xccheckout 23 | *.moved-aside 24 | *.xcuserstate 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | 36 | ## Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | 41 | .build/ 42 | 43 | 44 | ## CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | 51 | Pods/ 52 | 53 | 54 | ## Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | 58 | Carthage 59 | -------------------------------------------------------------------------------- /Tests/NukeTests/DataPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | import Combine 7 | @testable import Nuke 8 | 9 | internal final class DataPublisherTests: XCTestCase { 10 | 11 | private var cancellable: (any Nuke.Cancellable)? 12 | 13 | func testInitNotStartsExecutionRightAway() { 14 | let operation = MockOperation() 15 | let publisher = DataPublisher(id: UUID().uuidString) { 16 | await operation.execute() 17 | } 18 | 19 | XCTAssertEqual(0, operation.executeCalls) 20 | 21 | let expOp = expectation(description: "Waits for MockOperation to complete execution") 22 | cancellable = publisher.sink { completion in expOp.fulfill() } receiveValue: { _ in } 23 | wait(for: [expOp], timeout: 0.2) 24 | 25 | XCTAssertEqual(1, operation.executeCalls) 26 | } 27 | 28 | private final class MockOperation: @unchecked Sendable { 29 | 30 | private(set) var executeCalls = 0 31 | 32 | func execute() async -> Data { 33 | executeCalls += 1 34 | await Task.yield() 35 | return Data() 36 | } 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Nuke/Processing/ImageProcessors+Circle.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | #if !os(macOS) 8 | import UIKit 9 | #else 10 | import AppKit 11 | #endif 12 | 13 | extension ImageProcessors { 14 | 15 | /// Rounds the corners of an image into a circle. If the image is not a square, 16 | /// crops it to a square first. 17 | public struct Circle: ImageProcessing, Hashable, CustomStringConvertible { 18 | private let border: ImageProcessingOptions.Border? 19 | 20 | /// - parameter border: `nil` by default. 21 | public init(border: ImageProcessingOptions.Border? = nil) { 22 | self.border = border 23 | } 24 | 25 | public func process(_ image: PlatformImage) -> PlatformImage? { 26 | image.processed.byDrawingInCircle(border: border) 27 | } 28 | 29 | public var identifier: String { 30 | let suffix = border.map { "?border=\($0)" } 31 | return "com.github.kean/nuke/circle" + (suffix ?? "") 32 | } 33 | 34 | public var description: String { 35 | "Circle(border: \(border?.description ?? "nil"))" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Nuke/Processing/ImageDecompression.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | enum ImageDecompression { 8 | static func isDecompressionNeeded(for response: ImageResponse) -> Bool { 9 | isDecompressionNeeded(for: response.image) ?? false 10 | } 11 | 12 | static func decompress(image: PlatformImage, isUsingPrepareForDisplay: Bool = false) -> PlatformImage { 13 | image.decompressed(isUsingPrepareForDisplay: isUsingPrepareForDisplay) ?? image 14 | } 15 | 16 | // MARK: Managing Decompression State 17 | 18 | #if swift(>=5.10) 19 | // Safe because it's never mutated. 20 | nonisolated(unsafe) static let isDecompressionNeededAK = malloc(1)! 21 | #else 22 | static let isDecompressionNeededAK = malloc(1)! 23 | #endif 24 | 25 | static func setDecompressionNeeded(_ isDecompressionNeeded: Bool, for image: PlatformImage) { 26 | objc_setAssociatedObject(image, isDecompressionNeededAK, isDecompressionNeeded, .OBJC_ASSOCIATION_RETAIN) 27 | } 28 | 29 | static func isDecompressionNeeded(for image: PlatformImage) -> Bool? { 30 | objc_getAssociatedObject(image, isDecompressionNeededAK) as? Bool 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Customization/ImageFormats/image-encoding.md: -------------------------------------------------------------------------------- 1 | # Image Encoding 2 | 3 | To encode images, use types conforming to the ``ImageEncoding`` protocol: 4 | 5 | ```swift 6 | public protocol ImageEncoding { 7 | func encode(image: UIImage) -> Data? 8 | } 9 | ``` 10 | 11 | There is currently no dedicated image encoder registry. Use the pipeline configuration to register custom decoders using ``ImagePipeline/Configuration-swift.struct/makeImageDecoder``. 12 | 13 | ## Built-In Image Encoders 14 | 15 | You can find all of the built-in encoders in the ``ImageEncoders`` namespace. 16 | 17 | ### ImageEncoders.Default 18 | 19 | ``ImageEncoders/Default`` encodes opaque images as `jpeg` and images with opacity as `png`. It can also be configured to use `heif` instead of `jpeg` using ``ImageEncoders/Default/isHEIFPreferred`` option. 20 | 21 | ### ImageEncoders.ImageIO 22 | 23 | ``ImageEncoders/ImageIO`` is an [Image I/O](https://developer.apple.com/documentation/imageio) based encoder. 24 | 25 | Image I/O is a system framework that allows applications to read and write most image file formats. This framework offers high efficiency, color management, and access to image metadata. 26 | 27 | ```swift 28 | let image: UIImage 29 | let encoder = ImageEncoders.ImageIO(type: .heif, compressionRatio: 0.8) 30 | let data = encoder.encode(image: image) 31 | ``` 32 | -------------------------------------------------------------------------------- /Sources/Nuke/Internal/Log.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import os 7 | 8 | func signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType, _ message: @autoclosure () -> String) { 9 | guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return } 10 | 11 | let log = log.value 12 | let signpostId = OSSignpostID(log: log, object: object) 13 | os_signpost(type, log: log, name: name, signpostID: signpostId, "%{public}s", message()) 14 | } 15 | 16 | func signpost(_ name: StaticString, _ work: () throws -> T) rethrows -> T { 17 | guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return try work() } 18 | 19 | let log = log.value 20 | let signpostId = OSSignpostID(log: log) 21 | os_signpost(.begin, log: log, name: name, signpostID: signpostId) 22 | let result = try work() 23 | os_signpost(.end, log: log, name: name, signpostID: signpostId) 24 | return result 25 | } 26 | 27 | private let log = Atomic(value: OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading")) 28 | 29 | enum Formatter { 30 | static func bytes(_ count: Int) -> String { 31 | bytes(Int64(count)) 32 | } 33 | 34 | static func bytes(_ count: Int64) -> String { 35 | ByteCountFormatter().string(fromByteCount: count) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Nuke/Decoding/ImageDecoders+Empty.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | extension ImageDecoders { 8 | /// A decoder that returns an empty placeholder image and attaches image 9 | /// data to the image container. 10 | public struct Empty: ImageDecoding, Sendable { 11 | public let isProgressive: Bool 12 | private let assetType: AssetType? 13 | 14 | public var isAsynchronous: Bool { false } 15 | 16 | /// Initializes the decoder. 17 | /// 18 | /// - Parameters: 19 | /// - type: Image type to be associated with an image container. 20 | /// `nil` by default. 21 | /// - isProgressive: If `false`, returns nil for every progressive 22 | /// scan. `false` by default. 23 | public init(assetType: AssetType? = nil, isProgressive: Bool = false) { 24 | self.assetType = assetType 25 | self.isProgressive = isProgressive 26 | } 27 | 28 | public func decode(_ data: Data) throws -> ImageContainer { 29 | ImageContainer(image: PlatformImage(), type: assetType, data: data, userInfo: [:]) 30 | } 31 | 32 | public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { 33 | isProgressive ? ImageContainer(image: PlatformImage(), type: assetType, data: data, userInfo: [:]) : nil 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/NukeUI/LazyImageState.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Nuke 7 | import SwiftUI 8 | import Combine 9 | 10 | /// Describes current image state. 11 | @MainActor 12 | public protocol LazyImageState { 13 | /// Returns the current fetch result. 14 | var result: Result? { get } 15 | 16 | /// Returns the fetched image. 17 | /// 18 | /// - note: In case pipeline has `isProgressiveDecodingEnabled` option enabled 19 | /// and the image being downloaded supports progressive decoding, the `image` 20 | /// might be updated multiple times during the download. 21 | var imageContainer: ImageContainer? { get } 22 | 23 | /// Returns `true` if the image is being loaded. 24 | var isLoading: Bool { get } 25 | 26 | /// The progress of the image download. 27 | var progress: FetchImage.Progress { get } 28 | } 29 | 30 | extension LazyImageState { 31 | /// Returns the current error. 32 | public var error: Error? { 33 | if case .failure(let error) = result { 34 | return error 35 | } 36 | return nil 37 | } 38 | 39 | /// Returns an image view. 40 | public var image: Image? { 41 | #if os(macOS) 42 | imageContainer.map { Image(nsImage: $0.image) } 43 | #else 44 | imageContainer.map { Image(uiImage: $0.image) } 45 | #endif 46 | } 47 | } 48 | 49 | extension FetchImage: LazyImageState {} 50 | -------------------------------------------------------------------------------- /Sources/Nuke/Tasks/TaskLoadData.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// Wrapper for tasks created by `loadData` calls. 8 | final class TaskLoadData: AsyncPipelineTask, @unchecked Sendable { 9 | override func start() { 10 | if let data = pipeline.cache.cachedData(for: request) { 11 | let container = ImageContainer(image: .init(), data: data) 12 | let response = ImageResponse(container: container, request: request) 13 | self.send(value: response, isCompleted: true) 14 | } else { 15 | self.loadData() 16 | } 17 | } 18 | 19 | private func loadData() { 20 | guard !request.options.contains(.returnCacheDataDontLoad) else { 21 | return send(error: .dataMissingInCache) 22 | } 23 | let request = request.withProcessors([]) 24 | dependency = pipeline.makeTaskFetchOriginalData(for: request).subscribe(self) { [weak self] in 25 | self?.didReceiveData($0.0, urlResponse: $0.1, isCompleted: $1) 26 | } 27 | } 28 | 29 | private func didReceiveData(_ data: Data, urlResponse: URLResponse?, isCompleted: Bool) { 30 | let container = ImageContainer(image: .init(), data: data) 31 | let response = ImageResponse(container: container, request: request, urlResponse: urlResponse) 32 | if isCompleted { 33 | send(value: response, isCompleted: isCompleted) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Nuke/Encoding/ImageEncoders+Default.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | #if !os(macOS) 8 | import UIKit 9 | #else 10 | import AppKit 11 | #endif 12 | 13 | extension ImageEncoders { 14 | /// A default adaptive encoder which uses best encoder available depending 15 | /// on the input image and its configuration. 16 | public struct Default: ImageEncoding { 17 | public var compressionQuality: Float 18 | 19 | /// Set to `true` to switch to HEIF when it is available on the current hardware. 20 | /// `false` by default. 21 | public var isHEIFPreferred = false 22 | 23 | public init(compressionQuality: Float = 0.8) { 24 | self.compressionQuality = compressionQuality 25 | } 26 | 27 | public func encode(_ image: PlatformImage) -> Data? { 28 | guard let cgImage = image.cgImage else { 29 | return nil 30 | } 31 | let type: AssetType 32 | if cgImage.isOpaque { 33 | if isHEIFPreferred && ImageEncoders.ImageIO.isSupported(type: .heic) { 34 | type = .heic 35 | } else { 36 | type = .jpeg 37 | } 38 | } else { 39 | type = .png 40 | } 41 | let encoder = ImageEncoders.ImageIO(type: type, compressionRatio: compressionQuality) 42 | return encoder.encode(image) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/MockImageDecoder.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Nuke 7 | 8 | class MockFailingDecoder: Nuke.ImageDecoding, @unchecked Sendable { 9 | func decode(_ data: Data) throws -> ImageContainer { 10 | throw MockError(description: "decoder-failed") 11 | } 12 | } 13 | 14 | class MockImageDecoder: ImageDecoding, @unchecked Sendable { 15 | private let decoder = ImageDecoders.Default() 16 | 17 | let name: String 18 | 19 | init(name: String) { 20 | self.name = name 21 | } 22 | 23 | func decode(_ data: Data) throws -> ImageContainer { 24 | try decoder.decode(data) 25 | } 26 | 27 | func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { 28 | decoder.decodePartiallyDownloadedData(data) 29 | } 30 | } 31 | 32 | class MockAnonymousImageDecoder: ImageDecoding, @unchecked Sendable { 33 | let closure: (Data, Bool) -> PlatformImage? 34 | 35 | init(_ closure: @escaping (Data, Bool) -> PlatformImage?) { 36 | self.closure = closure 37 | } 38 | 39 | convenience init(output: PlatformImage) { 40 | self.init { _, _ in output } 41 | } 42 | 43 | func decode(_ data: Data) throws -> ImageContainer { 44 | guard let image = closure(data, true) else { 45 | throw ImageDecodingError.unknown 46 | } 47 | return ImageContainer(image: image) 48 | } 49 | 50 | func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { 51 | closure(data, false).map { ImageContainer(image: $0) } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImageProcessorsTests/AnonymousTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | #if !os(macOS) 9 | import UIKit 10 | #endif 11 | 12 | class ImageProcessorsAnonymousTests: XCTestCase { 13 | 14 | func testAnonymousProcessorsHaveDifferentIdentifiers() { 15 | XCTAssertEqual( 16 | ImageProcessors.Anonymous(id: "1", { $0 }).identifier, 17 | ImageProcessors.Anonymous(id: "1", { $0 }).identifier 18 | ) 19 | XCTAssertNotEqual( 20 | ImageProcessors.Anonymous(id: "1", { $0 }).identifier, 21 | ImageProcessors.Anonymous(id: "2", { $0 }).identifier 22 | ) 23 | } 24 | 25 | func testAnonymousProcessorsHaveDifferentHashableIdentifiers() { 26 | XCTAssertEqual( 27 | ImageProcessors.Anonymous(id: "1", { $0 }).hashableIdentifier, 28 | ImageProcessors.Anonymous(id: "1", { $0 }).hashableIdentifier 29 | ) 30 | XCTAssertNotEqual( 31 | ImageProcessors.Anonymous(id: "1", { $0 }).hashableIdentifier, 32 | ImageProcessors.Anonymous(id: "2", { $0 }).hashableIdentifier 33 | ) 34 | } 35 | 36 | func testAnonymousProcessorIsApplied() throws { 37 | // Given 38 | let processor = ImageProcessors.Anonymous(id: "1") { 39 | $0.nk_test_processorIDs = ["1"] 40 | return $0 41 | } 42 | 43 | // When 44 | let image = try XCTUnwrap(processor.process(Test.image)) 45 | 46 | // Then 47 | XCTAssertEqual(image.nk_test_processorIDs, ["1"]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Nuke/Processing/ImageProcessors+GaussianBlur.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) 6 | 7 | import Foundation 8 | import CoreImage 9 | 10 | #if !os(macOS) 11 | import UIKit 12 | #else 13 | import AppKit 14 | #endif 15 | 16 | extension ImageProcessors { 17 | /// Blurs an image using `CIGaussianBlur` filter. 18 | public struct GaussianBlur: ImageProcessing, Hashable, CustomStringConvertible { 19 | private let radius: Int 20 | 21 | /// Initializes the receiver with a blur radius. 22 | /// 23 | /// - parameter radius: `8` by default. 24 | public init(radius: Int = 8) { 25 | self.radius = radius 26 | } 27 | 28 | /// Applies `CIGaussianBlur` filter to the image. 29 | public func process(_ image: PlatformImage) -> PlatformImage? { 30 | try? _process(image) 31 | } 32 | 33 | /// Applies `CIGaussianBlur` filter to the image. 34 | public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer { 35 | try container.map(_process(_:)) 36 | } 37 | 38 | private func _process(_ image: PlatformImage) throws -> PlatformImage { 39 | try CoreImageFilter.applyFilter(named: "CIGaussianBlur", parameters: ["inputRadius": radius], to: image) 40 | } 41 | 42 | public var identifier: String { 43 | "com.github.kean/nuke/gaussian_blur?radius=\(radius)" 44 | } 45 | 46 | public var description: String { 47 | "GaussianBlur(radius: \(radius))" 48 | } 49 | } 50 | } 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /Tests/Host/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Nuke/Internal/Extensions.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import CryptoKit 7 | 8 | extension String { 9 | /// Calculates SHA1 from the given string and returns its hex representation. 10 | /// 11 | /// ```swift 12 | /// print("http://test.com".sha1) 13 | /// // prints "50334ee0b51600df6397ce93ceed4728c37fee4e" 14 | /// ``` 15 | var sha1: String? { 16 | guard let input = self.data(using: .utf8) else { 17 | return nil // The conversion to .utf8 should never fail 18 | } 19 | let digest = Insecure.SHA1.hash(data: input) 20 | var output = "" 21 | for byte in digest { 22 | output.append(String(format: "%02x", byte)) 23 | } 24 | return output 25 | } 26 | } 27 | 28 | extension URL { 29 | var isLocalResource: Bool { 30 | scheme == "file" || scheme == "data" 31 | } 32 | } 33 | 34 | extension OperationQueue { 35 | convenience init(maxConcurrentCount: Int) { 36 | self.init() 37 | self.maxConcurrentOperationCount = maxConcurrentCount 38 | } 39 | } 40 | 41 | extension ImageRequest.Priority { 42 | var taskPriority: TaskPriority { 43 | switch self { 44 | case .veryLow: return .veryLow 45 | case .low: return .low 46 | case .normal: return .normal 47 | case .high: return .high 48 | case .veryHigh: return .veryHigh 49 | } 50 | } 51 | } 52 | 53 | final class AnonymousCancellable: Cancellable { 54 | let onCancel: @Sendable () -> Void 55 | 56 | init(_ onCancel: @Sendable @escaping () -> Void) { 57 | self.onCancel = onCancel 58 | } 59 | 60 | func cancel() { 61 | onCancel() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Documentation/Migrations/Nuke 7 Migration Guide.md: -------------------------------------------------------------------------------- 1 | # Nuke 7 Migration Guide 2 | 3 | This guide is provided in order to ease the transition of existing applications using Nuke 6.x to the latest APIs, as well as explain the design and structure of new and changed functionality. 4 | 5 | This migration guide is still work in progress, the finished version is going to be available when Nuke 7 is finally released. 6 | 7 | ## Requirements 8 | 9 | - iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0 10 | - Xcode 9.2 11 | - Swift 4.0 12 | 13 | ## Overview 14 | 15 | Nuke 7 is the biggest release yet. It contains more features and refinements that all of the previous releases combined. There are a lot of new APIs in Nuke 7, fortunately, it's almost completely source-compatible with Nuke 6. 16 | 17 | > Source-compatibility was removed in [Nuke 7.5](https://github.com/kean/Nuke/releases/tag/7.5). The latest source-compatible release is [Nuke 7.4.2](https://github.com/kean/Nuke/releases/tag/7.4.2). The best way to migrate would be to either upgrade to Nuke 7.4.2 first, or to drop this [Deprecated.swift](https://gist.github.com/kean/a14ca485ce72bef0e50cbb2f36ec7d91) into your project and follow the instructions from the warnings. 18 | 19 | Most of the new APIs have `Image*` prefix. Some of the types with `Image*` prefix are new (e.g. `ImagePipeline` which replaced `Manager` and `Loader`), some were just renamed (e.g. `ImageRequest` instead of `Request`), and some are reimagining of old APIs (e.g. `ImageDecoding` instead of `Decoding`). 20 | 21 | If you're using a deprecated API you're going to see a deprecation message with a suggestion which new API you should use instead. All of the deprecated APIs work exactly as they used to in the previous versions. The only exception is `DataLoading` protocol which was replaced with a new version, but most of the apps are not using it directly. 22 | -------------------------------------------------------------------------------- /Tests/Host/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Tests/NukeExtensionsTests/NukeExtensionsTestsHelpers.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | @testable import NukeExtensions 8 | 9 | #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) 10 | extension XCTestCase { 11 | @MainActor 12 | func expectToFinishLoadingImage(with request: ImageRequest, 13 | options: ImageLoadingOptions? = nil, 14 | into imageView: ImageDisplayingView, 15 | completion: ((_ result: Result) -> Void)? = nil) { 16 | let expectation = self.expectation(description: "Image loaded for \(request)") 17 | NukeExtensions.loadImage( 18 | with: request, 19 | options: options, 20 | into: imageView, 21 | completion: { result in 22 | XCTAssertTrue(Thread.isMainThread) 23 | completion?(result) 24 | expectation.fulfill() 25 | }) 26 | } 27 | 28 | @MainActor 29 | func expectToLoadImage(with request: ImageRequest, options: ImageLoadingOptions? = nil, into imageView: ImageDisplayingView) { 30 | expectToFinishLoadingImage(with: request, options: options, into: imageView) { result in 31 | XCTAssertTrue(result.isSuccess) 32 | } 33 | } 34 | } 35 | 36 | extension ImageLoadingOptions { 37 | @MainActor 38 | private static var stack = [ImageLoadingOptions]() 39 | 40 | @MainActor 41 | static func pushShared(_ shared: ImageLoadingOptions) { 42 | stack.append(ImageLoadingOptions.shared) 43 | ImageLoadingOptions.shared = shared 44 | } 45 | 46 | @MainActor 47 | static func popShared() { 48 | ImageLoadingOptions.shared = stack.removeLast() 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /Sources/Nuke/Processing/ImageProcessors+RoundedCorners.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import CoreGraphics 7 | 8 | #if !os(macOS) 9 | import UIKit 10 | #else 11 | import AppKit 12 | #endif 13 | 14 | extension ImageProcessors { 15 | /// Rounds the corners of an image to the specified radius. 16 | /// 17 | /// - important: In order for the corners to be displayed correctly, the image must exactly match the size 18 | /// of the image view in which it will be displayed. See ``ImageProcessors/Resize`` for more info. 19 | public struct RoundedCorners: ImageProcessing, Hashable, CustomStringConvertible { 20 | private let radius: CGFloat 21 | private let border: ImageProcessingOptions.Border? 22 | 23 | /// Initializes the processor with the given radius. 24 | /// 25 | /// - parameters: 26 | /// - radius: The radius of the corners. 27 | /// - unit: Unit of the radius. 28 | /// - border: An optional border drawn around the image. 29 | public init(radius: CGFloat, unit: ImageProcessingOptions.Unit = .points, border: ImageProcessingOptions.Border? = nil) { 30 | self.radius = radius.converted(to: unit) 31 | self.border = border 32 | } 33 | 34 | public func process(_ image: PlatformImage) -> PlatformImage? { 35 | image.processed.byAddingRoundedCorners(radius: radius, border: border) 36 | } 37 | 38 | public var identifier: String { 39 | let suffix = border.map { ",border=\($0)" } 40 | return "com.github.kean/nuke/rounded_corners?radius=\(radius)" + (suffix ?? "") 41 | } 42 | 43 | public var description: String { 44 | "RoundedCorners(radius: \(radius) pixels, border: \(border?.description ?? "nil"))" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/CombineExtensions.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Nuke 6 | import Combine 7 | 8 | extension Publishers { 9 | struct Anonymous: Publisher { 10 | private var closure: (AnySubscriber) -> Void 11 | 12 | init(closure: @escaping (AnySubscriber) -> Void) { 13 | self.closure = closure 14 | } 15 | 16 | func receive(subscriber: S) where S: Subscriber, Anonymous.Failure == S.Failure, Anonymous.Output == S.Input { 17 | let subscription = Subscriptions.Anonymous(subscriber: subscriber) 18 | subscriber.receive(subscription: subscription) 19 | subscription.start(closure) 20 | } 21 | } 22 | } 23 | 24 | extension Subscriptions { 25 | final class Anonymous: Subscription where SubscriberType.Input == Output, Failure == SubscriberType.Failure { 26 | 27 | private var subscriber: SubscriberType? 28 | 29 | init(subscriber: SubscriberType) { 30 | self.subscriber = subscriber 31 | } 32 | 33 | func start(_ closure: @escaping (AnySubscriber) -> Void) { 34 | if let subscriber { 35 | closure(AnySubscriber(subscriber)) 36 | } 37 | } 38 | 39 | func request(_ demand: Subscribers.Demand) { 40 | // Ignore demand for now 41 | } 42 | 43 | func cancel() { 44 | self.subscriber = nil 45 | } 46 | 47 | } 48 | } 49 | 50 | extension AnyPublisher { 51 | static func create(_ closure: @escaping (AnySubscriber) -> Void) -> AnyPublisher { 52 | return Publishers.Anonymous(closure: closure) 53 | .eraseToAnyPublisher() 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImageProcessorsTests/DecompressionTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | class ImageDecompressionTests: XCTestCase { 9 | 10 | func testDecompressionNotNeededFlagSet() throws { 11 | // Given 12 | let input = Test.image 13 | ImageDecompression.setDecompressionNeeded(true, for: input) 14 | 15 | // When 16 | let output = ImageDecompression.decompress(image: input) 17 | 18 | // Then 19 | XCTAssertFalse(ImageDecompression.isDecompressionNeeded(for: output) ?? false) 20 | } 21 | 22 | func testGrayscalePreserved() throws { 23 | // Given 24 | let input = Test.image(named: "grayscale", extension: "jpeg") 25 | XCTAssertEqual(input.cgImage?.bitsPerComponent, 8) 26 | XCTAssertEqual(input.cgImage?.bitsPerPixel, 8) 27 | 28 | // When 29 | let output = ImageDecompression.decompress(image: input, isUsingPrepareForDisplay: true) 30 | 31 | // Then 32 | // The original image doesn't have an alpha channel (kCGImageAlphaNone), 33 | // but this parameter combination (8 bbc and kCGImageAlphaNone) is not 34 | // supported by CGContext. Thus we are switching to a different format. 35 | #if os(iOS) || os(tvOS) || os(visionOS) 36 | if #available(iOS 15.0, tvOS 15.0, *) { 37 | XCTAssertEqual(output.cgImage?.bitsPerPixel, 8) // Yay, preparingForDisplay supports it 38 | XCTAssertEqual(output.cgImage?.bitsPerComponent, 8) 39 | } else { 40 | XCTAssertEqual(output.cgImage?.bitsPerPixel, 8) 41 | XCTAssertEqual(output.cgImage?.bitsPerComponent, 8) 42 | } 43 | #else 44 | XCTAssertEqual(output.cgImage?.bitsPerPixel, 8) 45 | XCTAssertEqual(output.cgImage?.bitsPerComponent, 8) 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImagePipelineTests/ImagePipelineDecodingTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | class ImagePipelineDecodingTests: XCTestCase { 9 | var dataLoader: MockDataLoader! 10 | var pipeline: ImagePipeline! 11 | 12 | override func setUp() { 13 | super.setUp() 14 | 15 | dataLoader = MockDataLoader() 16 | pipeline = ImagePipeline { 17 | $0.dataLoader = dataLoader 18 | $0.imageCache = nil 19 | } 20 | } 21 | 22 | func testExperimentalDecoder() throws { 23 | // Given 24 | let decoder = MockExperimentalDecoder() 25 | 26 | let dummyImage = PlatformImage() 27 | let dummyData = "123".data(using: .utf8) 28 | decoder._decode = { data in 29 | return ImageContainer(image: dummyImage, data: dummyData, userInfo: ["a": 1]) 30 | } 31 | 32 | pipeline = pipeline.reconfigured { 33 | $0.makeImageDecoder = { _ in decoder } 34 | } 35 | 36 | // When 37 | var response: ImageResponse? 38 | expect(pipeline).toLoadImage(with: Test.request, completion: { 39 | response = $0.value 40 | }) 41 | wait() 42 | 43 | // Then 44 | let container = try XCTUnwrap(response?.container) 45 | XCTAssertNotNil(container.image) 46 | XCTAssertEqual(container.data, dummyData) 47 | XCTAssertEqual(container.userInfo["a"] as? Int, 1) 48 | } 49 | } 50 | 51 | private final class MockExperimentalDecoder: ImageDecoding, @unchecked Sendable { 52 | var _decode: ((Data) -> ImageContainer?)! 53 | 54 | func decode(_ data: Data) throws -> ImageContainer { 55 | guard let image = _decode(data) else { 56 | throw ImageDecodingError.unknown 57 | } 58 | return image 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Nuke/ImageResponse.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | #if canImport(UIKit) 8 | import UIKit 9 | #endif 10 | 11 | #if canImport(AppKit) 12 | import AppKit 13 | #endif 14 | 15 | /// An image response that contains a fetched image and some metadata. 16 | public struct ImageResponse: @unchecked Sendable { 17 | /// An image container with an image and associated metadata. 18 | public var container: ImageContainer 19 | 20 | #if os(macOS) 21 | /// A convenience computed property that returns an image from the container. 22 | public var image: NSImage { container.image } 23 | #else 24 | /// A convenience computed property that returns an image from the container. 25 | public var image: UIImage { container.image } 26 | #endif 27 | 28 | /// Returns `true` if the image in the container is a preview of the image. 29 | public var isPreview: Bool { container.isPreview } 30 | 31 | /// The request for which the response was created. 32 | public var request: ImageRequest 33 | 34 | /// A response. `nil` unless the resource was fetched from the network or an 35 | /// HTTP cache. 36 | public var urlResponse: URLResponse? 37 | 38 | /// Contains a cache type in case the image was returned from one of the 39 | /// pipeline caches (not including any of the HTTP caches if enabled). 40 | public var cacheType: CacheType? 41 | 42 | /// Initializes the response with the given image. 43 | public init(container: ImageContainer, request: ImageRequest, urlResponse: URLResponse? = nil, cacheType: CacheType? = nil) { 44 | self.container = container 45 | self.request = request 46 | self.urlResponse = urlResponse 47 | self.cacheType = cacheType 48 | } 49 | 50 | /// A cache type. 51 | public enum CacheType: Sendable { 52 | /// Memory cache (see ``ImageCaching``) 53 | case memory 54 | /// Disk cache (see ``DataCaching``) 55 | case disk 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/NukeUI Unit Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/Nuke Performance Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/NukeExtensions Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Documentation/NukeUI.docc/Extensions/FetchImage-Extensions.md: -------------------------------------------------------------------------------- 1 | # ``NukeUI/FetchImage`` 2 | 3 | ## Overview 4 | 5 | ``FetchImage`` is an observable object ([`ObservableObject`](https://developer.apple.com/documentation/combine/observableobject)) that allows you to manage the download of an image and observe the download status. It acts as a ViewModel that manages the image download state making it easy to add image loading to your custom SwiftUI views. 6 | 7 | ## Creating Custom Views 8 | 9 | ```swift 10 | struct ImageView: View { 11 | let url: URL 12 | 13 | @StateObject private var image = FetchImage() 14 | 15 | var body: some View { 16 | ZStack { 17 | Rectangle().fill(Color.gray) 18 | image.image? 19 | .resizable() 20 | .aspectRatio(contentMode: .fill) 21 | .clipped() 22 | } 23 | .onAppear { image.load(url) } 24 | .onChange(of: url) { image.load($0) } 25 | .onDisappear { image.reset() } 26 | } 27 | } 28 | ``` 29 | 30 | ``FetchImage`` gives you full control over how to manage the download and how to display the image. For example, if you want the download to continue when the view leaves the screen, change the appearance callbacks accordingly. 31 | 32 | ```swift 33 | struct ImageView: View { 34 | let url: URL 35 | 36 | @StateObject private var image = FetchImage() 37 | 38 | var body: some View { 39 | // ... 40 | .onAppear { 41 | image.priority = .normal 42 | image.load(url) 43 | } 44 | .onDisappear { 45 | image.priority = .low 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ## Topics 52 | 53 | ### Initializers 54 | 55 | - ``init()`` 56 | 57 | ### Loading Images 58 | 59 | - ``load(_:)-9my9q`` 60 | - ``load(_:)-53ybw`` 61 | - ``load(_:)-6pey2`` 62 | - ``cancel()`` 63 | - ``reset()`` 64 | 65 | ### State 66 | 67 | - ``result`` 68 | - ``imageContainer`` 69 | - ``isLoading`` 70 | - ``progress-swift.property`` 71 | 72 | ### Options 73 | 74 | - ``priority`` 75 | - ``processors`` 76 | - ``pipeline`` 77 | - ``transaction`` 78 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/Nuke Unit Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 39 | 40 | 46 | 47 | 49 | 50 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Sources/Nuke/Internal/DataPublisher.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | @preconcurrency import Combine 7 | 8 | final class DataPublisher { 9 | let id: String 10 | private let _sink: (@escaping ((PublisherCompletion) -> Void), @escaping ((Data) -> Void)) -> any Cancellable 11 | 12 | init(id: String, _ publisher: P) where P.Output == Data { 13 | self.id = id 14 | self._sink = { onCompletion, onValue in 15 | let cancellable = publisher.sink(receiveCompletion: { 16 | switch $0 { 17 | case .finished: onCompletion(.finished) 18 | case .failure(let error): onCompletion(.failure(error)) 19 | } 20 | }, receiveValue: { 21 | onValue($0) 22 | }) 23 | return AnonymousCancellable { cancellable.cancel() } 24 | } 25 | } 26 | 27 | convenience init(id: String, _ data: @Sendable @escaping () async throws -> Data) { 28 | self.init(id: id, publisher(from: data)) 29 | } 30 | 31 | func sink(receiveCompletion: @escaping ((PublisherCompletion) -> Void), receiveValue: @escaping ((Data) -> Void)) -> any Cancellable { 32 | _sink(receiveCompletion, receiveValue) 33 | } 34 | } 35 | 36 | private func publisher(from closure: @Sendable @escaping () async throws -> Data) -> AnyPublisher { 37 | Deferred { 38 | Future { promise in 39 | let promise = UncheckedSendableBox(value: promise) 40 | Task { 41 | do { 42 | let data = try await closure() 43 | promise.value(.success(data)) 44 | } catch { 45 | promise.value(.failure(error)) 46 | } 47 | } 48 | } 49 | }.eraseToAnyPublisher() 50 | } 51 | 52 | enum PublisherCompletion { 53 | case finished 54 | case failure(Error) 55 | } 56 | 57 | /// - warning: Avoid using it! 58 | struct UncheckedSendableBox: @unchecked Sendable { 59 | let value: Value 60 | } 61 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/Nuke Thread Safety Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 39 | 40 | 46 | 47 | 49 | 50 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Sources/Nuke/Internal/LinkedList.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// A doubly linked list. 8 | final class LinkedList { 9 | // first <-> node <-> ... <-> last 10 | private(set) var first: Node? 11 | private(set) var last: Node? 12 | 13 | deinit { 14 | // This way we make sure that the deallocations do no happen recursively 15 | // (and potentially overflow the stack). 16 | removeAllElements() 17 | } 18 | 19 | var isEmpty: Bool { 20 | last == nil 21 | } 22 | 23 | /// Adds an element to the end of the list. 24 | @discardableResult 25 | func append(_ element: Element) -> Node { 26 | let node = Node(value: element) 27 | append(node) 28 | return node 29 | } 30 | 31 | /// Adds a node to the end of the list. 32 | func append(_ node: Node) { 33 | if let last { 34 | last.next = node 35 | node.previous = last 36 | self.last = node 37 | } else { 38 | last = node 39 | first = node 40 | } 41 | } 42 | 43 | func remove(_ node: Node) { 44 | node.next?.previous = node.previous // node.previous is nil if node=first 45 | node.previous?.next = node.next // node.next is nil if node=last 46 | if node === last { 47 | last = node.previous 48 | } 49 | if node === first { 50 | first = node.next 51 | } 52 | node.next = nil 53 | node.previous = nil 54 | } 55 | 56 | func removeAllElements() { 57 | // avoid recursive Nodes deallocation 58 | var node = first 59 | while let next = node?.next { 60 | node?.next = nil 61 | next.previous = nil 62 | node = next 63 | } 64 | last = nil 65 | first = nil 66 | } 67 | 68 | final class Node { 69 | let value: Element 70 | fileprivate var next: Node? 71 | fileprivate var previous: Node? 72 | 73 | init(value: Element) { 74 | self.value = value 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImageDecoderRegistryTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | final class ImageDecoderRegistryTests: XCTestCase { 9 | func testDefaultDecoderIsReturned() { 10 | // Given 11 | let context = ImageDecodingContext.mock 12 | 13 | // Then 14 | let decoder = ImageDecoderRegistry().decoder(for: context) 15 | XCTAssertTrue(decoder is ImageDecoders.Default) 16 | } 17 | 18 | func testRegisterDecoder() { 19 | // Given 20 | let registry = ImageDecoderRegistry() 21 | let context = ImageDecodingContext.mock 22 | 23 | // When 24 | registry.register { _ in 25 | return MockImageDecoder(name: "A") 26 | } 27 | 28 | // Then 29 | let decoder1 = registry.decoder(for: context) as? MockImageDecoder 30 | XCTAssertEqual(decoder1?.name, "A") 31 | 32 | // When 33 | registry.register { _ in 34 | return MockImageDecoder(name: "B") 35 | } 36 | 37 | // Then 38 | let decoder2 = registry.decoder(for: context) as? MockImageDecoder 39 | XCTAssertEqual(decoder2?.name, "B") 40 | } 41 | 42 | func testClearDecoders() { 43 | // Given 44 | let registry = ImageDecoderRegistry() 45 | let context = ImageDecodingContext.mock 46 | 47 | registry.register { _ in 48 | return MockImageDecoder(name: "A") 49 | } 50 | 51 | // When 52 | registry.clear() 53 | 54 | // Then 55 | let noDecoder = registry.decoder(for: context) 56 | XCTAssertNil(noDecoder) 57 | } 58 | 59 | func testWhenReturningNextDecoderIsEvaluated() { 60 | // Given 61 | let registry = ImageDecoderRegistry() 62 | registry.register { _ in 63 | return nil 64 | } 65 | 66 | // When 67 | let context = ImageDecodingContext.mock 68 | let decoder = ImageDecoderRegistry().decoder(for: context) 69 | 70 | // Then 71 | XCTAssertTrue(decoder is ImageDecoders.Default) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImagePipelineTests/ImagePipelineFormatsTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | class ImagePipelineFormatsTests: XCTestCase { 9 | var dataLoader: MockDataLoader! 10 | var pipeline: ImagePipeline! 11 | 12 | override func setUp() { 13 | super.setUp() 14 | 15 | dataLoader = MockDataLoader() 16 | pipeline = ImagePipeline { 17 | $0.dataLoader = dataLoader 18 | $0.imageCache = nil 19 | } 20 | } 21 | 22 | func testExtendedColorSpaceSupport() throws { 23 | // Given 24 | dataLoader.results[Test.url] = .success( 25 | (Test.data(name: "image-p3", extension: "jpg"), URLResponse(url: Test.url, mimeType: "jpeg", expectedContentLength: 20, textEncodingName: nil)) 26 | ) 27 | 28 | // When 29 | var result: Result? 30 | expect(pipeline).toLoadImage(with: Test.request) { 31 | result = $0 32 | } 33 | wait() 34 | 35 | // Then 36 | let image = try XCTUnwrap(result?.value?.image) 37 | let cgImage = try XCTUnwrap(image.cgImage) 38 | let colorSpace = try XCTUnwrap(cgImage.colorSpace) 39 | #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) 40 | XCTAssertTrue(colorSpace.isWideGamutRGB) 41 | #elseif os(watchOS) 42 | XCTAssertFalse(colorSpace.isWideGamutRGB) 43 | #endif 44 | } 45 | 46 | func testGrayscaleSupport() throws { 47 | // Given 48 | dataLoader.results[Test.url] = .success( 49 | (Test.data(name: "grayscale", extension: "jpeg"), URLResponse(url: Test.url, mimeType: "jpeg", expectedContentLength: 20, textEncodingName: nil)) 50 | ) 51 | 52 | // When 53 | var result: Result? 54 | expect(pipeline).toLoadImage(with: Test.request) { 55 | result = $0 56 | } 57 | wait() 58 | 59 | // Then 60 | let image = try XCTUnwrap(result?.value?.image) 61 | let cgImage = try XCTUnwrap(image.cgImage) 62 | XCTAssertEqual(cgImage.bitsPerComponent, 8) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/MockProgressiveDataLoader.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Nuke 7 | 8 | // One-shot data loader that servers data split into chunks, only send one chunk 9 | // per one `resume()` call. 10 | final class MockProgressiveDataLoader: DataLoading, @unchecked Sendable { 11 | let urlResponse: HTTPURLResponse 12 | var chunks: [Data] 13 | let data = Test.data(name: "progressive", extension: "jpeg") 14 | 15 | class _MockTask: Cancellable, @unchecked Sendable { 16 | func cancel() { 17 | // Do nothing 18 | } 19 | } 20 | 21 | private var didReceiveData: (Data, URLResponse) -> Void = { _, _ in } 22 | private var completion: (Error?) -> Void = { _ in } 23 | 24 | init() { 25 | self.urlResponse = HTTPURLResponse(url: Test.url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Length": "\(data.count)"])! 26 | self.chunks = Array(_createChunks(for: data, size: data.count / 3)) 27 | } 28 | 29 | func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable { 30 | self.didReceiveData = didReceiveData 31 | self.completion = completion 32 | self.resume() 33 | return _MockTask() 34 | } 35 | 36 | func resumeServingChunks(_ count: Int) { 37 | for _ in 0.. Void = {}) { 53 | DispatchQueue.main.async { 54 | if let chunk = self.chunks.first { 55 | self.chunks.removeFirst() 56 | self.didReceiveData(chunk, self.urlResponse) 57 | if self.chunks.isEmpty { 58 | self.completion(nil) 59 | completed() 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/NukePerformanceTests/ImageCachePerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | import Nuke 7 | 8 | class ImageCachePerformanceTests: XCTestCase { 9 | func testCacheWrite() { 10 | let cache = ImageCache() 11 | let image = ImageContainer(image: PlatformImage()) 12 | 13 | let urls = (0..<100_000).map { _ in return URL(string: "http://test.com/\(rnd(500))")! } 14 | let requests = urls.map { ImageRequest(url: $0) } 15 | 16 | measure { 17 | for request in requests { 18 | cache[request] = image 19 | } 20 | } 21 | } 22 | 23 | func testCacheHit() { 24 | let cache = ImageCache() 25 | let image = ImageContainer(image: PlatformImage()) 26 | 27 | for index in 0..<2000 { 28 | cache[ImageRequest(url: URL(string: "http://test.com/\(index)")!)] = image 29 | } 30 | 31 | var hits = 0 32 | 33 | let urls = (0..<100_000).map { _ in return URL(string: "http://test.com/\(rnd(2000))")! } 34 | let requests = urls.map { ImageRequest(url: $0) } 35 | 36 | measure { 37 | for request in requests { 38 | if cache[request] != nil { 39 | hits += 1 40 | } 41 | } 42 | } 43 | 44 | print("hits: \(hits)") 45 | } 46 | 47 | func testCacheMiss() { 48 | let cache = ImageCache() 49 | 50 | var misses = 0 51 | 52 | let urls = (0..<100_000).map { _ in return URL(string: "http://test.com/\(rnd(200))")! } 53 | let requests = urls.map { ImageRequest(url: $0) } 54 | 55 | measure { 56 | for request in requests { 57 | if cache[request] != nil { 58 | misses += 1 59 | } 60 | } 61 | } 62 | 63 | print("misses: \(misses)") 64 | } 65 | 66 | func testCacheReplacement() { 67 | let cache = ImageCache() 68 | let request = Test.request 69 | let image = Test.container 70 | 71 | measure { 72 | for _ in 0..<100_000 { 73 | cache[request] = image 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/MockDataLoader.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Nuke 7 | 8 | private let data: Data = Test.data(name: "fixture", extension: "jpeg") 9 | 10 | private final class MockDataTask: Cancellable, @unchecked Sendable { 11 | var _cancel: () -> Void = { } 12 | func cancel() { 13 | _cancel() 14 | } 15 | } 16 | 17 | class MockDataLoader: DataLoading, @unchecked Sendable { 18 | static let DidStartTask = Notification.Name("com.github.kean.Nuke.Tests.MockDataLoader.DidStartTask") 19 | static let DidCancelTask = Notification.Name("com.github.kean.Nuke.Tests.MockDataLoader.DidCancelTask") 20 | 21 | @Atomic var createdTaskCount = 0 22 | var results = [URL: Result<(Data, URLResponse), NSError>]() 23 | let queue = OperationQueue() 24 | var isSuspended: Bool { 25 | get { queue.isSuspended } 26 | set { queue.isSuspended = newValue } 27 | } 28 | 29 | func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable { 30 | let task = MockDataTask() 31 | 32 | NotificationCenter.default.post(name: MockDataLoader.DidStartTask, object: self) 33 | 34 | createdTaskCount += 1 35 | 36 | let operation = BlockOperation { 37 | if let result = self.results[request.url!] { 38 | switch result { 39 | case let .success(val): 40 | let data = val.0 41 | if !data.isEmpty { 42 | didReceiveData(data.prefix(data.count / 2), val.1) 43 | didReceiveData(data.suffix(data.count / 2), val.1) 44 | } 45 | completion(nil) 46 | case let .failure(err): 47 | completion(err) 48 | } 49 | } else { 50 | didReceiveData(data, URLResponse(url: request.url ?? Test.url, mimeType: "jpeg", expectedContentLength: 22789, textEncodingName: nil)) 51 | completion(nil) 52 | } 53 | } 54 | queue.addOperation(operation) 55 | 56 | task._cancel = { 57 | NotificationCenter.default.post(name: MockDataLoader.DidCancelTask, object: self) 58 | operation.cancel() 59 | } 60 | 61 | return task 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImageProcessorsTests/GaussianBlurTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | #if !os(macOS) 9 | import UIKit 10 | #endif 11 | 12 | #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) 13 | 14 | class ImageProcessorsGaussianBlurTest: XCTestCase { 15 | func testApplyBlur() { 16 | // Given 17 | let image = Test.image 18 | let processor = ImageProcessors.GaussianBlur() 19 | XCTAssertFalse(processor.description.isEmpty) // Bumping that test coverage 20 | 21 | // When 22 | XCTAssertNotNil(processor.process(image)) 23 | } 24 | 25 | func testApplyBlurProducesImagesBackedByCoreGraphics() { 26 | // Given 27 | let image = Test.image 28 | let processor = ImageProcessors.GaussianBlur() 29 | 30 | // When 31 | XCTAssertNotNil(processor.process(image)) 32 | } 33 | 34 | func testApplyBlurProducesTransparentImages() throws { 35 | // Given 36 | let image = Test.image 37 | let processor = ImageProcessors.GaussianBlur() 38 | 39 | // When 40 | let processed = try XCTUnwrap(processor.process(image)) 41 | 42 | // Then 43 | XCTAssertEqual(processed.cgImage?.isOpaque, false) 44 | } 45 | 46 | func testImagesWithSameRadiusHasSameIdentifiers() { 47 | XCTAssertEqual( 48 | ImageProcessors.GaussianBlur(radius: 2).identifier, 49 | ImageProcessors.GaussianBlur(radius: 2).identifier 50 | ) 51 | } 52 | 53 | func testImagesWithDifferentRadiusHasDifferentIdentifiers() { 54 | XCTAssertNotEqual( 55 | ImageProcessors.GaussianBlur(radius: 2).identifier, 56 | ImageProcessors.GaussianBlur(radius: 3).identifier 57 | ) 58 | } 59 | 60 | func testImagesWithSameRadiusHasSameHashableIdentifiers() { 61 | XCTAssertEqual( 62 | ImageProcessors.GaussianBlur(radius: 2).hashableIdentifier, 63 | ImageProcessors.GaussianBlur(radius: 2).hashableIdentifier 64 | ) 65 | } 66 | 67 | func testImagesWithDifferentRadiusHasDifferentHashableIdentifiers() { 68 | XCTAssertNotEqual( 69 | ImageProcessors.GaussianBlur(radius: 2).hashableIdentifier, 70 | ImageProcessors.GaussianBlur(radius: 3).hashableIdentifier 71 | ) 72 | } 73 | } 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /Sources/Nuke/Tasks/AsyncPipelineTask.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | // Each task holds a strong reference to the pipeline. This is by design. The 8 | // user does not need to hold a strong reference to the pipeline. 9 | class AsyncPipelineTask: AsyncTask, @unchecked Sendable { 10 | let pipeline: ImagePipeline 11 | // A canonical request representing the unit work performed by the task. 12 | let request: ImageRequest 13 | 14 | init(_ pipeline: ImagePipeline, _ request: ImageRequest) { 15 | self.pipeline = pipeline 16 | self.request = request 17 | } 18 | } 19 | 20 | // Returns all image tasks subscribed to the current pipeline task. 21 | // A suboptimal approach just to make the new DiskCachPolicy.automatic work. 22 | protocol ImageTaskSubscribers { 23 | var imageTasks: [ImageTask] { get } 24 | } 25 | 26 | extension ImageTask: ImageTaskSubscribers { 27 | var imageTasks: [ImageTask] { 28 | [self] 29 | } 30 | } 31 | 32 | extension AsyncPipelineTask: ImageTaskSubscribers { 33 | var imageTasks: [ImageTask] { 34 | subscribers.flatMap { subscribers -> [ImageTask] in 35 | (subscribers as? ImageTaskSubscribers)?.imageTasks ?? [] 36 | } 37 | } 38 | } 39 | 40 | extension AsyncPipelineTask { 41 | /// Decodes the data on the dedicated queue and calls the completion 42 | /// on the pipeline's internal queue. 43 | func decode(_ context: ImageDecodingContext, decoder: any ImageDecoding, _ completion: @Sendable @escaping (Result) -> Void) { 44 | @Sendable func decode() -> Result { 45 | signpost(context.isCompleted ? "DecodeImageData" : "DecodeProgressiveImageData") { 46 | Result { try decoder.decode(context) } 47 | .mapError { .decodingFailed(decoder: decoder, context: context, error: $0) } 48 | } 49 | } 50 | guard decoder.isAsynchronous else { 51 | return completion(decode()) 52 | } 53 | operation = pipeline.configuration.imageDecodingQueue.add { [weak self] in 54 | guard let self else { return } 55 | let response = decode() 56 | self.pipeline.queue.async { 57 | completion(response) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Nuke/Decoding/ImageDecoderRegistry.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// A registry of image codecs. 8 | public final class ImageDecoderRegistry: @unchecked Sendable { 9 | /// A shared registry. 10 | public static let shared = ImageDecoderRegistry() 11 | 12 | private var matches = [(ImageDecodingContext) -> (any ImageDecoding)?]() 13 | private let lock = NSLock() 14 | 15 | /// Initializes a custom registry. 16 | public init() { 17 | register(ImageDecoders.Default.init) 18 | } 19 | 20 | /// Returns a decoder that matches the given context. 21 | public func decoder(for context: ImageDecodingContext) -> (any ImageDecoding)? { 22 | lock.lock() 23 | defer { lock.unlock() } 24 | 25 | for match in matches.reversed() { 26 | if let decoder = match(context) { 27 | return decoder 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | /// Registers a decoder to be used in a given decoding context. 34 | /// 35 | /// **Progressive Decoding** 36 | /// 37 | /// The decoder is created once and is used for the entire decoding session, 38 | /// including progressively decoded images. If the decoder doesn't support 39 | /// progressive decoding, return `nil` when `isCompleted` is `false`. 40 | public func register(_ match: @escaping (ImageDecodingContext) -> (any ImageDecoding)?) { 41 | lock.lock() 42 | defer { lock.unlock() } 43 | 44 | matches.append(match) 45 | } 46 | 47 | /// Removes all registered decoders. 48 | public func clear() { 49 | lock.lock() 50 | defer { lock.unlock() } 51 | 52 | matches = [] 53 | } 54 | } 55 | 56 | /// Image decoding context used when selecting which decoder to use. 57 | public struct ImageDecodingContext: @unchecked Sendable { 58 | public var request: ImageRequest 59 | public var data: Data 60 | /// Returns `true` if the download was completed. 61 | public var isCompleted: Bool 62 | public var urlResponse: URLResponse? 63 | public var cacheType: ImageResponse.CacheType? 64 | 65 | public init(request: ImageRequest, data: Data, isCompleted: Bool = true, urlResponse: URLResponse? = nil, cacheType: ImageResponse.CacheType? = nil) { 66 | self.request = request 67 | self.data = data 68 | self.isCompleted = isCompleted 69 | self.urlResponse = urlResponse 70 | self.cacheType = cacheType 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Nuke/Processing/ImageProcessors+Composition.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | #if !os(macOS) 8 | import UIKit 9 | #else 10 | import AppKit 11 | #endif 12 | 13 | extension ImageProcessors { 14 | /// Composes multiple processors. 15 | public struct Composition: ImageProcessing, Hashable, CustomStringConvertible { 16 | let processors: [any ImageProcessing] 17 | 18 | /// Composes multiple processors. 19 | public init(_ processors: [any ImageProcessing]) { 20 | // note: multiple compositions are not flatten by default. 21 | self.processors = processors 22 | } 23 | 24 | /// Processes the given image by applying each processor in an order in 25 | /// which they were added. If one of the processors fails to produce 26 | /// an image the processing stops and `nil` is returned. 27 | public func process(_ image: PlatformImage) -> PlatformImage? { 28 | processors.reduce(image) { image, processor in 29 | autoreleasepool { 30 | image.flatMap(processor.process) 31 | } 32 | } 33 | } 34 | 35 | /// Processes the given image by applying each processor in an order in 36 | /// which they were added. If one of the processors fails to produce 37 | /// an image the processing stops and an error is thrown. 38 | public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer { 39 | try processors.reduce(container) { container, processor in 40 | try autoreleasepool { 41 | try processor.process(container, context: context) 42 | } 43 | } 44 | } 45 | 46 | /// Returns combined identifier of all the underlying processors. 47 | public var identifier: String { 48 | processors.map({ $0.identifier }).joined() 49 | } 50 | 51 | /// Creates a combined hash of all the given processors. 52 | public func hash(into hasher: inout Hasher) { 53 | for processor in processors { 54 | hasher.combine(processor.hashableIdentifier) 55 | } 56 | } 57 | 58 | /// Compares all the underlying processors for equality. 59 | public static func == (lhs: Composition, rhs: Composition) -> Bool { 60 | lhs.processors == rhs.processors 61 | } 62 | 63 | public var description: String { 64 | "Composition(processors: \(processors))" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/NukeVideo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Tests/NukeExtensions.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Nuke 7 | 8 | extension ImagePipeline.Error: Equatable { 9 | public static func == (lhs: ImagePipeline.Error, rhs: ImagePipeline.Error) -> Bool { 10 | switch (lhs, rhs) { 11 | case (.dataMissingInCache, .dataMissingInCache): return true 12 | case let (.dataLoadingFailed(lhs), .dataLoadingFailed(rhs)): 13 | return lhs as NSError == rhs as NSError 14 | case (.dataIsEmpty, .dataIsEmpty): return true 15 | case (.decoderNotRegistered, .decoderNotRegistered): return true 16 | case (.decodingFailed, .decodingFailed): return true 17 | case (.processingFailed, .processingFailed): return true 18 | case (.imageRequestMissing, .imageRequestMissing): return true 19 | case (.pipelineInvalidated, .pipelineInvalidated): return true 20 | default: return false 21 | } 22 | } 23 | } 24 | 25 | extension ImageResponse: Equatable { 26 | public static func == (lhs: ImageResponse, rhs: ImageResponse) -> Bool { 27 | return lhs.image === rhs.image 28 | } 29 | } 30 | 31 | extension ImagePipeline { 32 | func reconfigured(_ configure: (inout ImagePipeline.Configuration) -> Void) -> ImagePipeline { 33 | var configuration = self.configuration 34 | configure(&configuration) 35 | return ImagePipeline(configuration: configuration) 36 | } 37 | } 38 | 39 | extension ImagePipeline { 40 | private static var stack = [ImagePipeline]() 41 | 42 | static func pushShared(_ shared: ImagePipeline) { 43 | stack.append(ImagePipeline.shared) 44 | ImagePipeline.shared = shared 45 | } 46 | 47 | static func popShared() { 48 | ImagePipeline.shared = stack.removeLast() 49 | } 50 | } 51 | 52 | extension ImageProcessing { 53 | /// A throwing version of a regular method. 54 | func processThrowing(_ image: PlatformImage) throws -> PlatformImage { 55 | let context = ImageProcessingContext(request: Test.request, response: Test.response, isCompleted: true) 56 | return (try process(ImageContainer(image: image), context: context)).image 57 | } 58 | } 59 | 60 | extension ImageCaching { 61 | subscript(request: ImageRequest) -> ImageContainer? { 62 | get { self[ImageCacheKey(request: request)] } 63 | set { self[ImageCacheKey(request: request)] = newValue } 64 | } 65 | } 66 | 67 | #if os(macOS) 68 | import Cocoa 69 | typealias _ImageView = NSImageView 70 | #elseif os(iOS) || os(tvOS) || os(visionOS) 71 | import UIKit 72 | typealias _ImageView = UIImageView 73 | #endif 74 | -------------------------------------------------------------------------------- /Sources/Nuke/Decoding/ImageDecoding.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// An image decoder. 8 | /// 9 | /// A decoder is a one-shot object created for a single image decoding session. 10 | /// 11 | /// - note: If you need additional information in the decoder, you can pass 12 | /// anything that you might need from the ``ImageDecodingContext``. 13 | public protocol ImageDecoding: Sendable { 14 | /// Return `true` if you want the decoding to be performed on the decoding 15 | /// queue (see ``ImagePipeline/Configuration-swift.struct/imageDecodingQueue``). If `false`, the decoding will be 16 | /// performed synchronously on the pipeline operation queue. By default, `true`. 17 | var isAsynchronous: Bool { get } 18 | 19 | /// Produces an image from the given image data. 20 | func decode(_ data: Data) throws -> ImageContainer 21 | 22 | /// Produces an image from the given partially downloaded image data. 23 | /// This method might be called multiple times during a single decoding 24 | /// session. When the image download is complete, ``decode(_:)`` method is called. 25 | /// 26 | /// - returns: nil by default. 27 | func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? 28 | } 29 | 30 | extension ImageDecoding { 31 | /// Returns `true` by default. 32 | public var isAsynchronous: Bool { true } 33 | 34 | /// The default implementation which simply returns `nil` (no progressive 35 | /// decoding available). 36 | public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { nil } 37 | } 38 | 39 | public enum ImageDecodingError: Error, CustomStringConvertible, Sendable { 40 | case unknown 41 | 42 | public var description: String { "Unknown" } 43 | } 44 | 45 | extension ImageDecoding { 46 | func decode(_ context: ImageDecodingContext) throws -> ImageResponse { 47 | let container: ImageContainer = try autoreleasepool { 48 | if context.isCompleted { 49 | return try decode(context.data) 50 | } else { 51 | if let preview = decodePartiallyDownloadedData(context.data) { 52 | return preview 53 | } 54 | throw ImageDecodingError.unknown 55 | } 56 | } 57 | #if !os(macOS) 58 | if container.userInfo[.isThumbnailKey] == nil { 59 | ImageDecompression.setDecompressionNeeded(true, for: container.image) 60 | } 61 | #endif 62 | return ImageResponse(container: container, request: context.request, urlResponse: context.urlResponse, cacheType: context.cacheType) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/NukeTests/LinkedListTest.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | class LinkedListTests: XCTestCase { 9 | let list = LinkedList() 10 | 11 | func testEmptyWhenCreated() { 12 | XCTAssertNil(list.first) 13 | XCTAssertNil(list.last) 14 | XCTAssertTrue(list.isEmpty) 15 | } 16 | 17 | // MARK: - Append 18 | 19 | func testAppendOnce() { 20 | // When 21 | list.append(1) 22 | 23 | // Then 24 | XCTAssertFalse(list.isEmpty) 25 | XCTAssertEqual(list.first?.value, 1) 26 | XCTAssertEqual(list.last?.value, 1) 27 | } 28 | 29 | func testAppendTwice() { 30 | // When 31 | list.append(1) 32 | list.append(2) 33 | 34 | // Then 35 | XCTAssertEqual(list.first?.value, 1) 36 | XCTAssertEqual(list.last?.value, 2) 37 | } 38 | 39 | // MARK: - Remove 40 | 41 | func testRemoveSingle() { 42 | // Given 43 | let node = list.append(1) 44 | 45 | // When 46 | list.remove(node) 47 | 48 | // Then 49 | XCTAssertNil(list.first) 50 | XCTAssertNil(list.last) 51 | } 52 | 53 | func testRemoveFromBeggining() { 54 | // Given 55 | let node = list.append(1) 56 | list.append(2) 57 | list.append(3) 58 | 59 | // When 60 | list.remove(node) 61 | 62 | // Then 63 | XCTAssertEqual(list.first?.value, 2) 64 | XCTAssertEqual(list.last?.value, 3) 65 | } 66 | 67 | func testRemoveFromEnd() { 68 | // Given 69 | list.append(1) 70 | list.append(2) 71 | let node = list.append(3) 72 | 73 | // When 74 | list.remove(node) 75 | 76 | // Then 77 | XCTAssertEqual(list.first?.value, 1) 78 | XCTAssertEqual(list.last?.value, 2) 79 | } 80 | 81 | func testRemoveFromMiddle() { 82 | // Given 83 | list.append(1) 84 | let node = list.append(2) 85 | list.append(3) 86 | 87 | // When 88 | list.remove(node) 89 | 90 | // Then 91 | XCTAssertEqual(list.first?.value, 1) 92 | XCTAssertEqual(list.last?.value, 3) 93 | } 94 | 95 | func testRemoveAll() { 96 | // Given 97 | list.append(1) 98 | list.append(2) 99 | list.append(3) 100 | 101 | // When 102 | list.removeAllElements() 103 | 104 | // Then 105 | XCTAssertNil(list.first) 106 | XCTAssertNil(list.last) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Nuke/Tasks/TaskFetchOriginalImage.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// Receives data from ``TaskLoadImageData`` and decodes it as it arrives. 8 | final class TaskFetchOriginalImage: AsyncPipelineTask, @unchecked Sendable { 9 | private var decoder: (any ImageDecoding)? 10 | 11 | override func start() { 12 | dependency = pipeline.makeTaskFetchOriginalData(for: request).subscribe(self) { [weak self] in 13 | self?.didReceiveData($0.0, urlResponse: $0.1, isCompleted: $1) 14 | } 15 | } 16 | 17 | /// Receiving data from `TaskFetchOriginalData`. 18 | private func didReceiveData(_ data: Data, urlResponse: URLResponse?, isCompleted: Bool) { 19 | guard isCompleted || pipeline.configuration.isProgressiveDecodingEnabled else { 20 | return 21 | } 22 | 23 | if !isCompleted && operation != nil { 24 | return // Back pressure - already decoding another progressive data chunk 25 | } 26 | 27 | if isCompleted { 28 | operation?.cancel() // Cancel any potential pending progressive decoding tasks 29 | } 30 | 31 | let context = ImageDecodingContext(request: request, data: data, isCompleted: isCompleted, urlResponse: urlResponse) 32 | guard let decoder = getDecoder(for: context) else { 33 | if isCompleted { 34 | send(error: .decoderNotRegistered(context: context)) 35 | } else { 36 | // Try again when more data is downloaded. 37 | } 38 | return 39 | } 40 | 41 | decode(context, decoder: decoder) { [weak self] in 42 | self?.didFinishDecoding(context: context, result: $0) 43 | } 44 | } 45 | 46 | private func didFinishDecoding(context: ImageDecodingContext, result: Result) { 47 | operation = nil 48 | 49 | switch result { 50 | case .success(let response): 51 | send(value: response, isCompleted: context.isCompleted) 52 | case .failure(let error): 53 | if context.isCompleted { 54 | send(error: error) 55 | } 56 | } 57 | } 58 | 59 | // Lazily creates decoding for task 60 | private func getDecoder(for context: ImageDecodingContext) -> (any ImageDecoding)? { 61 | // Return the existing processor in case it has already been created. 62 | if let decoder { 63 | return decoder 64 | } 65 | let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) 66 | self.decoder = decoder 67 | return decoder 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Nuke/Tasks/TaskFetchWithPublisher.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// Fetches data using the publisher provided with the request. 8 | /// Unlike `TaskFetchOriginalImageData`, there is no resumable data involved. 9 | final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)>, @unchecked Sendable { 10 | private lazy var data = Data() 11 | 12 | override func start() { 13 | if request.options.contains(.skipDataLoadingQueue) { 14 | loadData(finish: { /* do nothing */ }) 15 | } else { 16 | // Wrap data request in an operation to limit the maximum number of 17 | // concurrent data tasks. 18 | operation = pipeline.configuration.dataLoadingQueue.add { [weak self] finish in 19 | guard let self else { 20 | return finish() 21 | } 22 | self.pipeline.queue.async { 23 | self.loadData { finish() } 24 | } 25 | } 26 | } 27 | } 28 | 29 | // This methods gets called inside data loading operation (Operation). 30 | private func loadData(finish: @escaping () -> Void) { 31 | guard !isDisposed else { 32 | return finish() 33 | } 34 | 35 | guard let publisher = request.publisher else { 36 | send(error: .dataLoadingFailed(error: URLError(.unknown))) // This is just a placeholder error, never thrown 37 | return assertionFailure("This should never happen") 38 | } 39 | 40 | let cancellable = publisher.sink(receiveCompletion: { [weak self] result in 41 | finish() // Finish the operation! 42 | guard let self else { return } 43 | self.pipeline.queue.async { 44 | self.dataTaskDidFinish(result) 45 | } 46 | }, receiveValue: { [weak self] data in 47 | guard let self else { return } 48 | self.pipeline.queue.async { 49 | self.data.append(data) 50 | } 51 | }) 52 | 53 | onCancelled = { 54 | finish() 55 | cancellable.cancel() 56 | } 57 | } 58 | 59 | private func dataTaskDidFinish(_ result: PublisherCompletion) { 60 | switch result { 61 | case .finished: 62 | guard !data.isEmpty else { 63 | send(error: .dataIsEmpty) 64 | return 65 | } 66 | storeDataInCacheIfNeeded(data) 67 | send(value: (data, nil), isCompleted: true) 68 | case .failure(let error): 69 | send(error: .dataLoadingFailed(error: error)) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | class ImagePipelineTaskDelegateTests: XCTestCase { 9 | private var dataLoader: MockDataLoader! 10 | private var pipeline: ImagePipeline! 11 | private var delegate: ImagePipelineObserver! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | dataLoader = MockDataLoader() 17 | delegate = ImagePipelineObserver() 18 | 19 | pipeline = ImagePipeline(delegate: delegate) { 20 | $0.dataLoader = dataLoader 21 | $0.imageCache = nil 22 | } 23 | } 24 | 25 | func testStartAndCompletedEvents() throws { 26 | var result: Result? 27 | expect(pipeline).toLoadImage(with: Test.request) { result = $0 } 28 | wait() 29 | 30 | // Then 31 | XCTAssertEqual(delegate.events, [ 32 | ImageTaskEvent.created, 33 | .started, 34 | .progressUpdated(completedUnitCount: 22789, totalUnitCount: 22789), 35 | .completed(result: try XCTUnwrap(result)) 36 | ]) 37 | } 38 | 39 | func testProgressUpdateEvents() throws { 40 | let request = ImageRequest(url: Test.url) 41 | dataLoader.results[Test.url] = .success( 42 | (Data(count: 20), URLResponse(url: Test.url, mimeType: "jpeg", expectedContentLength: 20, textEncodingName: nil)) 43 | ) 44 | 45 | var result: Result? 46 | expect(pipeline).toFailRequest(request) { result = $0 } 47 | wait() 48 | 49 | // Then 50 | XCTAssertEqual(delegate.events, [ 51 | ImageTaskEvent.created, 52 | .started, 53 | .progressUpdated(completedUnitCount: 10, totalUnitCount: 20), 54 | .progressUpdated(completedUnitCount: 20, totalUnitCount: 20), 55 | .completed(result: try XCTUnwrap(result)) 56 | ]) 57 | } 58 | 59 | func testCancellationEvents() { 60 | dataLoader.queue.isSuspended = true 61 | 62 | expectNotification(MockDataLoader.DidStartTask, object: dataLoader) 63 | let task = pipeline.loadImage(with: Test.request) { _ in 64 | XCTFail() 65 | } 66 | wait() // Wait till operation is created 67 | 68 | expectNotification(ImagePipelineObserver.didCancelTask, object: delegate) 69 | task.cancel() 70 | wait() 71 | 72 | // Then 73 | XCTAssertEqual(delegate.events, [ 74 | ImageTaskEvent.created, 75 | .started, 76 | .cancelled 77 | ]) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.scripts/create-xcframeworks.sh: -------------------------------------------------------------------------------- 1 | ROOT="./.build/xcframeworks" 2 | 3 | rm -rf $ROOT 4 | 5 | for SDK in iphoneos iphonesimulator macosx appletvos appletvsimulator watchos watchsimulator 6 | do 7 | xcodebuild archive \ 8 | -scheme NukeUI \ 9 | -archivePath "$ROOT/nuke-$SDK.xcarchive" \ 10 | -sdk $SDK \ 11 | SKIP_INSTALL=NO \ 12 | BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ 13 | DEBUG_INFORMATION_FORMAT=DWARF 14 | done 15 | 16 | xcodebuild -create-xcframework \ 17 | -framework "$ROOT/nuke-iphoneos.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 18 | -framework "$ROOT/nuke-iphonesimulator.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 19 | -output "$ROOT/Nuke.xcframework" 20 | 21 | xcodebuild -create-xcframework \ 22 | -framework "$ROOT/nuke-iphoneos.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 23 | -framework "$ROOT/nuke-iphonesimulator.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 24 | -output "$ROOT/NukeUI.xcframework" 25 | 26 | cd $ROOT 27 | zip -r -X nuke-xcframeworks-ios.zip *.xcframework 28 | rm -rf *.xcframework 29 | cd - 30 | 31 | xcodebuild -create-xcframework \ 32 | -framework "$ROOT/nuke-iphoneos.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 33 | -framework "$ROOT/nuke-iphonesimulator.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 34 | -framework "$ROOT/nuke-macosx.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 35 | -framework "$ROOT/nuke-appletvos.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 36 | -framework "$ROOT/nuke-appletvsimulator.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 37 | -framework "$ROOT/nuke-watchos.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 38 | -framework "$ROOT/nuke-watchsimulator.xcarchive/Products/Library/Frameworks/Nuke.framework" \ 39 | -output "$ROOT/Nuke.xcframework" 40 | 41 | xcodebuild -create-xcframework \ 42 | -framework "$ROOT/nuke-iphoneos.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 43 | -framework "$ROOT/nuke-iphonesimulator.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 44 | -framework "$ROOT/nuke-macosx.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 45 | -framework "$ROOT/nuke-appletvos.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 46 | -framework "$ROOT/nuke-appletvsimulator.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 47 | -framework "$ROOT/nuke-watchos.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 48 | -framework "$ROOT/nuke-watchsimulator.xcarchive/Products/Library/Frameworks/NukeUI.framework" \ 49 | -output "$ROOT/NukeUI.xcframework" 50 | 51 | cd $ROOT 52 | zip -r -X nuke-xcframeworks-all-platforms.zip *.xcframework 53 | rm -rf *.xcframework 54 | cd - 55 | 56 | mv $ROOT/*.zip ./ 57 | -------------------------------------------------------------------------------- /Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import CoreGraphics 7 | import ImageIO 8 | 9 | #if !os(macOS) 10 | import UIKit 11 | #else 12 | import AppKit 13 | #endif 14 | 15 | extension ImageEncoders { 16 | /// An Image I/O based encoder. 17 | /// 18 | /// Image I/O is a system framework that allows applications to read and 19 | /// write most image file formats. This framework offers high efficiency, 20 | /// color management, and access to image metadata. 21 | public struct ImageIO: ImageEncoding { 22 | public let type: AssetType 23 | public let compressionRatio: Float 24 | 25 | /// - parameter format: The output format. Make sure that the format is 26 | /// supported on the current hardware.s 27 | /// - parameter compressionRatio: 0.8 by default. 28 | public init(type: AssetType, compressionRatio: Float = 0.8) { 29 | self.type = type 30 | self.compressionRatio = compressionRatio 31 | } 32 | 33 | private static let availability = Atomic<[AssetType: Bool]>(value: [:]) 34 | 35 | /// Returns `true` if the encoding is available for the given format on 36 | /// the current hardware. Some of the most recent formats might not be 37 | /// available so its best to check before using them. 38 | public static func isSupported(type: AssetType) -> Bool { 39 | if let isAvailable = availability.value[type] { 40 | return isAvailable 41 | } 42 | let isAvailable = CGImageDestinationCreateWithData( 43 | NSMutableData() as CFMutableData, type.rawValue as CFString, 1, nil 44 | ) != nil 45 | availability.withLock { $0[type] = isAvailable } 46 | return isAvailable 47 | } 48 | 49 | public func encode(_ image: PlatformImage) -> Data? { 50 | guard let source = image.cgImage, 51 | let data = CFDataCreateMutable(nil, 0), 52 | let destination = CGImageDestinationCreateWithData(data, type.rawValue as CFString, 1, nil) else { 53 | return nil 54 | } 55 | var options: [CFString: Any] = [ 56 | kCGImageDestinationLossyCompressionQuality: compressionRatio 57 | ] 58 | #if canImport(UIKit) 59 | options[kCGImagePropertyOrientation] = CGImagePropertyOrientation(image.imageOrientation).rawValue 60 | #endif 61 | CGImageDestinationAddImage(destination, source, options as CFDictionary) 62 | guard CGImageDestinationFinalize(destination) else { 63 | return nil 64 | } 65 | return data as Data 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/NukeVideo/ImageDecoders+Video.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | #if !os(watchOS) && !os(visionOS) 6 | 7 | import Foundation 8 | import AVKit 9 | import AVFoundation 10 | import Nuke 11 | 12 | extension ImageDecoders { 13 | /// The video decoder. 14 | /// 15 | /// To enable the video decoder, register it with a shared registry: 16 | /// 17 | /// ```swift 18 | /// ImageDecoderRegistry.shared.register(ImageDecoders.Video.init) 19 | /// ``` 20 | public final class Video: ImageDecoding, @unchecked Sendable { 21 | private var didProducePreview = false 22 | private let type: AssetType 23 | public var isAsynchronous: Bool { true } 24 | 25 | private let lock = NSLock() 26 | 27 | public init?(context: ImageDecodingContext) { 28 | guard let type = AssetType(context.data), type.isVideo else { return nil } 29 | self.type = type 30 | } 31 | 32 | public func decode(_ data: Data) throws -> ImageContainer { 33 | ImageContainer(image: PlatformImage(), type: type, data: data, userInfo: [ 34 | .videoAssetKey: AVDataAsset(data: data, type: type) 35 | ]) 36 | } 37 | 38 | public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { 39 | lock.lock() 40 | defer { lock.unlock() } 41 | 42 | guard let type = AssetType(data), type.isVideo else { return nil } 43 | guard !didProducePreview else { 44 | return nil // We only need one preview 45 | } 46 | guard let preview = makePreview(for: data, type: type) else { 47 | return nil 48 | } 49 | didProducePreview = true 50 | return ImageContainer(image: preview, type: type, isPreview: true, data: data, userInfo: [ 51 | .videoAssetKey: AVDataAsset(data: data, type: type) 52 | ]) 53 | } 54 | } 55 | } 56 | 57 | extension ImageContainer.UserInfoKey { 58 | /// A key for a video asset (`AVAsset`) 59 | public static let videoAssetKey: ImageContainer.UserInfoKey = "com.github/kean/nuke/video-asset" 60 | } 61 | 62 | private func makePreview(for data: Data, type: AssetType) -> PlatformImage? { 63 | let asset = AVDataAsset(data: data, type: type) 64 | let generator = AVAssetImageGenerator(asset: asset) 65 | guard let cgImage = try? generator.copyCGImage(at: CMTime(value: 0, timescale: 1), actualTime: nil) else { 66 | return nil 67 | } 68 | return PlatformImage(cgImage: cgImage) 69 | } 70 | 71 | #endif 72 | 73 | #if os(macOS) 74 | extension NSImage { 75 | convenience init(cgImage: CGImage) { 76 | self.init(cgImage: cgImage, size: .zero) 77 | } 78 | } 79 | #endif 80 | -------------------------------------------------------------------------------- /Documentation/Nuke.docc/Customization/ImageProcessing/image-processing.md: -------------------------------------------------------------------------------- 1 | # Image Processing 2 | 3 | Learn how to use existing image filters and create custom ones. 4 | 5 | ## Overview 6 | 7 | Nuke features a powerful and efficient image processing infrastructure with multiple built-in processors and an API for creating custom ones. 8 | 9 | ```swift 10 | ImageRequest(url: url, processors: [ 11 | .resize(size: imageView.bounds.size) 12 | ]) 13 | ``` 14 | 15 | The built-in processors can all be found in the ``ImageProcessors`` namespace, but the preferred way to create them is by using static factory methods on ``ImageProcessing`` protocol. 16 | 17 | ## Custom Processors 18 | 19 | Custom processors need to implement ``ImageProcessing`` protocol. For the basic image processing needs, implement ``ImageProcessing/process(_:)`` method and create an identifier that uniquely identifies the processor. For processors with no input parameters, return a static string. 20 | 21 | ```swift 22 | public protocol ImageProcessing { 23 | func process(image: UIImage) -> UIImage? // NSImage on macOS 24 | var identifier: String { get } 25 | } 26 | ``` 27 | 28 | > All processing tasks are executed on a dedicated queue (``ImagePipeline/Configuration-swift.struct/imageProcessingQueue``). 29 | 30 | If your processor needs to manipulate image metadata (``ImageContainer``) or get access to more information via ``ImageProcessingContext``, there is an additional method that you can implement in addition to ``ImageProcessing/process(_:context:)-26ffb``. 31 | 32 | ```swift 33 | public protocol ImageProcessing { 34 | func process(_ image container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer 35 | } 36 | ``` 37 | 38 | In addition to ``ImageProcessing/identifier`` (a `String`), you can implement ``ImageProcessing/hashableIdentifier-2i3a7`` to be used by the memory cache where string manipulations would be too slow. By default, this method returns the `identifier` string. If your processor conforms to `Hashable` protocol, it gets a default ``ImageProcessing/hashableIdentifier-2i3a7`` implementation that returns `self`. 39 | 40 | ## Topics 41 | 42 | ### Image Processing 43 | 44 | - ``ImageProcessing`` 45 | - ``ImageProcessingOptions`` 46 | - ``ImageProcessingContext`` 47 | - ``ImageProcessingError`` 48 | 49 | ### Built-In Processors 50 | 51 | - ``ImageProcessing/resize(size:unit:contentMode:crop:upscale:)`` 52 | - ``ImageProcessing/resize(width:unit:upscale:)`` 53 | - ``ImageProcessing/resize(height:unit:upscale:)`` 54 | - ``ImageProcessing/circle(border:)`` 55 | - ``ImageProcessing/roundedCorners(radius:unit:border:)`` 56 | - ``ImageProcessing/gaussianBlur(radius:)`` 57 | - ``ImageProcessing/coreImageFilter(name:)`` 58 | - ``ImageProcessing/coreImageFilter(name:parameters:identifier:)`` 59 | - ``ImageProcessing/process(id:_:)`` 60 | - ``ImageProcessors`` 61 | -------------------------------------------------------------------------------- /Sources/NukeVideo/AVDataAsset.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import AVKit 6 | import Foundation 7 | import Nuke 8 | 9 | extension AssetType { 10 | /// Returns `true` if the asset represents a video file 11 | public var isVideo: Bool { 12 | self == .mp4 || self == .m4v || self == .mov 13 | } 14 | } 15 | 16 | #if !os(watchOS) 17 | 18 | private extension AssetType { 19 | var avFileType: AVFileType? { 20 | switch self { 21 | case .mp4: return .mp4 22 | case .m4v: return .m4v 23 | case .mov: return .mov 24 | default: return nil 25 | } 26 | } 27 | } 28 | 29 | // This class keeps strong pointer to DataAssetResourceLoader 30 | final class AVDataAsset: AVURLAsset, @unchecked Sendable { 31 | private let resourceLoaderDelegate: DataAssetResourceLoader 32 | 33 | init(data: Data, type: AssetType?) { 34 | self.resourceLoaderDelegate = DataAssetResourceLoader( 35 | data: data, 36 | contentType: type?.avFileType?.rawValue ?? AVFileType.mp4.rawValue 37 | ) 38 | 39 | // The URL is irrelevant 40 | let url = URL(string: "in-memory-data://\(UUID().uuidString)") ?? URL(fileURLWithPath: "/dev/null") 41 | super.init(url: url, options: nil) 42 | 43 | resourceLoader.setDelegate(resourceLoaderDelegate, queue: .global()) 44 | } 45 | } 46 | 47 | // This allows LazyImage to play video from memory. 48 | private final class DataAssetResourceLoader: NSObject, AVAssetResourceLoaderDelegate { 49 | private let data: Data 50 | private let contentType: String 51 | 52 | init(data: Data, contentType: String) { 53 | self.data = data 54 | self.contentType = contentType 55 | } 56 | 57 | // MARK: - DataAssetResourceLoader 58 | 59 | func resourceLoader( 60 | _ resourceLoader: AVAssetResourceLoader, 61 | shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest 62 | ) -> Bool { 63 | if let contentRequest = loadingRequest.contentInformationRequest { 64 | contentRequest.contentType = contentType 65 | contentRequest.contentLength = Int64(data.count) 66 | contentRequest.isByteRangeAccessSupported = true 67 | } 68 | 69 | if let dataRequest = loadingRequest.dataRequest { 70 | if dataRequest.requestsAllDataToEndOfResource { 71 | dataRequest.respond(with: data[dataRequest.requestedOffset...]) 72 | } else { 73 | let range = dataRequest.requestedOffset..<(dataRequest.requestedOffset + Int64(dataRequest.requestedLength)) 74 | dataRequest.respond(with: data[range]) 75 | } 76 | } 77 | 78 | loadingRequest.finishLoading() 79 | 80 | return true 81 | } 82 | } 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /Sources/Nuke/Processing/ImageProcessingOptions.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | #if canImport(UIKit) 8 | import UIKit 9 | #endif 10 | 11 | #if canImport(AppKit) 12 | import AppKit 13 | #endif 14 | 15 | /// A namespace with shared image processing options. 16 | public enum ImageProcessingOptions: Sendable { 17 | 18 | public enum Unit: CustomStringConvertible, Sendable { 19 | case points 20 | case pixels 21 | 22 | public var description: String { 23 | switch self { 24 | case .points: return "points" 25 | case .pixels: return "pixels" 26 | } 27 | } 28 | } 29 | 30 | /// Draws a border. 31 | /// 32 | /// - important: To make sure that the border looks the way you expect, 33 | /// make sure that the images you display exactly match the size of the 34 | /// views in which they get displayed. If you can't guarantee that, pleasee 35 | /// consider adding border to a view layer. This should be your primary 36 | /// option regardless. 37 | public struct Border: Hashable, CustomStringConvertible, @unchecked Sendable { 38 | public let width: CGFloat 39 | 40 | #if canImport(UIKit) 41 | public let color: UIColor 42 | 43 | /// - parameters: 44 | /// - color: Border color. 45 | /// - width: Border width. 46 | /// - unit: Unit of the width. 47 | public init(color: UIColor, width: CGFloat = 1, unit: Unit = .points) { 48 | self.color = color 49 | self.width = width.converted(to: unit) 50 | } 51 | #else 52 | public let color: NSColor 53 | 54 | /// - parameters: 55 | /// - color: Border color. 56 | /// - width: Border width. 57 | /// - unit: Unit of the width. 58 | public init(color: NSColor, width: CGFloat = 1, unit: Unit = .points) { 59 | self.color = color 60 | self.width = width.converted(to: unit) 61 | } 62 | #endif 63 | 64 | public var description: String { 65 | "Border(color: \(color.hex), width: \(width) pixels)" 66 | } 67 | } 68 | 69 | /// An option for how to resize the image. 70 | public enum ContentMode: CustomStringConvertible, Sendable { 71 | /// Scales the image so that it completely fills the target area. 72 | /// Maintains the aspect ratio of the original image. 73 | case aspectFill 74 | 75 | /// Scales the image so that it fits the target size. Maintains the 76 | /// aspect ratio of the original image. 77 | case aspectFit 78 | 79 | public var description: String { 80 | switch self { 81 | case .aspectFill: return ".aspectFill" 82 | case .aspectFit: return ".aspectFit" 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImagePipelineTests/ImagePipelineProcessorTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | #if !os(macOS) 9 | import UIKit 10 | #endif 11 | 12 | class ImagePipelineProcessorTests: XCTestCase { 13 | var mockDataLoader: MockDataLoader! 14 | var pipeline: ImagePipeline! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | 19 | mockDataLoader = MockDataLoader() 20 | pipeline = ImagePipeline { 21 | $0.dataLoader = mockDataLoader 22 | $0.imageCache = nil 23 | } 24 | } 25 | 26 | override func tearDown() { 27 | super.tearDown() 28 | } 29 | 30 | // MARK: - Applying Filters 31 | 32 | func testThatImageIsProcessed() { 33 | // Given 34 | let request = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "processor1")]) 35 | 36 | // When 37 | expect(pipeline).toLoadImage(with: request) { result in 38 | // Then 39 | let image = result.value?.image 40 | XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["processor1"]) 41 | } 42 | wait() 43 | } 44 | 45 | // MARK: - Composing Filters 46 | 47 | func testApplyingMultipleProcessors() { 48 | // Given 49 | let request = ImageRequest( 50 | url: Test.url, 51 | processors: [ 52 | MockImageProcessor(id: "processor1"), 53 | MockImageProcessor(id: "processor2") 54 | ] 55 | ) 56 | 57 | // When 58 | expect(pipeline).toLoadImage(with: request) { result in 59 | // Then 60 | let image = result.value?.image 61 | XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["processor1", "processor2"]) 62 | } 63 | wait() 64 | } 65 | 66 | func testPerformingRequestWithoutProcessors() { 67 | // Given 68 | let request = ImageRequest(url: Test.url, processors: []) 69 | 70 | // When 71 | expect(pipeline).toLoadImage(with: request) { result in 72 | // Then 73 | let image = result.value?.image 74 | XCTAssertEqual(image?.nk_test_processorIDs ?? [], []) 75 | } 76 | wait() 77 | } 78 | 79 | // MARK: - Decompression 80 | 81 | #if !os(macOS) 82 | func testDecompressionSkippedIfProcessorsAreApplied() { 83 | // Given 84 | let request = ImageRequest(url: Test.url, processors: [ImageProcessors.Anonymous(id: "1", { image in 85 | XCTAssertTrue(ImageDecompression.isDecompressionNeeded(for: image) == true) 86 | return image 87 | })]) 88 | 89 | // When 90 | expect(pipeline).toLoadImage(with: request) { result in 91 | // Then 92 | } 93 | wait() 94 | } 95 | #endif 96 | } 97 | -------------------------------------------------------------------------------- /Tests/NukePerformanceTests/DataCachePeformanceTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | import Nuke 7 | 8 | class DataCachePeformanceTests: XCTestCase { 9 | var cache: DataCache! 10 | var count = 1000 11 | 12 | override func setUp() { 13 | super.setUp() 14 | 15 | cache = try! DataCache(name: UUID().uuidString) 16 | _ = cache["key"] // Wait till index is loaded. 17 | } 18 | 19 | override func tearDown() { 20 | super.tearDown() 21 | 22 | try? FileManager.default.removeItem(at: cache.path) 23 | } 24 | 25 | // MARK: - Write 26 | 27 | func testWriteWithFlush() { 28 | let data = Array(0.. Data { 98 | var bytes = [UInt8](repeating: 0, count: count) 99 | let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) 100 | assert(status == errSecSuccess) 101 | return Data(bytes) 102 | } 103 | -------------------------------------------------------------------------------- /Sources/Nuke/Internal/Operation.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | final class Operation: Foundation.Operation, @unchecked Sendable { 8 | override var isExecuting: Bool { 9 | get { 10 | os_unfair_lock_lock(lock) 11 | defer { os_unfair_lock_unlock(lock) } 12 | return _isExecuting 13 | } 14 | set { 15 | os_unfair_lock_lock(lock) 16 | _isExecuting = newValue 17 | os_unfair_lock_unlock(lock) 18 | 19 | willChangeValue(forKey: "isExecuting") 20 | didChangeValue(forKey: "isExecuting") 21 | } 22 | } 23 | 24 | override var isFinished: Bool { 25 | get { 26 | os_unfair_lock_lock(lock) 27 | defer { os_unfair_lock_unlock(lock) } 28 | return _isFinished 29 | } 30 | set { 31 | os_unfair_lock_lock(lock) 32 | _isFinished = newValue 33 | os_unfair_lock_unlock(lock) 34 | 35 | willChangeValue(forKey: "isFinished") 36 | didChangeValue(forKey: "isFinished") 37 | } 38 | } 39 | 40 | typealias Starter = @Sendable (_ finish: @Sendable @escaping () -> Void) -> Void 41 | private let starter: Starter 42 | 43 | private var _isExecuting = false 44 | private var _isFinished = false 45 | private var isFinishCalled = false 46 | private let lock: os_unfair_lock_t 47 | 48 | deinit { 49 | lock.deinitialize(count: 1) 50 | lock.deallocate() 51 | } 52 | 53 | init(starter: @escaping Starter) { 54 | self.starter = starter 55 | 56 | self.lock = .allocate(capacity: 1) 57 | self.lock.initialize(to: os_unfair_lock()) 58 | } 59 | 60 | override func start() { 61 | guard !isCancelled else { 62 | isFinished = true 63 | return 64 | } 65 | isExecuting = true 66 | starter { [weak self] in 67 | self?._finish() 68 | } 69 | } 70 | 71 | private func _finish() { 72 | os_unfair_lock_lock(lock) 73 | guard !isFinishCalled else { 74 | return os_unfair_lock_unlock(lock) 75 | } 76 | isFinishCalled = true 77 | os_unfair_lock_unlock(lock) 78 | 79 | isExecuting = false 80 | isFinished = true 81 | } 82 | } 83 | 84 | extension OperationQueue { 85 | /// Adds simple `BlockOperation`. 86 | func add(_ closure: @Sendable @escaping () -> Void) -> BlockOperation { 87 | let operation = BlockOperation(block: closure) 88 | addOperation(operation) 89 | return operation 90 | } 91 | 92 | /// Adds asynchronous operation (`Nuke.Operation`) with the given starter. 93 | func add(_ starter: @escaping Operation.Starter) -> Operation { 94 | let operation = Operation(starter: starter) 95 | addOperation(operation) 96 | return operation 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/NukePerformanceTests/ImageViewPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | import Nuke 7 | import NukeExtensions 8 | 9 | class ImageViewPerformanceTests: XCTestCase { 10 | private let dummyCacheRequest = ImageRequest(url: URL(string: "http://test.com/9999999)")!, processors: [ImageProcessors.Resize(size: CGSize(width: 2, height: 2))]) 11 | 12 | override func setUp() { 13 | super.setUp() 14 | 15 | // Store something in memory cache to avoid going through an optimized empty Dictionary path 16 | ImagePipeline.shared.configuration.imageCache?[dummyCacheRequest] = ImageContainer(image: PlatformImage()) 17 | } 18 | 19 | override func tearDown() { 20 | super.tearDown() 21 | 22 | ImagePipeline.shared.configuration.imageCache?[dummyCacheRequest] = nil 23 | } 24 | 25 | // This is the primary use case that we are optimizing for - loading images 26 | // into target, the API that majoriy of the apps are going to use. 27 | func testImageViewMainThreadPerformance() { 28 | let view = _ImageView() 29 | 30 | let urls = (0..<20_000).map { _ in return URL(string: "http://test.com/1)")! } 31 | 32 | measure { 33 | for url in urls { 34 | NukeExtensions.loadImage(with: url, into: view) 35 | } 36 | } 37 | } 38 | 39 | func testImageViewMainThreadPerformanceCacheHit() { 40 | let view = _ImageView() 41 | 42 | let requests = (0..<50_000).map { _ in ImageRequest(url: URL(string: "http://test.com/1)")!) } 43 | for request in requests { 44 | ImagePipeline.shared.configuration.imageCache?[request] = ImageContainer(image: PlatformImage()) 45 | } 46 | 47 | measure { 48 | for request in requests { 49 | NukeExtensions.loadImage(with: request, into: view) 50 | } 51 | } 52 | } 53 | 54 | func testImageViewMainThreadPerformanceWithProcessor() { 55 | let view = _ImageView() 56 | 57 | let urls = (0..<20_000).map { _ in return URL(string: "http://test.com/1)")! } 58 | 59 | measure { 60 | for url in urls { 61 | let request = ImageRequest(url: url, processors: [ImageProcessors.Resize(size: CGSize(width: 1, height: 1))]) 62 | NukeExtensions.loadImage(with: request, into: view) 63 | } 64 | } 65 | } 66 | 67 | func testImageViewMainThreadPerformanceWithProcessorAndSimilarImageInCache() { 68 | let view = _ImageView() 69 | 70 | let urls = (0..<20_000).map { _ in return URL(string: "http://test.com/9999999)")! } 71 | 72 | measure { 73 | for url in urls { 74 | let request = ImageRequest(url: url, processors: [ImageProcessors.Resize(size: CGSize(width: 1, height: 1))]) 75 | NukeExtensions.loadImage(with: request, into: view) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImageEncoderTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | final class ImageEncoderTests: XCTestCase { 9 | func testEncodeImage() throws { 10 | // Given 11 | let image = Test.image 12 | let encoder = ImageEncoders.Default() 13 | 14 | // When 15 | let data = try XCTUnwrap(encoder.encode(image)) 16 | 17 | // Then 18 | XCTAssertEqual(AssetType(data), .jpeg) 19 | } 20 | 21 | func testEncodeImagePNGOpaque() throws { 22 | // Given 23 | let image = Test.image(named: "fixture", extension: "png") 24 | let encoder = ImageEncoders.Default() 25 | 26 | // When 27 | let data = try XCTUnwrap(encoder.encode(image)) 28 | 29 | // Then 30 | #if os(macOS) 31 | // It seems that on macOS, NSImage created from png has an alpha 32 | // component regardless of whether the input image has it. 33 | XCTAssertEqual(AssetType(data), .png) 34 | #else 35 | XCTAssertEqual(AssetType(data), .jpeg) 36 | #endif 37 | } 38 | 39 | func testEncodeImagePNGTransparent() throws { 40 | // Given 41 | let image = Test.image(named: "swift", extension: "png") 42 | let encoder = ImageEncoders.Default() 43 | 44 | // When 45 | let data = try XCTUnwrap(encoder.encode(image)) 46 | 47 | // Then 48 | XCTAssertEqual(AssetType(data), .png) 49 | } 50 | 51 | func testPrefersHEIF() throws { 52 | // Given 53 | let image = Test.image 54 | var encoder = ImageEncoders.Default() 55 | encoder.isHEIFPreferred = true 56 | 57 | // When 58 | let data = try XCTUnwrap(encoder.encode(image)) 59 | 60 | // Then 61 | XCTAssertNil(AssetType(data)) // TODO: update when HEIF support is added 62 | } 63 | 64 | #if os(iOS) || os(tvOS) || os(visionOS) 65 | 66 | func testEncodeCoreImageBackedImage() throws { 67 | // Given 68 | let image = try ImageProcessors.GaussianBlur().processThrowing(Test.image) 69 | let encoder = ImageEncoders.Default() 70 | 71 | // When 72 | let data = try XCTUnwrap(encoder.encode(image)) 73 | 74 | // Then encoded as PNG because GaussianBlur produces 75 | // images with alpha channel 76 | XCTAssertEqual(AssetType(data), .png) 77 | } 78 | 79 | #endif 80 | 81 | // MARK: - Misc 82 | 83 | func testIsOpaqueWithOpaquePNG() { 84 | let image = Test.image(named: "fixture", extension: "png") 85 | #if os(macOS) 86 | XCTAssertFalse(image.cgImage!.isOpaque) 87 | #else 88 | XCTAssertTrue(image.cgImage!.isOpaque) 89 | #endif 90 | } 91 | 92 | func testIsOpaqueWithTransparentPNG() { 93 | let image = Test.image(named: "swift", extension: "png") 94 | XCTAssertFalse(image.cgImage!.isOpaque) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/NukePerformanceTests/ImageProcessingPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | import Nuke 7 | 8 | class ImageProcessingPerformanceTests: XCTestCase { 9 | func testCreatingProcessorIdentifiers() { 10 | let decompressor = ImageProcessors.Resize(size: CGSize(width: 1, height: 1), contentMode: .aspectFill, upscale: false) 11 | 12 | measure { 13 | for _ in 0..<25_000 { 14 | _ = decompressor.identifier 15 | } 16 | } 17 | } 18 | 19 | func testComparingTwoProcessorCompositions() { 20 | let lhs = ImageProcessors.Composition([MockImageProcessor(id: "123"), ImageProcessors.Resize(size: CGSize(width: 1, height: 1), contentMode: .aspectFill, upscale: false)]) 21 | let rhs = ImageProcessors.Composition([MockImageProcessor(id: "124"), ImageProcessors.Resize(size: CGSize(width: 1, height: 1), contentMode: .aspectFill, upscale: false)]) 22 | 23 | measure { 24 | for _ in 0..<25_000 { 25 | if lhs.hashableIdentifier == rhs.hashableIdentifier { 26 | // do nothing 27 | } 28 | } 29 | } 30 | } 31 | 32 | func testImageDecoding() { 33 | let decoder = ImageDecoders.Default() 34 | 35 | let data = Test.data 36 | measure { 37 | for _ in 0..<1_000 { 38 | _ = try? decoder.decode(data) 39 | } 40 | } 41 | } 42 | 43 | // MARK: Creating Thumbnails 44 | 45 | func testResizeImage() throws { 46 | let image = try XCTUnwrap(makeHighResolutionImage()) 47 | let processor = ImageProcessors.Resize(size: CGSize(width: 64, height: 64), unit: .pixels) 48 | 49 | measure { 50 | for _ in 0..<10 { 51 | _ = processor.process(image) 52 | } 53 | } 54 | } 55 | 56 | func testCreateThumbnail() throws { 57 | let image = try XCTUnwrap(makeHighResolutionImage()) 58 | let data = try XCTUnwrap(ImageEncoders.ImageIO(type: .jpeg).encode(image)) 59 | let options = ImageRequest.ThumbnailOptions(size: CGSize(width: 64, height: 64), unit: .pixels) 60 | 61 | measure { 62 | for _ in 0..<10 { 63 | _ = options.makeThumbnail(with: data) 64 | } 65 | } 66 | } 67 | 68 | // Should be roughly identical to the flexible target size. 69 | func testCreateThumbnailStaticSize() throws { 70 | let image = try XCTUnwrap(makeHighResolutionImage()) 71 | let data = try XCTUnwrap(ImageEncoders.ImageIO(type: .jpeg).encode(image)) 72 | let options = ImageRequest.ThumbnailOptions(maxPixelSize: 64) 73 | 74 | measure { 75 | for _ in 0..<10 { 76 | _ = options.makeThumbnail(with: data) 77 | } 78 | } 79 | } 80 | } 81 | 82 | private func makeHighResolutionImage() -> PlatformImage? { 83 | ImageProcessors.Resize(width: 4000, unit: .pixels, upscale: true).process(Test.image) 84 | } 85 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/NukeUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Tests/NukeTests/ImagePipelineTests/ImagePipelineConfigurationTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | class ImagePipelineConfigurationTests: XCTestCase { 9 | 10 | func testImageIsLoadedWithRateLimiterDisabled() { 11 | // Given 12 | let dataLoader = MockDataLoader() 13 | let pipeline = ImagePipeline { 14 | $0.dataLoader = dataLoader 15 | $0.imageCache = nil 16 | 17 | $0.isRateLimiterEnabled = false 18 | } 19 | 20 | // When/Then 21 | expect(pipeline).toLoadImage(with: Test.request) 22 | wait() 23 | } 24 | 25 | // MARK: DataCache 26 | 27 | func testWithDataCache() { 28 | let pipeline = ImagePipeline(configuration: .withDataCache) 29 | XCTAssertNotNil(pipeline.configuration.dataCache) 30 | } 31 | 32 | // MARK: Changing Callback Queue 33 | 34 | func testChangingCallbackQueueLoadImage() { 35 | // Given 36 | let queue = DispatchQueue(label: "testChangingCallbackQueue") 37 | let queueKey = DispatchSpecificKey() 38 | queue.setSpecific(key: queueKey, value: ()) 39 | 40 | let dataLoader = MockDataLoader() 41 | let pipeline = ImagePipeline { 42 | $0.dataLoader = dataLoader 43 | $0.imageCache = nil 44 | 45 | $0._callbackQueue = queue 46 | } 47 | 48 | // When/Then 49 | let expectation = self.expectation(description: "Image Loaded") 50 | pipeline.loadImage(with: Test.request, progress: { _, _, _ in 51 | XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) 52 | }, completion: { _ in 53 | XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) 54 | expectation.fulfill() 55 | }) 56 | wait() 57 | } 58 | 59 | func testChangingCallbackQueueLoadData() { 60 | // Given 61 | let queue = DispatchQueue(label: "testChangingCallbackQueue") 62 | let queueKey = DispatchSpecificKey() 63 | queue.setSpecific(key: queueKey, value: ()) 64 | 65 | let dataLoader = MockDataLoader() 66 | let pipeline = ImagePipeline { 67 | $0.dataLoader = dataLoader 68 | $0.imageCache = nil 69 | 70 | $0._callbackQueue = queue 71 | } 72 | 73 | // When/Then 74 | let expectation = self.expectation(description: "Image data Loaded") 75 | pipeline.loadData(with: Test.request, progress: { _, _ in 76 | XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) 77 | }, completion: { _ in 78 | XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) 79 | expectation.fulfill() 80 | }) 81 | wait() 82 | } 83 | 84 | func testEnablingSignposts() { 85 | ImagePipeline.Configuration.isSignpostLoggingEnabled = false // Just padding 86 | ImagePipeline.Configuration.isSignpostLoggingEnabled = true 87 | ImagePipeline.Configuration.isSignpostLoggingEnabled = false 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/NukeExtensions.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Sources/Nuke/Internal/ImagePublisher.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Combine 7 | 8 | /// A publisher that starts a new `ImageTask` when a subscriber is added. 9 | /// 10 | /// If the requested image is available in the memory cache, the value is 11 | /// delivered immediately. When the subscription is cancelled, the task also 12 | /// gets cancelled. 13 | /// 14 | /// - note: In case the pipeline has `isProgressiveDecodingEnabled` option enabled 15 | /// and the image being downloaded supports progressive decoding, the publisher 16 | /// might emit more than a single value. 17 | struct ImagePublisher: Publisher, Sendable { 18 | typealias Output = ImageResponse 19 | typealias Failure = ImagePipeline.Error 20 | 21 | let request: ImageRequest 22 | let pipeline: ImagePipeline 23 | 24 | func receive(subscriber: S) where S: Subscriber, S: Sendable, Failure == S.Failure, Output == S.Input { 25 | let subscription = ImageSubscription( 26 | request: self.request, 27 | pipeline: self.pipeline, 28 | subscriber: subscriber 29 | ) 30 | subscriber.receive(subscription: subscription) 31 | } 32 | } 33 | 34 | private final class ImageSubscription: Subscription where S: Subscriber, S: Sendable, S.Input == ImageResponse, S.Failure == ImagePipeline.Error { 35 | private var task: ImageTask? 36 | private let subscriber: S? 37 | private let request: ImageRequest 38 | private let pipeline: ImagePipeline 39 | private var isStarted = false 40 | 41 | init(request: ImageRequest, pipeline: ImagePipeline, subscriber: S) { 42 | self.pipeline = pipeline 43 | self.request = request 44 | self.subscriber = subscriber 45 | 46 | } 47 | 48 | func request(_ demand: Subscribers.Demand) { 49 | guard demand > 0 else { return } 50 | guard let subscriber else { return } 51 | 52 | if let image = pipeline.cache[request] { 53 | _ = subscriber.receive(ImageResponse(container: image, request: request, cacheType: .memory)) 54 | 55 | if !image.isPreview { 56 | subscriber.receive(completion: .finished) 57 | return 58 | } 59 | } 60 | 61 | task = pipeline.loadImage( 62 | with: request, 63 | progress: { response, _, _ in 64 | if let response { 65 | // Send progressively decoded image (if enabled and if any) 66 | _ = subscriber.receive(response) 67 | } 68 | }, 69 | completion: { result in 70 | switch result { 71 | case let .success(response): 72 | _ = subscriber.receive(response) 73 | subscriber.receive(completion: .finished) 74 | case let .failure(error): 75 | subscriber.receive(completion: .failure(error)) 76 | } 77 | } 78 | ) 79 | } 80 | 81 | func cancel() { 82 | task?.cancel() 83 | task = nil 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Documentation/NukeUI.docc/Extensions/LazyImage-Extensions.md: -------------------------------------------------------------------------------- 1 | # ``NukeUI/LazyImage`` 2 | 3 | ## Using LazyImage 4 | 5 | The view is instantiated with a [`URL`](https://developer.apple.com/documentation/foundation/url) or an ``ImageRequest``. 6 | 7 | ```swift 8 | struct ContainerView: View { 9 | var body: some View { 10 | LazyImage(url: URL(string: "https://example.com/image.jpeg")) 11 | } 12 | } 13 | ``` 14 | 15 | The view is called "lazy" because it loads the image only when it appears on the screen. And when it disappears, the current request automatically gets canceled. When the view reappears, the download picks up where it left off, thanks to [resumable downloads](https://kean.blog/post/resumable-downloads). 16 | 17 | > Tip: To change the `onDisappear` behavior, use ``LazyImage/onDisappear(_:)``. 18 | 19 | Until the image loads, the view displays a standard placeholder that fills the available space, just like [AsyncImage](https://developer.apple.com/documentation/SwiftUI/AsyncImage) does. After the load completes successfully, the view updates to display the image. 20 | 21 | ![nukeui demo](nukeui-preview) 22 | 23 | To gain more control over the loading process and how the image is displayed, ``LazyImage/init(url:transaction:content:)``, which takes a `content` closure that receives a ``LazyImageState``. 24 | 25 | ```swift 26 | LazyImage(url: URL(string: "https://example.com/image.jpeg")) { state in 27 | if let image = state.image { 28 | image.resizable().aspectRatio(contentMode: .fill) 29 | } else if state.error != nil { 30 | Color.red // Indicates an error 31 | } else { 32 | Color.blue // Acts as a placeholder 33 | } 34 | } 35 | ``` 36 | 37 | > Important: You can’t apply image-specific modifiers, like `resizable(capInsets:resizingMode:)`, directly to a `LazyImage`. Instead, apply them to the `Image` instance that your content closure gets when defining the view’s appearance. 38 | 39 | When the image is loaded, it is displayed with no animation, which is a recommended option. If you add an animation, it's automatically applied when the image is downloaded, but not when it's retrieved from the memory cache. 40 | 41 | ```swift 42 | LazyImage(url: URL(string: "https://example.com/image.jpeg")) 43 | .animation(.default) 44 | ``` 45 | 46 | `LazyImage` can be instantiated with an `ImageRequest` or configured using convenience modifiers. 47 | 48 | ```swift 49 | LazyImage(request: ImageRequest( 50 | url: URL(string: "https://example.com/image.jpeg"), 51 | processors: [.resize(width: 44)] 52 | )) 53 | 54 | LazyImage(url: URL(string: "https://example.com/image.jpeg")) 55 | .processors([.resize(width: 44)]) 56 | .priority(.high) 57 | .pipeline(customPipeline) 58 | ``` 59 | 60 | > Tip: ``LazyImage`` is built on top of ``FetchImage``. If you want even more control, you can use it directly instead. 61 | 62 | ## Topics 63 | 64 | ### Initializers 65 | 66 | - ``init(url:)`` 67 | - ``init(request:)`` 68 | - ``init(url:transaction:content:)`` 69 | - ``init(request:transaction:content:)`` 70 | 71 | ### Cancellation 72 | 73 | - ``onDisappear(_:)`` 74 | 75 | ### Request Options 76 | 77 | - ``priority(_:)`` 78 | - ``processors(_:)`` 79 | - ``pipeline(_:)`` 80 | -------------------------------------------------------------------------------- /Sources/Nuke/Pipeline/ImagePipeline+Error.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | extension ImagePipeline { 8 | /// Represents all possible image pipeline errors. 9 | public enum Error: Swift.Error, CustomStringConvertible, @unchecked Sendable { 10 | /// Returned if data not cached and ``ImageRequest/Options-swift.struct/returnCacheDataDontLoad`` option is specified. 11 | case dataMissingInCache 12 | /// Data loader failed to load image data with a wrapped error. 13 | case dataLoadingFailed(error: Swift.Error) 14 | /// Data loader returned empty data. 15 | case dataIsEmpty 16 | /// No decoder registered for the given data. 17 | /// 18 | /// This error can only be thrown if the pipeline has custom decoders. 19 | /// By default, the pipeline uses ``ImageDecoders/Default`` as a catch-all. 20 | case decoderNotRegistered(context: ImageDecodingContext) 21 | /// Decoder failed to produce a final image. 22 | case decodingFailed(decoder: any ImageDecoding, context: ImageDecodingContext, error: Swift.Error) 23 | /// Processor failed to produce a final image. 24 | case processingFailed(processor: any ImageProcessing, context: ImageProcessingContext, error: Swift.Error) 25 | /// Load image method was called with no image request. 26 | case imageRequestMissing 27 | /// Image pipeline is invalidated and no requests can be made. 28 | case pipelineInvalidated 29 | } 30 | } 31 | 32 | extension ImagePipeline.Error { 33 | /// Returns underlying data loading error. 34 | public var dataLoadingError: Swift.Error? { 35 | switch self { 36 | case .dataLoadingFailed(let error): 37 | return error 38 | default: 39 | return nil 40 | } 41 | } 42 | 43 | public var description: String { 44 | switch self { 45 | case .dataMissingInCache: 46 | return "Failed to load data from cache and download is disabled." 47 | case let .dataLoadingFailed(error): 48 | return "Failed to load image data. Underlying error: \(error)." 49 | case .dataIsEmpty: 50 | return "Data loader returned empty data." 51 | case .decoderNotRegistered: 52 | return "No decoders registered for the downloaded data." 53 | case let .decodingFailed(decoder, _, error): 54 | let underlying = error is ImageDecodingError ? "" : " Underlying error: \(error)." 55 | return "Failed to decode image data using decoder \(decoder).\(underlying)" 56 | case let .processingFailed(processor, _, error): 57 | let underlying = error is ImageProcessingError ? "" : " Underlying error: \(error)." 58 | return "Failed to process the image using processor \(processor).\(underlying)" 59 | case .imageRequestMissing: 60 | return "Load image method was called with no image request or no URL." 61 | case .pipelineInvalidated: 62 | return "Image pipeline is invalidated and no requests can be made." 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Documentation/Migrations/Nuke 11 Migration Guide.md: -------------------------------------------------------------------------------- 1 | # Nuke 11 Migration Guide 2 | 3 | This guide eases the transition of the existing apps that use Nuke 10.x to the latest version of the framework. 4 | 5 | > To learn about the new features in Nuke 11, see the [release notes](https://github.com/kean/Nuke/releases/tag/11.0.0). 6 | 7 | ## Minimum Requirements 8 | 9 | - iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0 10 | - Xcode 13.3 11 | - Swift 5.6 12 | 13 | ## Error Reporting Improvements 14 | 15 | If you are implementing custom image decoders or processors, their primary APIs are now throwing to allow the users to provide more information in case something goes wrong: 16 | 17 | ```swift 18 | // Before (Nuke 10) 19 | public protocol ImageDecoding { 20 | func decode(_ data: Data) -> ImageContainer? 21 | } 22 | 23 | // After (Nuke 11) 24 | public protocol ImageDecoding { 25 | func decode(_ data: Data) throws -> ImageContainer 26 | } 27 | ``` 28 | 29 | > You can use a new `ImageDecodingContext.unknown` in case there is nothing to report. 30 | 31 | ```swift 32 | // Before (Nuke 10) 33 | public protocol ImageProcessing { 34 | // This method has no changes. 35 | func process(_ image: PlatformImage) -> PlatformImage? 36 | 37 | func process(_ container: ImageContainer, context: ImageProcessingContext) -> ImageContainer? 38 | } 39 | 40 | // After (Nuke 11) 41 | public protocol ImageProcessing { 42 | // This method has no changes. 43 | func process(_ image: PlatformImage) -> PlatformImage? 44 | 45 | // This is now throwing. 46 | func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer 47 | } 48 | ``` 49 | 50 | ## ImageProcessing and Hashable 51 | 52 | If you are implementing custom image processors `ImageProcessing` that implement `hashableIdentifier` and return self, you can remove the `hashableIdentifier` implementation and use the one provided by default. 53 | 54 | ```swift 55 | // Before (Nuke 10) 56 | extension ImageProcessors { 57 | /// Scales an image to a specified size. 58 | public struct Resize: ImageProcessing, Hashable { 59 | private let size: CGSize 60 | 61 | var hashableIdentiifer: AnyHashable { self } 62 | } 63 | } 64 | 65 | // After (Nuke 11) 66 | extension ImageProcessors { 67 | /// Scales an image to a specified size. 68 | public struct Resize: ImageProcessing, Hashable { 69 | private let size: CGSize 70 | } 71 | } 72 | ``` 73 | 74 | ## Invalidation 75 | 76 | If you invalidate the pipeline, any new requests will immediately fail with `ImagePipeline/Error/pipelineInvalidated` error. 77 | 78 | ## ImageRequestConvertible 79 | 80 | `ImageRequestConvertible` was originally introduced in [Nuke 9.2](https://github.com/kean/Nuke/releases/tag/9.2.0) to reduce number of `loadImage(:)` APIs in code completion, but it's no longer an issue with the new async/await APIs. 81 | 82 | `ImageRequestConvertible` is soft-deprecated in Nuke 11. The other soft-deprecated APIs, such as a closure-based `ImagePipeline/loadImage(:)` will continue working with it. The new APIs, such as async/await `ImagePipeline/image(for:)` will work with `URL` and `ImageRequest` which is better for discoverability and performance. 83 | 84 | If you are using `ImageRequestConvertible` in your code, consider removing it now. But it won't be officially deprecated until the next major release. 85 | -------------------------------------------------------------------------------- /Sources/Nuke/Internal/ImageRequestKeys.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// Uniquely identifies a cache processed image. 8 | final class MemoryCacheKey: Hashable, Sendable { 9 | // Using a reference type turned out to be significantly faster 10 | private let imageId: String? 11 | private let scale: Float 12 | private let thumbnail: ImageRequest.ThumbnailOptions? 13 | private let processors: [any ImageProcessing] 14 | 15 | init(_ request: ImageRequest) { 16 | self.imageId = request.preferredImageId 17 | self.scale = request.scale ?? 1 18 | self.thumbnail = request.thumbnail 19 | self.processors = request.processors 20 | } 21 | 22 | func hash(into hasher: inout Hasher) { 23 | hasher.combine(imageId) 24 | hasher.combine(scale) 25 | hasher.combine(thumbnail) 26 | hasher.combine(processors.count) 27 | } 28 | 29 | static func == (lhs: MemoryCacheKey, rhs: MemoryCacheKey) -> Bool { 30 | lhs.imageId == rhs.imageId && lhs.scale == rhs.scale && lhs.thumbnail == rhs.thumbnail && lhs.processors == rhs.processors 31 | } 32 | } 33 | 34 | // MARK: - Identifying Tasks 35 | 36 | /// Uniquely identifies a task of retrieving the processed image. 37 | final class TaskLoadImageKey: Hashable, Sendable { 38 | private let loadKey: TaskFetchOriginalImageKey 39 | private let options: ImageRequest.Options 40 | private let processors: [any ImageProcessing] 41 | 42 | init(_ request: ImageRequest) { 43 | self.loadKey = TaskFetchOriginalImageKey(request) 44 | self.options = request.options 45 | self.processors = request.processors 46 | } 47 | 48 | func hash(into hasher: inout Hasher) { 49 | hasher.combine(loadKey.hashValue) 50 | hasher.combine(options.hashValue) 51 | hasher.combine(processors.count) 52 | } 53 | 54 | static func == (lhs: TaskLoadImageKey, rhs: TaskLoadImageKey) -> Bool { 55 | lhs.loadKey == rhs.loadKey && lhs.options == rhs.options && lhs.processors == rhs.processors 56 | } 57 | } 58 | 59 | /// Uniquely identifies a task of retrieving the original image. 60 | struct TaskFetchOriginalImageKey: Hashable { 61 | private let dataLoadKey: TaskFetchOriginalDataKey 62 | private let scale: Float 63 | private let thumbnail: ImageRequest.ThumbnailOptions? 64 | 65 | init(_ request: ImageRequest) { 66 | self.dataLoadKey = TaskFetchOriginalDataKey(request) 67 | self.scale = request.scale ?? 1 68 | self.thumbnail = request.thumbnail 69 | } 70 | } 71 | 72 | /// Uniquely identifies a task of retrieving the original image data. 73 | struct TaskFetchOriginalDataKey: Hashable { 74 | private let imageId: String? 75 | private let cachePolicy: URLRequest.CachePolicy 76 | private let allowsCellularAccess: Bool 77 | 78 | init(_ request: ImageRequest) { 79 | self.imageId = request.imageId 80 | switch request.resource { 81 | case .url, .publisher: 82 | self.cachePolicy = .useProtocolCachePolicy 83 | self.allowsCellularAccess = true 84 | case let .urlRequest(urlRequest): 85 | self.cachePolicy = urlRequest.cachePolicy 86 | self.allowsCellularAccess = urlRequest.allowsCellularAccess 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Documentation/Migrations/Nuke 9 Migration Guide.md: -------------------------------------------------------------------------------- 1 | # Nuke 9 Migration Guide 2 | 3 | This guide is provided in order to ease the transition of existing applications using Nuke 8.x to the latest version, as well as explain the design and structure of new and changed functionality. 4 | 5 | > To learn about the new features in Nuke 9 see the [release notes](https://github.com/kean/Nuke/releases/tag/9.0.0). 6 | 7 | ## Minimum Requirements 8 | 9 | - iOS 11.0, tvOS 11.0, macOS 10.13, watchOS 4.0 10 | - Xcode 11.0 11 | - Swift 5.1 12 | 13 | ## Overview 14 | 15 | Nuke 9 contains a ton of new features, refinements, and performance improvements. There are some breaking changes and deprecated which the compiler is going to guide you through as you update. 16 | 17 | ## ImageProcessing 18 | 19 | If you have custom image processors (`ImageProcessing` protocol), update `process(image:)` method to use the new signature. There are now two levels of image processing APIs: the basic and the advanced one. Please implement the one that best fits your needs. 20 | 21 | ```swift 22 | // Nuke 8 23 | func process(_ image: PlatformImage, context: ImageProcessingContext?) -> PlatformImage? 24 | 25 | // Nuke 9 26 | func process(_ image: UIImage) -> UIImage? // NSImage on macOS 27 | // Optional 28 | func process(_ image container: ImageContainer, context: ImageProcessingContext) -> ImageContainer? 29 | ``` 30 | 31 | ## ImageDecoding 32 | 33 | ```swift 34 | // Nuke 8 35 | public protocol ImageDecoding { 36 | func decode(data: Data, isFinal: Bool) -> PlatformImage? 37 | } 38 | 39 | // Nuke 9 40 | public protocol ImageDecoding { 41 | func decode(_ data: Data) -> ImageContainer? 42 | // Optional 43 | func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? 44 | } 45 | 46 | public protocol ImageDecoderRegistering: ImageDecoding { 47 | init?(data: Data, context: ImageDecodingContext) 48 | // Optional 49 | init?(partiallyDownloadedData data: Data, context: ImageDecodingContext) 50 | } 51 | ``` 52 | 53 | ## ImageEncoding 54 | 55 | If you have custom encoders (`ImageEncoding` protocol), update `encode(image:)` method to use the new signature. There are now two levels of image encoding APIs: the basic and the advanced one. Please implement the one that best fits your needs. 56 | 57 | ```swift 58 | // Nuke 8 59 | public protocol ImageEncoding { 60 | func encode(image: PlatformImage) -> Data? 61 | } 62 | 63 | // Nuke 9 64 | public protocol ImageEncoding { 65 | func encode(_ image: PlatformImage) -> Data? 66 | // Optional 67 | func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data? 68 | } 69 | ``` 70 | 71 | ## ImageCaching 72 | 73 | `ImageCaching` was updated to use `ImageContainer` type. Individual methods were replaced with a subscript. 74 | 75 | ```swift 76 | // Nuke 8 77 | public protocol ImageCaching: AnyObject { 78 | /// Returns the `ImageResponse` stored in the cache with the given request. 79 | func cachedResponse(for request: ImageRequest) -> ImageResponse? 80 | 81 | /// Stores the given `ImageResponse` in the cache using the given request. 82 | func storeResponse(_ response: ImageResponse, for request: ImageRequest) 83 | 84 | /// Remove the response for the given request. 85 | func removeResponse(for request: ImageRequest) 86 | } 87 | 88 | // Nuke 9 89 | public protocol ImageCaching: AnyObject { 90 | subscript(request: ImageRequest) -> ImageContainer? 91 | } 92 | -------------------------------------------------------------------------------- /Tests/MockImageProcessor.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import Nuke 7 | 8 | extension PlatformImage { 9 | var nk_test_processorIDs: [String] { 10 | get { 11 | return (objc_getAssociatedObject(self, AssociatedKeys.processorId) as? [String]) ?? [String]() 12 | } 13 | set { 14 | objc_setAssociatedObject(self, AssociatedKeys.processorId, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 15 | } 16 | } 17 | } 18 | 19 | private enum AssociatedKeys { 20 | #if swift(>=5.10) 21 | // Safe because it's never mutated. 22 | nonisolated(unsafe) static let processorId = malloc(1)! 23 | #else 24 | static let processorId = malloc(1)! 25 | #endif 26 | } 27 | 28 | // MARK: - MockImageProcessor 29 | 30 | final class MockImageProcessor: ImageProcessing, CustomStringConvertible { 31 | let identifier: String 32 | 33 | init(id: String) { 34 | self.identifier = id 35 | } 36 | 37 | func process(_ image: PlatformImage) -> PlatformImage? { 38 | var processorIDs: [String] = image.nk_test_processorIDs 39 | #if os(macOS) 40 | let processedImage = image.copy() as! PlatformImage 41 | #else 42 | guard let copy = image.cgImage?.copy() else { 43 | return image 44 | } 45 | let processedImage = PlatformImage(cgImage: copy) 46 | #endif 47 | processorIDs.append(identifier) 48 | processedImage.nk_test_processorIDs = processorIDs 49 | return processedImage 50 | } 51 | 52 | var description: String { 53 | "MockImageProcessor(id: \(identifier))" 54 | } 55 | } 56 | 57 | // MARK: - MockFailingProcessor 58 | 59 | final class MockFailingProcessor: ImageProcessing { 60 | func process(_ image: PlatformImage) -> PlatformImage? { 61 | nil 62 | } 63 | 64 | var identifier: String { 65 | "MockFailingProcessor" 66 | } 67 | } 68 | 69 | struct MockError: Error, Equatable { 70 | let description: String 71 | } 72 | 73 | // MARK: - MockEmptyImageProcessor 74 | 75 | final class MockEmptyImageProcessor: ImageProcessing { 76 | let identifier = "MockEmptyImageProcessor" 77 | 78 | func process(_ image: PlatformImage) -> PlatformImage? { 79 | image 80 | } 81 | 82 | static func == (lhs: MockEmptyImageProcessor, rhs: MockEmptyImageProcessor) -> Bool { 83 | true 84 | } 85 | } 86 | 87 | // MARK: - MockProcessorFactory 88 | 89 | /// Counts number of applied processors 90 | final class MockProcessorFactory { 91 | var numberOfProcessorsApplied: Int = 0 92 | let lock = NSLock() 93 | 94 | private final class Processor: ImageProcessing, @unchecked Sendable { 95 | var identifier: String { processor.identifier } 96 | var factory: MockProcessorFactory! 97 | let processor: MockImageProcessor 98 | 99 | init(id: String) { 100 | self.processor = MockImageProcessor(id: id) 101 | } 102 | 103 | func process(_ image: PlatformImage) -> PlatformImage? { 104 | factory.lock.lock() 105 | factory.numberOfProcessorsApplied += 1 106 | factory.lock.unlock() 107 | return processor.process(image) 108 | } 109 | } 110 | 111 | func make(id: String) -> any ImageProcessing { 112 | let processor = Processor(id: id) 113 | processor.factory = self 114 | return processor 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/NukeTests/RateLimiterTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | class RateLimiterTests: XCTestCase { 9 | var queue: DispatchQueue! 10 | var queueKey: DispatchSpecificKey! 11 | var rateLimiter: RateLimiter! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | queue = DispatchQueue(label: "com.github.kean.rate-limiter-tests") 17 | 18 | queueKey = DispatchSpecificKey() 19 | queue.setSpecific(key: queueKey, value: ()) 20 | 21 | // Note: we set very short rate to avoid bucket form being refilled too quickly 22 | rateLimiter = RateLimiter(queue: queue, rate: 10, burst: 2) 23 | } 24 | 25 | func testThatBurstIsExecutedimmediately() { 26 | // Given 27 | var isExecuted = Array(repeating: false, count: 4) 28 | 29 | // When 30 | for i in isExecuted.indices { 31 | queue.sync { 32 | rateLimiter.execute { 33 | isExecuted[i] = true 34 | return true 35 | } 36 | } 37 | } 38 | 39 | // Then 40 | XCTAssertEqual(isExecuted, [true, true, false, false], "Expect first 2 items to be executed immediately") 41 | } 42 | 43 | func testThatNotExecutedItemDoesntExtractFromBucket() { 44 | // Given 45 | var isExecuted = Array(repeating: false, count: 4) 46 | 47 | // When 48 | for i in isExecuted.indices { 49 | queue.sync { 50 | rateLimiter.execute { 51 | isExecuted[i] = true 52 | return i != 1 // important! 53 | } 54 | } 55 | } 56 | 57 | // Then 58 | XCTAssertEqual(isExecuted, [true, true, true, false], "Expect first 2 items to be executed immediately") 59 | } 60 | 61 | func testOverflow() { 62 | // Given 63 | var isExecuted = Array(repeating: false, count: 3) 64 | 65 | // When 66 | let expectation = self.expectation(description: "All work executed") 67 | expectation.expectedFulfillmentCount = isExecuted.count 68 | 69 | queue.sync { 70 | for i in isExecuted.indices { 71 | rateLimiter.execute { 72 | isExecuted[i] = true 73 | expectation.fulfill() 74 | return true 75 | } 76 | } 77 | } 78 | 79 | // When time is passed 80 | wait() 81 | 82 | // Then 83 | queue.sync { 84 | XCTAssertEqual(isExecuted, [true, true, true], "Expect 3rd item to be executed after a short delay") 85 | } 86 | } 87 | 88 | func testOverflowItemsExecutedOnSpecificQueue() { 89 | // Given 90 | let isExecuted = Array(repeating: false, count: 3) 91 | 92 | let expectation = self.expectation(description: "All work executed") 93 | expectation.expectedFulfillmentCount = isExecuted.count 94 | 95 | queue.sync { 96 | for _ in isExecuted.indices { 97 | rateLimiter.execute { 98 | expectation.fulfill() 99 | // Then delayed task also executed on queue 100 | XCTAssertNotNil(DispatchQueue.getSpecific(key: self.queueKey)) 101 | return true 102 | } 103 | } 104 | } 105 | wait() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Nuke.xcodeproj/xcshareddata/xcschemes/Nuke Tests Host.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Tests/NukePerformanceTests/ImagePipelinePerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | import Nuke 7 | 8 | class ImagePipelinePerfomanceTests: XCTestCase { 9 | /// A very broad test that establishes how long in general it takes to load 10 | /// data, decode, and decomperss 50+ images. It's very useful to get a 11 | /// broad picture about how loader options affect perofmance. 12 | func testLoaderOverallPerformance() { 13 | let pipeline = makePipeline() 14 | 15 | let requests = (0...5000).map { ImageRequest(url: URL(string: "http://test.com/\($0)")) } 16 | let callbackQueue = DispatchQueue(label: "testLoaderOverallPerformance") 17 | measure { 18 | var finished: Int = 0 19 | let semaphore = DispatchSemaphore(value: 0) 20 | for request in requests { 21 | pipeline.loadImage(with: request, queue: callbackQueue, progress: nil) { _ in 22 | finished += 1 23 | if finished == requests.count { 24 | semaphore.signal() 25 | } 26 | } 27 | } 28 | semaphore.wait() 29 | } 30 | } 31 | 32 | func testAsyncAwaitPerformance() { 33 | let pipeline = makePipeline() 34 | 35 | let requests = (0...5000).map { ImageRequest(url: URL(string: "http://test.com/\($0)")) } 36 | 37 | measure { 38 | let semaphore = DispatchSemaphore(value: 0) 39 | Task.detached { 40 | await withTaskGroup(of: Void.self) { group in 41 | for request in requests { 42 | group.addTask { 43 | _ = try? await pipeline.image(for: request) 44 | } 45 | } 46 | } 47 | semaphore.signal() 48 | } 49 | semaphore.wait() 50 | } 51 | } 52 | 53 | func testAsyncImageTaskPerformance() { 54 | let pipeline = makePipeline() 55 | 56 | let requests = (0...5000).map { ImageRequest(url: URL(string: "http://test.com/\($0)")) } 57 | 58 | measure { 59 | let semaphore = DispatchSemaphore(value: 0) 60 | Task.detached { 61 | await withTaskGroup(of: Void.self) { group in 62 | for request in requests { 63 | group.addTask { 64 | _ = try? await pipeline.imageTask(with: request).image 65 | } 66 | } 67 | } 68 | semaphore.signal() 69 | } 70 | semaphore.wait() 71 | } 72 | } 73 | } 74 | 75 | private func makePipeline() -> ImagePipeline { 76 | struct MockDecoder: ImageDecoding { 77 | static let container = ImageContainer(image: Test.image) 78 | 79 | func decode(_ data: Data) throws -> ImageContainer { 80 | MockDecoder.container 81 | } 82 | } 83 | 84 | let pipeline = ImagePipeline { 85 | $0.imageCache = nil 86 | 87 | $0.dataLoader = MockDataLoader() 88 | 89 | $0.isDecompressionEnabled = false 90 | 91 | // This must be off for this test, because rate limiter is optimized for 92 | // the actual loading in the apps and not the synthetic tests like this. 93 | $0.isRateLimiterEnabled = false 94 | 95 | // Remove decoding from the equation 96 | $0.makeImageDecoder = { _ in ImageDecoders.Empty() } 97 | } 98 | 99 | return pipeline 100 | } 101 | -------------------------------------------------------------------------------- /Tests/ImagePipelineObserver.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import XCTest 6 | @testable import Nuke 7 | 8 | final class ImagePipelineObserver: ImagePipelineDelegate, @unchecked Sendable { 9 | var startedTaskCount = 0 10 | var cancelledTaskCount = 0 11 | var completedTaskCount = 0 12 | 13 | static let didStartTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidStartTask") 14 | static let didCancelTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidCancelTask") 15 | static let didCompleteTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidFinishTask") 16 | 17 | static let taskKey = "taskKey" 18 | static let resultKey = "resultKey" 19 | 20 | var events = [ImageTaskEvent]() 21 | 22 | var onTaskCreated: ((ImageTask) -> Void)? 23 | 24 | private let lock = NSLock() 25 | 26 | private func append(_ event: ImageTaskEvent) { 27 | lock.lock() 28 | events.append(event) 29 | lock.unlock() 30 | } 31 | 32 | func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) { 33 | onTaskCreated?(task) 34 | append(.created) 35 | } 36 | 37 | func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) { 38 | startedTaskCount += 1 39 | NotificationCenter.default.post(name: ImagePipelineObserver.didStartTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task]) 40 | append(.started) 41 | } 42 | 43 | func imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline) { 44 | append(.cancelled) 45 | 46 | cancelledTaskCount += 1 47 | NotificationCenter.default.post(name: ImagePipelineObserver.didCancelTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task]) 48 | } 49 | 50 | func imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline) { 51 | append(.progressUpdated(completedUnitCount: progress.completed, totalUnitCount: progress.total)) 52 | } 53 | 54 | func imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline) { 55 | append(.intermediateResponseReceived(response: response)) 56 | } 57 | 58 | func imageTask(_ task: ImageTask, didCompleteWithResult result: Result, pipeline: ImagePipeline) { 59 | append(.completed(result: result)) 60 | 61 | completedTaskCount += 1 62 | NotificationCenter.default.post(name: ImagePipelineObserver.didCompleteTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task, ImagePipelineObserver.resultKey: result]) 63 | } 64 | } 65 | 66 | enum ImageTaskEvent: Equatable { 67 | case created 68 | case started 69 | case cancelled 70 | case intermediateResponseReceived(response: ImageResponse) 71 | case progressUpdated(completedUnitCount: Int64, totalUnitCount: Int64) 72 | case completed(result: Result) 73 | 74 | static func == (lhs: ImageTaskEvent, rhs: ImageTaskEvent) -> Bool { 75 | switch (lhs, rhs) { 76 | case (.created, .created): return true 77 | case (.started, .started): return true 78 | case (.cancelled, .cancelled): return true 79 | case let (.intermediateResponseReceived(lhs), .intermediateResponseReceived(rhs)): return lhs == rhs 80 | case let (.progressUpdated(lhsTotal, lhsCompleted), .progressUpdated(rhsTotal, rhsCompleted)): 81 | return (lhsTotal, lhsCompleted) == (rhsTotal, rhsCompleted) 82 | case let (.completed(lhs), .completed(rhs)): return lhs == rhs 83 | default: return false 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Nuke/Decoding/AssetType.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | /// A uniform type identifier (UTI). 8 | public struct AssetType: ExpressibleByStringLiteral, Hashable, Sendable { 9 | public let rawValue: String 10 | 11 | public init(rawValue: String) { 12 | self.rawValue = rawValue 13 | } 14 | 15 | public init(stringLiteral value: String) { 16 | self.rawValue = value 17 | } 18 | 19 | public static let png: AssetType = "public.png" 20 | public static let jpeg: AssetType = "public.jpeg" 21 | public static let gif: AssetType = "com.compuserve.gif" 22 | /// HEIF (High Efficiency Image Format) by Apple. 23 | public static let heic: AssetType = "public.heic" 24 | 25 | /// WebP 26 | /// 27 | /// Native decoding support only available on the following platforms: macOS 11, 28 | /// iOS 14, watchOS 7, tvOS 14. 29 | public static let webp: AssetType = "public.webp" 30 | 31 | public static let mp4: AssetType = "public.mpeg4" 32 | 33 | /// The M4V file format is a video container format developed by Apple and 34 | /// is very similar to the MP4 format. The primary difference is that M4V 35 | /// files may optionally be protected by DRM copy protection. 36 | public static let m4v: AssetType = "public.m4v" 37 | 38 | public static let mov: AssetType = "public.mov" 39 | } 40 | 41 | extension AssetType { 42 | /// Determines a type of the image based on the given data. 43 | public init?(_ data: Data) { 44 | guard let type = AssetType.make(data) else { 45 | return nil 46 | } 47 | self = type 48 | } 49 | 50 | private static func make(_ data: Data) -> AssetType? { 51 | func _match(_ numbers: [UInt8?], offset: Int = 0) -> Bool { 52 | guard data.count >= numbers.count else { 53 | return false 54 | } 55 | return zip(numbers.indices, numbers).allSatisfy { index, number in 56 | guard let number else { return true } 57 | guard (index + offset) < data.count else { return false } 58 | return data[index + offset] == number 59 | } 60 | } 61 | 62 | // JPEG magic numbers https://en.wikipedia.org/wiki/JPEG 63 | if _match([0xFF, 0xD8, 0xFF]) { return .jpeg } 64 | 65 | // PNG Magic numbers https://en.wikipedia.org/wiki/Portable_Network_Graphics 66 | if _match([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) { return .png } 67 | 68 | // GIF magic numbers https://en.wikipedia.org/wiki/GIF 69 | if _match([0x47, 0x49, 0x46]) { return .gif } 70 | 71 | // WebP magic numbers https://en.wikipedia.org/wiki/List_of_file_signatures 72 | if _match([0x52, 0x49, 0x46, 0x46, nil, nil, nil, nil, 0x57, 0x45, 0x42, 0x50]) { return .webp } 73 | 74 | // see https://stackoverflow.com/questions/21879981/avfoundation-avplayer-supported-formats-no-vob-or-mpg-containers 75 | // https://en.wikipedia.org/wiki/List_of_file_signatures 76 | if _match([0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D], offset: 4) { return .mp4 } 77 | 78 | // https://www.garykessler.net/library/file_sigs.html 79 | if _match([0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32], offset: 4) { return .m4v } 80 | 81 | if _match([0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x56, 0x20], offset: 4) { return .m4v } 82 | 83 | // MOV magic numbers https://www.garykessler.net/library/file_sigs.html 84 | if _match([0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20], offset: 4) { return .mov } 85 | 86 | // Either not enough data, or we just don't support this format. 87 | return nil 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Documentation/Migrations/Nuke 5 Migration Guide.md: -------------------------------------------------------------------------------- 1 | # Nuke 5 Migration Guide 2 | 3 | This guide is provided in order to ease the transition of existing applications using Nuke 4.x to the latest APIs, as well as explain the design and structure of new and changed functionality. 4 | 5 | ## Requirements 6 | 7 | - iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0 8 | - Xcode 8 9 | - Swift 3 10 | 11 | ## Overview 12 | 13 | Nuke 5 is a relatively small release which removes some of the complexity from the framework. Hopefully it will make *contributing* to Nuke easier. 14 | 15 | One of the major changes is the removal of promisified API as well as `Promise` itself. Promises were briefly added in Nuke 4 as an effort to simplify async code. The major downsides of promises are compelex memory management, extra complexity for users unfamiliar with promises, complicated debugging, performance penalties. Ultimately I decided that promises were adding more problems that they were solving. 16 | 17 | Chances are that changes made in Nuke 5 are not going to affect your code. 18 | 19 | ## Changes 20 | 21 | ### Removed promisified API and `Promise` itself 22 | 23 | > - Remove promisified API, use simple closures instead. For example, `Loading` protocol's method `func loadImage(with request: Request, token: CancellationToken?) -> Promise` was replaced with a method with a completion closure `func loadImage(with request: Request, token: CancellationToken?, completion: @escaping (Result) -> Void)`. The same applies to `DataLoading` protocol. 24 | > - Remove `Promise` class 25 | > - Remove `PromiseResolution` enum 26 | > - Remove `Response` typealias 27 | > - Add `Result` enum which is now used as a replacement for `PromiseResolution` (for instance, in `Target` protocol, etc) 28 | 29 | - If you've used promisified APIs you should replace them with a new closure-based APIs. If you still want to use promisified APIs please use [PromiseKit](https://github.com/mxcl/PromiseKit) or some other promise library to wrap Nuke APIs. 30 | - If you've provided a custom `Loading` or `DataLoading` protocols you should update them to a new closure-based APIs. 31 | - Replace `PromiseResolution` with `Result` where necessary (custom `Target` conformances, custom `Manager.Handler`). 32 | 33 | ### Memory cache is now managed exclusively by `Manager` 34 | 35 | > - Remove memory cache from `Loader` 36 | > - `Manager` now not only reads, but also writes to `Cache` 37 | > - `Manager` now has new methods to load images w/o target (Nuke 5.0.1) 38 | 39 | - If you're not constructing a custom `Loader` and you're not using it directly this change doesn't affect you 40 | - If you're using custom `Loader` directly and rely on its memory caching please new `Manager` APIs that load images w/o target 41 | - If you're constructing a custom `Loader` but don't use it directly then simply update to a new initializer which not longer requires you to pass memory cache in 42 | 43 | ### Removed `DataCaching` and `CachingDataLoader` 44 | 45 | - Instead of using those types you'll need to wrap `DataLoader` by yourself. For more info see [Third Party Libraries: Using Other Caching Libraries](https://github.com/kean/Nuke/blob/5.0/Documentation/Guides/Third%20Party%20Libraries.md#using-other-caching-libraries). 46 | 47 | ### Other Changes 48 | 49 | Make sure that you take those minor changes into account to: 50 | 51 | > - `Loader` constructor now provides a default value for `DataDecoding` object 52 | > - `DataLoading` protocol now works with a `Nuke.Request` and not `URLRequest` in case some extra info from `URLRequest` is required 53 | > - Reduce default `URLCache` disk capacity from 200 MB to 150 MB 54 | > - Reduce default `maxConcurrentOperationCount` of `DataLoader` from 8 to 6. 55 | > - Shared objects (like `Manager.shared`) are now constants. 56 | > - `Preheater` is now initialized with `Manager` instead of `Loading` object 57 | --------------------------------------------------------------------------------