├── .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 |
--------------------------------------------------------------------------------