Skip to content

Commit ddd6410

Browse files
authored
Merge pull request #86 from SDWebImage/feature_public_image_manager
Make the ImageManager public, which is useful for custom View who need to bind the data source
2 parents 3d43d8b + 72c7c8d commit ddd6410

File tree

8 files changed

+147
-16
lines changed

8 files changed

+147
-16
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- `ImageManager` now public. Which allows advanced usage for custom View type. Use `@ObservedObject` to bind the manager with your own View and update the image.
810

911
## [1.0.0] - 2020-03-03
1012
### Added

Example/SDWebImageSwiftUIDemo/ContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ struct ContentView: View {
5454
"https://www.sample-videos.com/img/Sample-png-image-1mb.png",
5555
"https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png",
5656
"https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg",
57-
"http://via.placeholder.com/200x200.jpg",
57+
"https://via.placeholder.com/200x200.jpg",
5858
"https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/w3c.svg",
5959
"https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/wikimedia.svg",
6060
"https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf",

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,40 @@ If you need powerful animated image, `AnimatedImage` is the one to choose. Remem
206206

207207
But, because `AnimatedImage` use `UIViewRepresentable` and driven by UIKit, currently there may be some small incompatible issues between UIKit and SwiftUI layout and animation system, or bugs related to SwiftUI itself. We try our best to match SwiftUI behavior, and provide the same API as `WebImage`, which make it easy to switch between these two types if needed.
208208

209+
### Use `ImageManager` for your own View type
210+
211+
The `ImageManager` is a class which conforms to Combine's [ObservableObject](https://developer.apple.com/documentation/combine/observableobject) protocol. Which is the core fetching data source of `WebImage` we provided.
212+
213+
For advanced use case, like loading image into the complicated View graph which you don't want to use `WebImage`. You can directly bind your own View type with the Manager.
214+
215+
It looks familiar like `SDWebImageManager`, but it's built for SwiftUI world, which provide the Source of Truth for loading images. You'd better use SwiftUI's `@ObservedObject` to bind each single manager instance for your View instance, which automatically update your View's body when image status changed.
216+
217+
```swift
218+
struct MyView : View {
219+
@ObservedObject var imageManager: ImageManager
220+
var body: some View {
221+
// Your custom complicated view graph
222+
Group {
223+
if imageManager.image != nil {
224+
Image(uiImage: imageManager.image!)
225+
} else {
226+
Rectangle().fill(Color.gray)
227+
}
228+
}
229+
// Trigger image loading when appear
230+
.onAppear { self.imageManager.load() }
231+
// Cancel image loading when disappear
232+
.onDisappear { self.imageManager.cancel() }
233+
}
234+
}
235+
236+
struct MyView_Previews: PreviewProvider {
237+
static var previews: some View {
238+
MyView(imageManager: ImageManager(url: URL(string: "https://via.placeholder.com/200x200.jpg"))
239+
}
240+
}
241+
```
242+
209243
### Customization and configuration setup
210244

211245
This framework is based on SDWebImage, which supports advanced customization and configuration to meet different users' demand.

SDWebImageSwiftUI.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
8080
32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
8181
32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
82+
32ED4826242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
83+
32ED4827242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
84+
32ED4828242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
8285
/* End PBXBuildFile section */
8386

8487
/* Begin PBXContainerItemProxy section */
@@ -181,6 +184,7 @@
181184
32C43E2922FD586200BE87F5 /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/tvOS/SDWebImage.framework; sourceTree = "<group>"; };
182185
32C43E2D22FD586E00BE87F5 /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/watchOS/SDWebImage.framework; sourceTree = "<group>"; };
183186
32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageSwiftUI.swift; sourceTree = "<group>"; };
187+
32ED4825242A13030053338E /* ImageManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManagerTests.swift; sourceTree = "<group>"; };
184188
/* End PBXFileReference section */
185189

