├── .gitignore ├── LICENSE ├── Package.swift ├── README.md └── Source └── RxNuke.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ## System 2 | .DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | Nuke.xcodeproj/xcshareddata/xcbaselines/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | 35 | ## Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | 40 | .build/ 41 | .swiftpm 42 | Package.resolved 43 | 44 | 45 | ## CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | 52 | Pods/ 53 | 54 | 55 | ## Carthage 56 | # 57 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 58 | 59 | Carthage 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2021 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "RxNuke", 6 | platforms: [ 7 | .macOS(.v10_15), 8 | .iOS(.v13), 9 | .tvOS(.v13), 10 | .watchOS(.v6) 11 | ], 12 | products: [ 13 | .library(name: "RxNuke", targets: ["RxNuke"]), 14 | ], 15 | dependencies: [ 16 | .package( 17 | url: "https://github.com/kean/Nuke.git", 18 | from: "12.0.0" 19 | ), 20 | .package( 21 | url: "https://github.com/ReactiveX/RxSwift.git", 22 | .upToNextMajor(from: "6.0.0") 23 | ) 24 | ], 25 | targets: [ 26 | .target(name: "RxNuke", dependencies: ["Nuke", "RxSwift"], path: "Source") 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | This repository contains [RxSwift](https://github.com/ReactiveX/RxSwift) extensions for [Nuke](https://github.com/kean/Nuke) as well as examples of common [use cases](#h_use_cases) solved by Rx. 4 | 5 | # Usage 6 | 7 | RxNuke provides a set of reactive extensions for Nuke: 8 | 9 | ```swift 10 | extension Reactive where Base: ImagePipeline { 11 | public func loadImage(with url: URL) -> Single 12 | public func loadImage(with request: ImageRequest) -> Single 13 | } 14 | ``` 15 | 16 | > A `Single` is a variation of `Observable` that, instead of emitting a series of elements, is always guaranteed to emit either a single element or an error. The common use case of `Single` is to wrap HTTP requests. See [Traits](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Traits.md#single) for more info. 17 | {:.info} 18 | 19 | Here's a basic example where we load an image and display the result on success: 20 | 21 | ```swift 22 | ImagePipeline.shared.rx.loadImage(with: url) 23 | .subscribe(onSuccess: { imageView.image = $0.image }) 24 | .disposed(by: disposeBag) 25 | ``` 26 | 27 | ## Going From Low to High Resolution 28 | 29 | Let's say you want to show a user a high-resolution image that takes a while it loads. You can show a spinner while the high-resolution image is downloaded, but you can improve the user experience by quickly downloading and displaying a thumbnail. 30 | 31 | > As an alternative, Nuke also supports progressive JPEG. To learn about it, see a [dedicated guide](/nuke/guides/progressive-decoding). 32 | {:.info} 33 | 34 | You can implement it using [`concat`](http://reactivex.io/documentation/operators/concat.html) operator. This operator results in a serial execution. It starts a thumbnail request, waits until it finishes, and only then starts a request for a high-resolution image. 35 | 36 | ```swift 37 | Observable.concat(pipeline.rx.loadImage(with: lowResUrl).orEmpty, 38 | pipeline.rx.loadImage(with: highResUtl).orEmpty) 39 | .subscribe(onNext: { imageView.image = $0.image }) 40 | .disposed(by: disposeBag) 41 | ``` 42 | 43 | > `orEmpty` is a custom property that ignores errors and completes the sequence instead 44 | > (equivalent to `func catchErrorJustComplete()` from [RxSwiftExt](https://github.com/RxSwiftCommunity/RxSwiftExt). 45 | {:.info} 46 | 47 | ```swift 48 | public extension RxSwift.PrimitiveSequence { 49 | var orEmpty: Observable { 50 | asObservable().catchError { _ in .empty() } 51 | } 52 | } 53 | ```` 54 | 55 | ## Loading the First Available Image 56 | 57 | Let's say you have multiple URLs for the same image. For example, you uploaded the image from the camera to the server; you have the image stored locally. When you display this image, it would be beneficial to first load the local URL, and if that fails, try to download from the network. 58 | 59 | This use case is very similar to [Going From Low to High Resolution](#going-from-low-to-high-resolution), except for the addition of the `.take(1)` operator that stops the execution when the first value is received. 60 | 61 | ```swift 62 | Observable.concat(pipeline.rx.loadImage(with: localUrl).orEmpty, 63 | pipeline.rx.loadImage(with: networkUrl).orEmpty) 64 | .take(1) 65 | .subscribe(onNext: { imageView.image = $0.image }) 66 | .disposed(by: disposeBag) 67 | ``` 68 | 69 | 70 | ## Load Multiple Images, Display All at Once 71 | 72 | Let's say you want to load two icons for a button, one icon for a `.normal` state, and one for a `.selected` state. You want to update the button, only when both icons are fully loaded. This can be achieved using a [`combineLatest`](http://reactivex.io/documentation/operators/combinelatest.html) operator. 73 | 74 | ```swift 75 | Observable.combineLatest(pipeline.rx.loadImage(with: iconUrl).asObservable(), 76 | pipeline.rx.loadImage(with: iconSelectedUrl).asObservable()) 77 | .subscribe(onNext: { icon, iconSelected in 78 | button.isHidden = false 79 | button.setImage(icon.image, for: .normal) 80 | button.setImage(iconSelected.image, for: .selected) 81 | }).disposed(by: disposeBag) 82 | ``` 83 | 84 | ## Showing Stale Image While Validating It 85 | 86 | Let's say you want to show the user a stale image stored in disk cache (`Foundation.URLCache`) while you go to the server to validate if the image is still fresh. It can be implemented using the same `append` operator that we covered [previously](#going-from-low-to-high-resolution). 87 | 88 | ```swift 89 | let cacheRequest = URLRequest(url: imageUrl, cachePolicy: .returnCacheDataDontLoad) 90 | let networkRequest = URLRequest(url: imageUrl, cachePolicy: .useProtocolCachePolicy) 91 | 92 | Observable.concat(pipeline.rx.loadImage(with: ImageRequest(urlRequest: cacheRequest).orEmpty, 93 | pipeline.rx.loadImage(with: ImageRequest(urlRequest: networkRequest)).orEmpty) 94 | .subscribe(onNext: { imageView.image = $0.image }) 95 | .disposed(by: disposeBag) 96 | ``` 97 | 98 | > See ["Image Caching"](/post/image-caching) to learn more about HTTP cache. 99 | {:.info} 100 | 101 | ## Auto Retry 102 | 103 | Auto-retry with an exponential backoff of other delay options (including immediate retry when a network connection is re-established) using [smart retry](https://kean.github.io/post/smart-retry). 104 | 105 | ```swift 106 | pipeline.rx.loadImage(with: request).asObservable() 107 | .retry(3, delay: .exponential(initial: 3, multiplier: 1, maxDelay: 16)) 108 | .subscribe(onNext: { imageView.image = $0.image }) 109 | .disposed(by: disposeBag) 110 | ``` 111 | 112 | 113 | ## Tracking Activities 114 | 115 | Suppose you want to show an activity indicator while waiting for an image to load. Here's how you can do it using `ActivityIndicator` class provided by [`RxSwiftUtilities`](https://github.com/RxSwiftCommunity/RxSwiftUtilities): 116 | 117 | ```swift 118 | let isBusy = ActivityIndicator() 119 | 120 | pipeline.rx.loadImage(with: imageUrl) 121 | .trackActivity(isBusy) 122 | .subscribe(onNext: { imageView.image = $0.image }) 123 | .disposed(by: disposeBag) 124 | 125 | isBusy.asDriver() 126 | .drive(activityIndicator.rx.isAnimating) 127 | .disposed(by: disposeBag) 128 | ``` 129 | 130 | 131 | ## In a Table or Collection View 132 | 133 | Here's how you can integrate the code provided in the previous examples into your table or collection view cells: 134 | 135 | ```swift 136 | final class ImageCell: UICollectionViewCell { 137 | private var imageView: UIImageView! 138 | private var disposeBag = DisposeBag() 139 | 140 | // <.. create an image view using your preferred way ..> 141 | 142 | func display(_ image: Single) { 143 | 144 | // Create a new dispose bag, previous dispose bag gets deallocated 145 | // and cancels all previous subscriptions. 146 | disposeBag = DisposeBag() 147 | 148 | imageView.image = nil 149 | 150 | // Load an image and display the result on success. 151 | image.subscribe(onSuccess: { [weak self] response in 152 | self?.imageView.image = response.image 153 | }).disposed(by: disposeBag) 154 | } 155 | } 156 | ``` 157 | 158 | 159 | # Requirements 160 | 161 | | RxNuke | Swift | Xcode | Platforms | 162 | |-------------|------------------|--------------------|---------------------------------------------------| 163 | | RxNuke 5.0 | Swift 5.6 | Xcode 13.3 | iOS 13.0 / watchOS 6.0 / macOS 10.15 / tvOS 13.0 | 164 | | RxNuke 4.0 | Swift 5.6 | Xcode 13.3 | iOS 13.0 / watchOS 6.0 / macOS 10.15 / tvOS 13.0 | 165 | | RxNuke 3.0 | Swift 5.3 | Xcode 12.0 | iOS 11.0 / watchOS 4.0 / macOS 10.13 / tvOS 11.0 | 166 | 167 | # License 168 | 169 | RxNuke is available under the MIT license. See the LICENSE file for more info. 170 | -------------------------------------------------------------------------------- /Source/RxNuke.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017-2023 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Nuke 6 | import RxSwift 7 | import Foundation 8 | 9 | extension ImagePipeline: ReactiveCompatible {} 10 | 11 | extension Reactive where Base: ImagePipeline { 12 | /// Loads an image with a given url. Emits the value synchronously if the 13 | /// image was found in memory cache. 14 | public func loadImage(with url: URL) -> Single { 15 | return self.loadImage(with: ImageRequest(url: url)) 16 | } 17 | 18 | /// Loads an image with a given request. Emits the value synchronously if the 19 | /// image was found in memory cache. 20 | public func loadImage(with request: ImageRequest) -> Single { 21 | return Single.create { single in 22 | if let image = self.base.cache[request] { 23 | single(.success(ImageResponse(container: image, request: request))) // return synchronously 24 | return Disposables.create() // nop 25 | } else { 26 | let task = self.base.loadImage(with: request, completion: { result in 27 | switch result { 28 | case let .success(response): 29 | single(.success(response)) 30 | case let .failure(error): 31 | single(.failure(error)) 32 | } 33 | }) 34 | return Disposables.create { task.cancel() } 35 | } 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------