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:
commit
ddd64100c7
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
34
README.md
34
README.md
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue