Skip to content

Commit

Permalink
Merge pull request #86 from SDWebImage/feature_public_image_manager
Browse files Browse the repository at this point in the history
Make the ImageManager public, which is useful for custom View who need to bind the data source
  • Loading branch information
dreampiggy authored Mar 24, 2020
2 parents 3d43d8b + 72c7c8d commit ddd6410
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- `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.

## [1.0.0] - 2020-03-03
### Added
Expand Down
2 changes: 1 addition & 1 deletion Example/SDWebImageSwiftUIDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct ContentView: View {
"https://www.sample-videos.com/img/Sample-png-image-1mb.png",
"https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png",
"https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg",
"http://via.placeholder.com/200x200.jpg",
"https://via.placeholder.com/200x200.jpg",
"https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/w3c.svg",
"https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/wikimedia.svg",
"https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf",
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,40 @@ If you need powerful animated image, `AnimatedImage` is the one to choose. Remem

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.

### Use `ImageManager` for your own View type

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.

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.

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.

```swift
struct MyView : View {
@ObservedObject var imageManager: ImageManager
var body: some View {
// Your custom complicated view graph
Group {
if imageManager.image != nil {
Image(uiImage: imageManager.image!)
} else {
Rectangle().fill(Color.gray)
}
}
// Trigger image loading when appear
.onAppear { self.imageManager.load() }
// Cancel image loading when disappear
.onDisappear { self.imageManager.cancel() }
}
}

struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(imageManager: ImageManager(url: URL(string: "https://via.placeholder.com/200x200.jpg"))
}
}
```

### Customization and configuration setup

This framework is based on SDWebImage, which supports advanced customization and configuration to meet different users' demand.
Expand Down
8 changes: 8 additions & 0 deletions SDWebImageSwiftUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
32ED4826242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
32ED4827242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
32ED4828242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
/* End PBXBuildFile section */

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

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -257,6 +261,7 @@
3211F84623DE984D00FC757F /* AnimatedImageTests.swift */,
3211F84F23DE98E300FC757F /* WebImageTests.swift */,
32BD9C4623E03B08008D5F6A /* IndicatorTests.swift */,
32ED4825242A13030053338E /* ImageManagerTests.swift */,
322E0F4723E57F09006836DC /* TestUtils.swift */,
);
path = Tests;
Expand Down Expand Up @@ -707,6 +712,7 @@
32BD9C4723E03B08008D5F6A /* IndicatorTests.swift in Sources */,
3211F84723DE984D00FC757F /* AnimatedImageTests.swift in Sources */,
322E0F4823E57F09006836DC /* TestUtils.swift in Sources */,
32ED4826242A13030053338E /* ImageManagerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -718,6 +724,7 @@
32BD9C4823E03B08008D5F6A /* IndicatorTests.swift in Sources */,
321C1D6A23DEDB98009CF62A /* AnimatedImageTests.swift in Sources */,
322E0F4923E57F09006836DC /* TestUtils.swift in Sources */,
32ED4827242A13030053338E /* ImageManagerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -729,6 +736,7 @@
32BD9C4923E03B08008D5F6A /* IndicatorTests.swift in Sources */,
321C1D6C23DEDB98009CF62A /* AnimatedImageTests.swift in Sources */,
322E0F4A23E57F09006836DC /* TestUtils.swift in Sources */,
32ED4828242A13030053338E /* ImageManagerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
67 changes: 55 additions & 12 deletions SDWebImageSwiftUI/Classes/ImageManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@
import SwiftUI
import SDWebImage

/// A Image observable object for handle image load process. This drive the Source of Truth for image loading status.
/// 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.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
class ImageManager : ObservableObject, IndicatorReportable {
@Published var image: PlatformImage? // loaded image, note when progressive loading, this will published multiple times with different partial image
@Published var isLoading: Bool = false // whether network is loading or cache is querying, should only be used for indicator binding
@Published var progress: Double = 0 // network progress, should only be used for indicator binding
public final class ImageManager : ObservableObject {
/// loaded image, note when progressive loading, this will published multiple times with different partial image
@Published public var image: PlatformImage?
/// loading error, you can grab the error code and reason listed in `SDWebImageErrorDomain`, to provide a user interface about the error reason
@Published public var error: Error?
/// whether network is loading or cache is querying, should only be used for indicator binding
@Published public var isLoading: Bool = false
/// network progress, should only be used for indicator binding
@Published public var progress: Double = 0
/// true means during incremental loading
@Published public var isIncremental: Bool = false

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

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

init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
/// Create a image manager for loading the specify url, with custom options and context.
/// - Parameter url: The image url
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
/// - 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.
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
self.url = url
self.options = options
self.context = context
Expand All @@ -39,7 +50,8 @@ class ImageManager : ObservableObject, IndicatorReportable {
}
}

func load() {
/// Start to load the url operation
public func load() {
isFirstLoad = false
if currentOperation != nil {
return
Expand Down Expand Up @@ -71,12 +83,12 @@ class ImageManager : ObservableObject, IndicatorReportable {
return
}
self.image = image
self.error = error
self.isIncremental = !finished
if finished {
self.isLoading = false
self.progress = 1
if let image = image {
self.isSuccess = true
self.successBlock?(image, cacheType)
} else {
self.failureBlock?(error ?? NSError())
Expand All @@ -85,9 +97,40 @@ class ImageManager : ObservableObject, IndicatorReportable {
}
}

func cancel() {
currentOperation?.cancel()
currentOperation = nil
/// Cancel the current url loading
public func cancel() {
if let operation = currentOperation {
operation.cancel()
currentOperation = nil
isLoading = false
}
}

}

// Completion Handler
extension ImageManager {
/// Provide the action when image load fails.
/// - Parameters:
/// - action: The action to perform. The first arg is the error during loading. If `action` is `nil`, the call has no effect.
public func setOnFailure(perform action: ((Error) -> Void)? = nil) {
self.failureBlock = action
}

/// Provide the action when image load successes.
/// - Parameters:
/// - 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.
public func setOnSuccess(perform action: ((PlatformImage, SDImageCacheType) -> Void)? = nil) {
self.successBlock = action
}

/// Provide the action when image load progress changes.
/// - Parameters:
/// - 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.
public func setOnProgress(perform action: ((Int, Int) -> Void)? = nil) {
self.progressBlock = action
}
}

// Indicator Reportor
extension ImageManager: IndicatorReportable {}
5 changes: 3 additions & 2 deletions SDWebImageSwiftUI/Classes/WebImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,15 @@ public struct WebImage : View {
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.onAppear {
guard self.retryOnAppear else { return }
if !self.imageManager.isSuccess {
// When using prorgessive loading, the new partial image will cause onAppear. Filter this case
if self.imageManager.image == nil && !self.imageManager.isIncremental {
self.imageManager.load()
}
}
.onDisappear {
guard self.cancelOnDisappear else { return }
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
if !self.imageManager.isSuccess && !self.imageManager.isIncremental {
if self.imageManager.image == nil && !self.imageManager.isIncremental {
self.imageManager.cancel()
}
}
Expand Down
43 changes: 43 additions & 0 deletions Tests/ImageManagerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import XCTest
import SwiftUI
import ViewInspector
@testable import SDWebImageSwiftUI

class ImageManagerTests: XCTestCase {

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}

func testImageManager() throws {
let expectation = self.expectation(description: "ImageManager usage with Combine")
let imageUrl = URL(string: "https://via.placeholder.com/500x500.jpg")
let imageManager = ImageManager(url: imageUrl)
imageManager.setOnSuccess { image, cacheType in
XCTAssertNotNil(image)
expectation.fulfill()
}
imageManager.setOnFailure { error in
XCTFail()
}
imageManager.setOnProgress { receivedSize, expectedSize in

}
imageManager.load()
XCTAssertNotNil(imageManager.currentOperation)
let sub = imageManager.objectWillChange
.subscribe(on: RunLoop.main)
.receive(on: RunLoop.main)
.sink { value in
print(value)
}
sub.cancel()
self.waitForExpectations(timeout: 5, handler: nil)
}
}
2 changes: 1 addition & 1 deletion Tests/TestUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftUI
import ViewInspector
@testable import SDWebImageSwiftUI

public extension PlatformViewRepresentable where Self: Inspectable {
extension PlatformViewRepresentable where Self: Inspectable {

func platformView() throws -> PlatformViewType {
#if os(macOS)
Expand Down

0 comments on commit ddd6410

Please sign in to comment.