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
This commit is contained in:
DreamPiggy 2020-03-24 20:06:58 +08:00 committed by GitHub
commit ddd64100c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 16 deletions

View File

@ -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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [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 ## [1.0.0] - 2020-03-03
### Added ### Added

View File

@ -54,7 +54,7 @@ struct ContentView: View {
"https://www.sample-videos.com/img/Sample-png-image-1mb.png", "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://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", "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/w3c.svg",
"https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/wikimedia.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", "https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf",

View File

@ -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. 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 ### Customization and configuration setup
This framework is based on SDWebImage, which supports advanced customization and configuration to meet different users' demand. This framework is based on SDWebImage, which supports advanced customization and configuration to meet different users' demand.

View File

@ -79,6 +79,9 @@
32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; }; 32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
32C43E3422FD5DF400BE87F5 /* 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 */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -181,6 +184,7 @@
32C43E2922FD586200BE87F5 /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/tvOS/SDWebImage.framework; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -257,6 +261,7 @@
3211F84623DE984D00FC757F /* AnimatedImageTests.swift */, 3211F84623DE984D00FC757F /* AnimatedImageTests.swift */,
3211F84F23DE98E300FC757F /* WebImageTests.swift */, 3211F84F23DE98E300FC757F /* WebImageTests.swift */,
32BD9C4623E03B08008D5F6A /* IndicatorTests.swift */, 32BD9C4623E03B08008D5F6A /* IndicatorTests.swift */,
32ED4825242A13030053338E /* ImageManagerTests.swift */,
322E0F4723E57F09006836DC /* TestUtils.swift */, 322E0F4723E57F09006836DC /* TestUtils.swift */,
); );
path = Tests; path = Tests;
@ -707,6 +712,7 @@
32BD9C4723E03B08008D5F6A /* IndicatorTests.swift in Sources */, 32BD9C4723E03B08008D5F6A /* IndicatorTests.swift in Sources */,
3211F84723DE984D00FC757F /* AnimatedImageTests.swift in Sources */, 3211F84723DE984D00FC757F /* AnimatedImageTests.swift in Sources */,
322E0F4823E57F09006836DC /* TestUtils.swift in Sources */, 322E0F4823E57F09006836DC /* TestUtils.swift in Sources */,
32ED4826242A13030053338E /* ImageManagerTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -718,6 +724,7 @@
32BD9C4823E03B08008D5F6A /* IndicatorTests.swift in Sources */, 32BD9C4823E03B08008D5F6A /* IndicatorTests.swift in Sources */,
321C1D6A23DEDB98009CF62A /* AnimatedImageTests.swift in Sources */, 321C1D6A23DEDB98009CF62A /* AnimatedImageTests.swift in Sources */,
322E0F4923E57F09006836DC /* TestUtils.swift in Sources */, 322E0F4923E57F09006836DC /* TestUtils.swift in Sources */,
32ED4827242A13030053338E /* ImageManagerTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -729,6 +736,7 @@
32BD9C4923E03B08008D5F6A /* IndicatorTests.swift in Sources */, 32BD9C4923E03B08008D5F6A /* IndicatorTests.swift in Sources */,
321C1D6C23DEDB98009CF62A /* AnimatedImageTests.swift in Sources */, 321C1D6C23DEDB98009CF62A /* AnimatedImageTests.swift in Sources */,
322E0F4A23E57F09006836DC /* TestUtils.swift in Sources */, 322E0F4A23E57F09006836DC /* TestUtils.swift in Sources */,
32ED4828242A13030053338E /* ImageManagerTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -9,16 +9,23 @@
import SwiftUI import SwiftUI
import SDWebImage 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, *) @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
class ImageManager : ObservableObject, IndicatorReportable { public final class ImageManager : ObservableObject {
@Published var image: PlatformImage? // loaded image, note when progressive loading, this will published multiple times with different partial image /// 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 public var image: PlatformImage?
@Published var progress: Double = 0 // network progress, should only be used for indicator binding /// 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 var manager: SDWebImageManager
weak var currentOperation: SDWebImageOperation? = nil 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 isFirstLoad: Bool = true // false after first call `load()`
var url: URL? var url: URL?
@ -28,7 +35,11 @@ class ImageManager : ObservableObject, IndicatorReportable {
var failureBlock: ((Error) -> Void)? var failureBlock: ((Error) -> Void)?
var progressBlock: ((Int, Int) -> 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.url = url
self.options = options self.options = options
self.context = context self.context = context
@ -39,7 +50,8 @@ class ImageManager : ObservableObject, IndicatorReportable {
} }
} }
func load() { /// Start to load the url operation
public func load() {
isFirstLoad = false isFirstLoad = false
if currentOperation != nil { if currentOperation != nil {
return return
@ -71,12 +83,12 @@ class ImageManager : ObservableObject, IndicatorReportable {
return return
} }
self.image = image self.image = image
self.error = error
self.isIncremental = !finished self.isIncremental = !finished
if finished { if finished {
self.isLoading = false self.isLoading = false
self.progress = 1 self.progress = 1
if let image = image { if let image = image {
self.isSuccess = true
self.successBlock?(image, cacheType) self.successBlock?(image, cacheType)
} else { } else {
self.failureBlock?(error ?? NSError()) self.failureBlock?(error ?? NSError())
@ -85,9 +97,40 @@ class ImageManager : ObservableObject, IndicatorReportable {
} }
} }
func cancel() { /// Cancel the current url loading
currentOperation?.cancel() public func cancel() {
currentOperation = nil 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 {}

View File

@ -120,14 +120,15 @@ public struct WebImage : View {
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.onAppear { .onAppear {
guard self.retryOnAppear else { return } 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() self.imageManager.load()
} }
} }
.onDisappear { .onDisappear {
guard self.cancelOnDisappear else { return } guard self.cancelOnDisappear else { return }
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case // 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() self.imageManager.cancel()
} }
} }

View File

@ -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)
}
}

View File

@ -3,7 +3,7 @@ import SwiftUI
import ViewInspector import ViewInspector
@testable import SDWebImageSwiftUI @testable import SDWebImageSwiftUI
public extension PlatformViewRepresentable where Self: Inspectable { extension PlatformViewRepresentable where Self: Inspectable {
func platformView() throws -> PlatformViewType { func platformView() throws -> PlatformViewType {
#if os(macOS) #if os(macOS)