186190
/* Begin PBXFrameworksBuildPhase section */
@@ -257,6 +261,7 @@
257261
3211F84623DE984D00FC757F /* AnimatedImageTests.swift */,
258262
3211F84F23DE98E300FC757F /* WebImageTests.swift */,
259263
32BD9C4623E03B08008D5F6A /* IndicatorTests.swift */,
264+
32ED4825242A13030053338E /* ImageManagerTests.swift */,
260265
322E0F4723E57F09006836DC /* TestUtils.swift */,
261266
);
262267
path = Tests;
@@ -707,6 +712,7 @@
707712
32BD9C4723E03B08008D5F6A /* IndicatorTests.swift in Sources */,
708713
3211F84723DE984D00FC757F /* AnimatedImageTests.swift in Sources */,
709714
322E0F4823E57F09006836DC /* TestUtils.swift in Sources */,
715+
32ED4826242A13030053338E /* ImageManagerTests.swift in Sources */,
710716
);
711717
runOnlyForDeploymentPostprocessing = 0;
712718
};
@@ -718,6 +724,7 @@
718724
32BD9C4823E03B08008D5F6A /* IndicatorTests.swift in Sources */,
719725
321C1D6A23DEDB98009CF62A /* AnimatedImageTests.swift in Sources */,
720726
322E0F4923E57F09006836DC /* TestUtils.swift in Sources */,
727+
32ED4827242A13030053338E /* ImageManagerTests.swift in Sources */,
721728
);
722729
runOnlyForDeploymentPostprocessing = 0;
723730
};
@@ -729,6 +736,7 @@
729736
32BD9C4923E03B08008D5F6A /* IndicatorTests.swift in Sources */,
730737
321C1D6C23DEDB98009CF62A /* AnimatedImageTests.swift in Sources */,
731738
322E0F4A23E57F09006836DC /* TestUtils.swift in Sources */,
739+
32ED4828242A13030053338E /* ImageManagerTests.swift in Sources */,
732740
);
733741
runOnlyForDeploymentPostprocessing = 0;
734742
};

SDWebImageSwiftUI/Classes/ImageManager.swift

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@
99
import SwiftUI
1010
import SDWebImage
1111

12+
/// A Image observable object for handle image load process. This drive the Source of Truth for image loading status.
13+
/// You can use `@ObservedObject` to associate each instance of manager to your View type, which update your view's body from SwiftUI framework when image was loaded.
1214
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
13-
class ImageManager : ObservableObject, IndicatorReportable {
14-
@Published var image: PlatformImage? // loaded image, note when progressive loading, this will published multiple times with different partial image
15-
@Published var isLoading: Bool = false // whether network is loading or cache is querying, should only be used for indicator binding
16-
@Published var progress: Double = 0 // network progress, should only be used for indicator binding
15+
public final class ImageManager : ObservableObject {
16+
/// loaded image, note when progressive loading, this will published multiple times with different partial image
17+
@Published public var image: PlatformImage?
18+
/// loading error, you can grab the error code and reason listed in `SDWebImageErrorDomain`, to provide a user interface about the error reason
19+
@Published public var error: Error?
20+
/// whether network is loading or cache is querying, should only be used for indicator binding
21+
@Published public var isLoading: Bool = false
22+
/// network progress, should only be used for indicator binding
23+
@Published public var progress: Double = 0
24+
/// true means during incremental loading
25+
@Published public var isIncremental: Bool = false
1726

1827
var manager: SDWebImageManager
1928
weak var currentOperation: SDWebImageOperation? = nil
20-
var isSuccess: Bool = false // true means request for this URL is ended forever, load() do nothing
21-
var isIncremental: Bool = false // true means during incremental loading
2229
var isFirstLoad: Bool = true // false after first call `load()`
2330

2431
var url: URL?
@@ -28,7 +35,11 @@ class ImageManager : ObservableObject, IndicatorReportable {
2835
var failureBlock: ((Error) -> Void)?
2936
var progressBlock: ((Int, Int) -> Void)?
3037

31-
init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
38+
/// Create a image manager for loading the specify url, with custom options and context.
39+
/// - Parameter url: The image url
40+
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
41+
/// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
42+
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
3243
self.url = url
3344
self.options = options
3445
self.context = context
@@ -39,7 +50,8 @@ class ImageManager : ObservableObject, IndicatorReportable {
3950
}
4051
}
4152

42-
func load() {
53+
/// Start to load the url operation
54+
public func load() {
4355
isFirstLoad = false
4456
if currentOperation != nil {
4557
return
@@ -71,12 +83,12 @@ class ImageManager : ObservableObject, IndicatorReportable {
7183
return
7284
}
7385
self.image = image
86+
self.error = error
7487
self.isIncremental = !finished
7588
if finished {
7689
self.isLoading = false
7790
self.progress = 1
7891
if let image = image {
79-
self.isSuccess = true
8092
self.successBlock?(image, cacheType)
8193
} else {
8294
self.failureBlock?(error ?? NSError())
@@ -85,9 +97,40 @@ class ImageManager : ObservableObject, IndicatorReportable {
8597
}
8698
}
8799

88-
func cancel() {
89-
currentOperation?.cancel()
90-
currentOperation = nil
100+
/// Cancel the current url loading
101+
public func cancel() {
102+
if let operation = currentOperation {
103+
operation.cancel()
104+
currentOperation = nil
105+
isLoading = false
106+
}
91107
}
92108

93109
}
110+
111+
// Completion Handler
112+
extension ImageManager {
113+
/// Provide the action when image load fails.
114+
/// - Parameters:
115+
/// - action: The action to perform. The first arg is the error during loading. If `action` is `nil`, the call has no effect.
116+
public func setOnFailure(perform action: ((Error) -> Void)? = nil) {
117+
self.failureBlock = action
118+
}
119+
120+
/// Provide the action when image load successes.
121+
/// - Parameters:
122+
/// - action: The action to perform. The first arg is the loaded image, the second arg is the cache type loaded from. If `action` is `nil`, the call has no effect.
123+
public func setOnSuccess(perform action: ((PlatformImage, SDImageCacheType) -> Void)? = nil) {
124+
self.successBlock = action
125+
}
126+
127+
/// Provide the action when image load progress changes.
128+
/// - Parameters:
129+
/// - action: The action to perform. The first arg is the received size, the second arg is the total size, all in bytes. If `action` is `nil`, the call has no effect.
130+
public func setOnProgress(perform action: ((Int, Int) -> Void)? = nil) {
131+
self.progressBlock = action
132+
}
133+
}
134+
135+
// Indicator Reportor
136+
extension ImageManager: IndicatorReportable {}

SDWebImageSwiftUI/Classes/WebImage.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,15 @@ public struct WebImage : View {
120120
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
121121
.onAppear {
122122
guard self.retryOnAppear else { return }
123-
if !self.imageManager.isSuccess {
123+
// When using prorgessive loading, the new partial image will cause onAppear. Filter this case
124+
if self.imageManager.image == nil && !self.imageManager.isIncremental {
124125
self.imageManager.load()
125126
}
126127
}
127128
.onDisappear {
128129
guard self.cancelOnDisappear else { return }
129130
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
130-
if !self.imageManager.isSuccess && !self.imageManager.isIncremental {
131+
if self.imageManager.image == nil && !self.imageManager.isIncremental {
131132
self.imageManager.cancel()
132133
}
133134
}

Tests/ImageManagerTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import XCTest
2+
import SwiftUI
3+
import ViewInspector
4+
@testable import SDWebImageSwiftUI
5+
6+
class ImageManagerTests: XCTestCase {
7+
8+
override func setUp() {
9+
super.setUp()
10+
// Put setup code here. This method is called before the invocation of each test method in the class.
11+
}
12+
13+
override func tearDown() {
14+
// Put teardown code here. This method is called after the invocation of each test method in the class.
15+
super.tearDown()
16+
}
17+
18+
func testImageManager() throws {
19+
let expectation = self.expectation(description: "ImageManager usage with Combine")
20+
let imageUrl = URL(string: "https://via.placeholder.com/500x500.jpg")
21+
let imageManager = ImageManager(url: imageUrl)
22+
imageManager.setOnSuccess { image, cacheType in
23+
XCTAssertNotNil(image)
24+
expectation.fulfill()
25+
}
26+
imageManager.setOnFailure { error in
27+
XCTFail()
28+
}
29+
imageManager.setOnProgress { receivedSize, expectedSize in
30+
31+
}
32+
imageManager.load()
33+
XCTAssertNotNil(imageManager.currentOperation)
34+
let sub = imageManager.objectWillChange
35+
.subscribe(on: RunLoop.main)
36+
.receive(on: RunLoop.main)
37+
.sink { value in
38+
print(value)
39+
}
40+
sub.cancel()
41+
self.waitForExpectations(timeout: 5, handler: nil)
42+
}
43+
}

Tests/TestUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import SwiftUI
33
import ViewInspector
44
@testable import SDWebImageSwiftUI
55

6-
public extension PlatformViewRepresentable where Self: Inspectable {
6+
extension PlatformViewRepresentable where Self: Inspectable {
77

88
func platformView() throws -> PlatformViewType {
99
#if os(macOS)

0 commit comments

Comments
 (0)