Merge pull request #232 from SDWebImage/fix_backport_ios13
Fix iOS 13 compatibility && State changes
This commit is contained in:
commit
9a82da2a1c
|
@ -34,6 +34,8 @@
|
|||
322E0E2228D332130003A55F /* Images.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 322E0DF228D331A20003A55F /* Images.bundle */; };
|
||||
322E0E2328D332130003A55F /* Images.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 322E0DF228D331A20003A55F /* Images.bundle */; };
|
||||
326B0D712345C01900D28269 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B0D702345C01900D28269 /* DetailView.swift */; };
|
||||
327B90F228DC4EBB003E8BD9 /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 327B90F128DC4EBB003E8BD9 /* ViewInspector */; };
|
||||
327B90F428DC4EC0003E8BD9 /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 327B90F328DC4EC0003E8BD9 /* ViewInspector */; };
|
||||
32DCFE9528D333E8001A17BF /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 32DCFE9428D333E8001A17BF /* ViewInspector */; };
|
||||
32E5290C2348A0C700EA46FF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5290B2348A0C700EA46FF /* AppDelegate.swift */; };
|
||||
32E529102348A0C900EA46FF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 32E5290F2348A0C900EA46FF /* Assets.xcassets */; };
|
||||
|
@ -220,6 +222,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
833A61715BAAB31702D867CC /* Pods_SDWebImageSwiftUITests_macOS.framework in Frameworks */,
|
||||
327B90F228DC4EBB003E8BD9 /* ViewInspector in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -228,6 +231,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2E3D81A12C757E01A3C420F2 /* Pods_SDWebImageSwiftUITests_tvOS.framework in Frameworks */,
|
||||
327B90F428DC4EC0003E8BD9 /* ViewInspector in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -517,9 +521,13 @@
|
|||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
327B90EE28DC4EAA003E8BD9 /* PBXTargetDependency */,
|
||||
322E0E0728D331F00003A55F /* PBXTargetDependency */,
|
||||
);
|
||||
name = "SDWebImageSwiftUITests macOS";
|
||||
packageProductDependencies = (
|
||||
327B90F128DC4EBB003E8BD9 /* ViewInspector */,
|
||||
);
|
||||
productName = "SDWebImageSwiftUITests macOS";
|
||||
productReference = 322E0E0228D331F00003A55F /* SDWebImageSwiftUITests macOS.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
|
@ -537,9 +545,13 @@
|
|||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
327B90F028DC4EAE003E8BD9 /* PBXTargetDependency */,
|
||||
322E0E1428D332050003A55F /* PBXTargetDependency */,
|
||||
);
|
||||
name = "SDWebImageSwiftUITests tvOS";
|
||||
packageProductDependencies = (
|
||||
327B90F328DC4EC0003E8BD9 /* ViewInspector */,
|
||||
);
|
||||
productName = "SDWebImageSwiftUITests tvOS";
|
||||
productReference = 322E0E0F28D332050003A55F /* SDWebImageSwiftUITests tvOS.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
|
@ -698,7 +710,7 @@
|
|||
);
|
||||
mainGroup = 607FACC71AFB9204008FA782;
|
||||
packageReferences = (
|
||||
32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector.git" */,
|
||||
32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */,
|
||||
);
|
||||
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -1225,6 +1237,14 @@
|
|||
target = 32E5291F2348A0D300EA46FF /* SDWebImageSwiftUIDemo-tvOS */;
|
||||
targetProxy = 322E0E1328D332050003A55F /* PBXContainerItemProxy */;
|
||||
};
|
||||
327B90EE28DC4EAA003E8BD9 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
productRef = 327B90ED28DC4EAA003E8BD9 /* ViewInspector */;
|
||||
};
|
||||
327B90F028DC4EAE003E8BD9 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
productRef = 327B90EF28DC4EAE003E8BD9 /* ViewInspector */;
|
||||
};
|
||||
32DCFE9728D333F1001A17BF /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
productRef = 32DCFE9628D333F1001A17BF /* ViewInspector */;
|
||||
|
@ -2044,7 +2064,7 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector.git" */ = {
|
||||
32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/nalexn/ViewInspector.git";
|
||||
requirement = {
|
||||
|
@ -2055,14 +2075,34 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
327B90ED28DC4EAA003E8BD9 /* ViewInspector */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
|
||||
productName = ViewInspector;
|
||||
};
|
||||
327B90EF28DC4EAE003E8BD9 /* ViewInspector */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
|
||||
productName = ViewInspector;
|
||||
};
|
||||
327B90F128DC4EBB003E8BD9 /* ViewInspector */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
|
||||
productName = ViewInspector;
|
||||
};
|
||||
327B90F328DC4EC0003E8BD9 /* ViewInspector */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
|
||||
productName = ViewInspector;
|
||||
};
|
||||
32DCFE9428D333E8001A17BF /* ViewInspector */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector.git" */;
|
||||
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
|
||||
productName = ViewInspector;
|
||||
};
|
||||
32DCFE9628D333F1001A17BF /* ViewInspector */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector.git" */;
|
||||
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
|
||||
productName = ViewInspector;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
|
|
@ -34,6 +34,52 @@ extension Indicator where T == ProgressView<EmptyView, EmptyView> {
|
|||
}
|
||||
#endif
|
||||
|
||||
// Test Switching url using @State
|
||||
struct ContentView2: View {
|
||||
@State var imageURLs = [
|
||||
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_1.jpg",
|
||||
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_2.jpg",
|
||||
"http://assets.sbnation.com/assets/2512203/dogflops.gif",
|
||||
"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"
|
||||
]
|
||||
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
|
||||
@State var imageIndex : Int = 0
|
||||
var body: some View {
|
||||
Group {
|
||||
Text("\(animated ? "AnimatedImage" : "WebImage") - \((imageURLs[imageIndex] as NSString).lastPathComponent)")
|
||||
Spacer()
|
||||
#if os(watchOS)
|
||||
WebImage(url:URL(string: imageURLs[imageIndex]))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
#else
|
||||
if self.animated {
|
||||
AnimatedImage(url:URL(string: imageURLs[imageIndex]))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} else {
|
||||
WebImage(url:URL(string: imageURLs[imageIndex]))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
#endif
|
||||
Spacer()
|
||||
Button("Next") {
|
||||
if imageIndex + 1 >= imageURLs.count {
|
||||
imageIndex = 0
|
||||
} else {
|
||||
imageIndex += 1
|
||||
}
|
||||
}
|
||||
Button("Reload") {
|
||||
SDImageCache.shared.clearMemory()
|
||||
SDImageCache.shared.clearDisk(onCompletion: nil)
|
||||
}
|
||||
Toggle("Switch", isOn: $animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@State var imageURLs = [
|
||||
"http://assets.sbnation.com/assets/2512203/dogflops.gif",
|
||||
|
@ -58,6 +104,58 @@ struct ContentView: View {
|
|||
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
|
||||
@EnvironmentObject var settings: UserSettings
|
||||
|
||||
// Used to avoid https://twitter.com/fatbobman/status/1572507700436807683?s=20&t=5rfj6BUza5Jii-ynQatCFA
|
||||
struct ItemView: View {
|
||||
@Binding var animated: Bool
|
||||
@State var url: String
|
||||
var body: some View {
|
||||
NavigationLink(destination: DetailView(url: url, animated: self.animated)) {
|
||||
HStack {
|
||||
if self.animated {
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
AnimatedImage(url: URL(string:url), isAnimating: .constant(true))
|
||||
.onViewUpdate { view, context in
|
||||
#if os(macOS)
|
||||
view.toolTip = url
|
||||
#endif
|
||||
}
|
||||
.indicator(SDWebImageActivityIndicator.medium)
|
||||
/**
|
||||
.placeholder(UIImage(systemName: "photo"))
|
||||
*/
|
||||
.transition(.fade)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
|
||||
#else
|
||||
WebImage(url: URL(string:url), isAnimating: self.$animated)
|
||||
.resizable()
|
||||
.indicator(.activity)
|
||||
.transition(.fade(duration: 0.5))
|
||||
.scaledToFit()
|
||||
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
|
||||
#endif
|
||||
} else {
|
||||
WebImage(url: URL(string:url), isAnimating: .constant(true))
|
||||
.resizable()
|
||||
/**
|
||||
.placeholder {
|
||||
Image(systemName: "photo")
|
||||
}
|
||||
*/
|
||||
.indicator(.activity)
|
||||
.transition(.fade(duration: 0.5))
|
||||
.scaledToFit()
|
||||
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
|
||||
}
|
||||
Text((url as NSString).lastPathComponent)
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
#if os(iOS)
|
||||
return NavigationView {
|
||||
|
@ -119,49 +217,8 @@ struct ContentView: View {
|
|||
func contentView() -> some View {
|
||||
List {
|
||||
ForEach(imageURLs, id: \.self) { url in
|
||||
NavigationLink(destination: DetailView(url: url, animated: self.animated)) {
|
||||
HStack {
|
||||
if self.animated {
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
AnimatedImage(url: URL(string:url), isAnimating: .constant(true))
|
||||
.onViewUpdate { view, context in
|
||||
#if os(macOS)
|
||||
view.toolTip = url
|
||||
#endif
|
||||
}
|
||||
.indicator(SDWebImageActivityIndicator.medium)
|
||||
/**
|
||||
.placeholder(UIImage(systemName: "photo"))
|
||||
*/
|
||||
.transition(.fade)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
|
||||
#else
|
||||
WebImage(url: URL(string:url), isAnimating: self.$animated)
|
||||
.resizable()
|
||||
.indicator(.activity)
|
||||
.transition(.fade(duration: 0.5))
|
||||
.scaledToFit()
|
||||
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
|
||||
#endif
|
||||
} else {
|
||||
WebImage(url: URL(string:url), isAnimating: .constant(true))
|
||||
.resizable()
|
||||
/**
|
||||
.placeholder {
|
||||
Image(systemName: "photo")
|
||||
}
|
||||
*/
|
||||
.indicator(.activity)
|
||||
.transition(.fade(duration: 0.5))
|
||||
.scaledToFit()
|
||||
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
|
||||
}
|
||||
Text((url as NSString).lastPathComponent)
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
// Must use top level view instead of inlined view structure
|
||||
ItemView(animated: $animated, url: url)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
indexSet.forEach { index in
|
||||
|
|
58
README.md
58
README.md
|
@ -270,7 +270,7 @@ It looks familiar like `SDWebImageManager`, but it's built for SwiftUI world, wh
|
|||
|
||||
```swift
|
||||
struct MyView : View {
|
||||
@ObservedObject var imageManager: ImageManager
|
||||
@ObservedObject var imageManager = ImageManager()
|
||||
var body: some View {
|
||||
// Your custom complicated view graph
|
||||
Group {
|
||||
|
@ -281,17 +281,11 @@ struct MyView : View {
|
|||
}
|
||||
}
|
||||
// Trigger image loading when appear
|
||||
.onAppear { self.imageManager.load() }
|
||||
.onAppear { self.imageManager.load(url: url) }
|
||||
// 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
|
||||
|
@ -337,6 +331,54 @@ For more information, it's really recommended to check our demo, to learn detail
|
|||
|
||||
### Common Problems
|
||||
|
||||
#### Using WebImage/AnimatedImage in List/LazyStack/LazyGrid and ForEach
|
||||
|
||||
SwiftUI has a known behavior(bug?) when using stateful view in `List/LazyStack/LazyGrid`.
|
||||
Only the **Top Level** view can hold its own `@State/@StateObject`, but the sub structure will lose state when scroll out of screen.
|
||||
However, WebImage/Animated is both stateful. To ensure the state keep in sync even when scroll out of screen. you may use some tricks.
|
||||
|
||||
See more: https://twitter.com/fatbobman/status/1572507700436807683?s=21&t=z4FkAWTMvjsgL-wKdJGreQ
|
||||
|
||||
In short, it's not recommanded to do so:
|
||||
|
||||
```swift
|
||||
struct ContentView {
|
||||
@State var imageURLs: [String]
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(imageURLs, id: \.self) { url in
|
||||
VStack {
|
||||
WebImage(url) // The top level is `VStack`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
instead, using this approach:
|
||||
|
||||
```swift
|
||||
struct ContentView {
|
||||
struct BodyView {
|
||||
@State var url: String
|
||||
var body: some View {
|
||||
VStack {
|
||||
WebImage(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@State var imageURLs: [String]
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(imageURLs, id: \.self) { url in
|
||||
BodyView(url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Image/WebImage/AnimatedImage in Button/NavigationLink
|
||||
|
||||
SwiftUI's `Button` apply overlay to its content (except `Text`) by default, this is common mistake to write code like this, which cause strange behavior:
|
||||
|
|
|
@ -23,6 +23,10 @@
|
|||
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
|
||||
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
|
||||
326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
|
||||
32B79C9528DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
|
||||
32B79C9628DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
|
||||
32B79C9728DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
|
||||
32B79C9828DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
|
||||
32B933E523659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
|
||||
32B933E623659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
|
||||
32B933E723659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
|
||||
|
@ -87,6 +91,7 @@
|
|||
326B8486236335110011BDFB /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
|
||||
326B848B236335400011BDFB /* ProgressIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = "<group>"; };
|
||||
326E480923431C0F00C633E9 /* ImageViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewWrapper.swift; sourceTree = "<group>"; };
|
||||
32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUICompatibility.swift; sourceTree = "<group>"; };
|
||||
32B933E423659A1900BB7CAD /* Transition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transition.swift; sourceTree = "<group>"; };
|
||||
32BC086F28D23D35002451BD /* StateObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateObject.swift; sourceTree = "<group>"; };
|
||||
32BC087028D23D35002451BD /* OnChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnChange.swift; sourceTree = "<group>"; };
|
||||
|
@ -233,6 +238,7 @@
|
|||
32C43DDE22FD54C600BE87F5 /* WebImage.swift */,
|
||||
32C43DDF22FD54C600BE87F5 /* AnimatedImage.swift */,
|
||||
32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */,
|
||||
32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */,
|
||||
326E480923431C0F00C633E9 /* ImageViewWrapper.swift */,
|
||||
32D26A012446B546005905DA /* Image.swift */,
|
||||
);
|
||||
|
@ -457,6 +463,7 @@
|
|||
326B84822363350C0011BDFB /* Indicator.swift in Sources */,
|
||||
32C43E3222FD5DE100BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
|
||||
326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
|
||||
32B79C9528DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
|
||||
326B8487236335110011BDFB /* ActivityIndicator.swift in Sources */,
|
||||
32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */,
|
||||
32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */,
|
||||
|
@ -479,6 +486,7 @@
|
|||
326B84832363350C0011BDFB /* Indicator.swift in Sources */,
|
||||
32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
|
||||
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
|
||||
32B79C9628DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
|
||||
326B8488236335110011BDFB /* ActivityIndicator.swift in Sources */,
|
||||
32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */,
|
||||
32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */,
|
||||
|
@ -501,6 +509,7 @@
|
|||
326B84842363350C0011BDFB /* Indicator.swift in Sources */,
|
||||
32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
|
||||
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
|
||||
32B79C9728DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
|
||||
326B8489236335110011BDFB /* ActivityIndicator.swift in Sources */,
|
||||
32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */,
|
||||
32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */,
|
||||
|
@ -523,6 +532,7 @@
|
|||
326B84852363350C0011BDFB /* Indicator.swift in Sources */,
|
||||
32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
|
||||
326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
|
||||
32B79C9828DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
|
||||
326B848A236335110011BDFB /* ActivityIndicator.swift in Sources */,
|
||||
32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */,
|
||||
32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */,
|
||||
|
|
|
@ -101,15 +101,7 @@ final class AnimatedImageConfiguration: ObservableObject {
|
|||
/// A Image View type to load image from url, data or bundle. Supports animated and static image format.
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
public struct AnimatedImage : PlatformViewRepresentable {
|
||||
@SwiftUI.StateObject var imageModel_SwiftUI = AnimatedImageModel()
|
||||
@Backport.StateObject var imageModel_Backport = AnimatedImageModel()
|
||||
var imageModel: AnimatedImageModel {
|
||||
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
|
||||
return imageModel_SwiftUI
|
||||
} else {
|
||||
return imageModel_Backport
|
||||
}
|
||||
}
|
||||
@ObservedObject var imageModel: AnimatedImageModel
|
||||
@ObservedObject var imageHandler = AnimatedImageHandler()
|
||||
@ObservedObject var imageLayout = AnimatedImageLayout()
|
||||
@ObservedObject var imageConfiguration = AnimatedImageConfiguration()
|
||||
|
@ -186,11 +178,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
|
||||
init(imageModel: AnimatedImageModel, isAnimating: Binding<Bool>) {
|
||||
self._isAnimating = isAnimating
|
||||
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
|
||||
_imageModel_SwiftUI = SwiftUI.StateObject(wrappedValue: imageModel)
|
||||
} else {
|
||||
_imageModel_Backport = Backport.StateObject(wrappedValue: imageModel)
|
||||
}
|
||||
_imageModel = ObservedObject(wrappedValue: imageModel)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
|
|
|
@ -21,49 +21,37 @@ public final class ImageManager : ObservableObject {
|
|||
@Published public var cacheType: SDImageCacheType = .none
|
||||
/// 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
|
||||
/// A observed object to pass through the image manager loading status to indicator
|
||||
@Published public var indicatorStatus = IndicatorStatus()
|
||||
|
||||
var manager: SDWebImageManager?
|
||||
weak var currentOperation: SDWebImageOperation? = nil
|
||||
|
||||
var url: URL?
|
||||
var options: SDWebImageOptions = []
|
||||
var context: [SDWebImageContextOption : Any]? = nil
|
||||
|
||||
var currentURL: URL?
|
||||
var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)?
|
||||
var failureBlock: ((Error) -> Void)?
|
||||
var progressBlock: ((Int, Int) -> Void)?
|
||||
|
||||
/// Create a image manager for loading the specify url, with custom options and context.
|
||||
public init() {}
|
||||
|
||||
/// Start to load the url operation
|
||||
/// - 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
|
||||
if let manager = context?[.customManager] as? SDWebImageManager {
|
||||
self.manager = manager
|
||||
public func load(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
|
||||
let manager: SDWebImageManager
|
||||
if let customManager = context?[.customManager] as? SDWebImageManager {
|
||||
manager = customManager
|
||||
} else {
|
||||
self.manager = .shared
|
||||
manager = .shared
|
||||
}
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
/// Start to load the url operation
|
||||
public func load() {
|
||||
guard let manager = manager else {
|
||||
if (currentOperation != nil && currentURL == url) {
|
||||
return
|
||||
}
|
||||
if currentOperation != nil {
|
||||
return
|
||||
}
|
||||
self.isLoading = true
|
||||
currentURL = url
|
||||
indicatorStatus.isLoading = true
|
||||
indicatorStatus.progress = 0
|
||||
currentOperation = manager.loadImage(with: url, options: options, context: context, progress: { [weak self] (receivedSize, expectedSize, _) in
|
||||
guard let self = self else {
|
||||
return
|
||||
|
@ -75,7 +63,7 @@ public final class ImageManager : ObservableObject {
|
|||
progress = 0
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.progress = progress
|
||||
self.indicatorStatus.progress = progress
|
||||
}
|
||||
self.progressBlock?(receivedSize, expectedSize)
|
||||
}) { [weak self] (image, data, error, cacheType, finished, _) in
|
||||
|
@ -95,8 +83,8 @@ public final class ImageManager : ObservableObject {
|
|||
if finished {
|
||||
self.imageData = data
|
||||
self.cacheType = cacheType
|
||||
self.isLoading = false
|
||||
self.progress = 1
|
||||
self.indicatorStatus.isLoading = false
|
||||
self.indicatorStatus.progress = 1
|
||||
if let image = image {
|
||||
self.successBlock?(image, data, cacheType)
|
||||
} else {
|
||||
|
@ -111,8 +99,9 @@ public final class ImageManager : ObservableObject {
|
|||
if let operation = currentOperation {
|
||||
operation.cancel()
|
||||
currentOperation = nil
|
||||
isLoading = false
|
||||
}
|
||||
indicatorStatus.isLoading = false
|
||||
currentURL = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -45,6 +45,8 @@ public final class ImagePlayer : ObservableObject {
|
|||
/// Current playing loop count
|
||||
@Published public var currentLoopCount: UInt = 0
|
||||
|
||||
var currentAnimatedImage: (PlatformImage & SDAnimatedImageProvider)?
|
||||
|
||||
/// Whether current player is valid for playing. This will check the internal player exist or not
|
||||
public var isValid: Bool {
|
||||
player != nil
|
||||
|
@ -97,10 +99,11 @@ public final class ImagePlayer : ObservableObject {
|
|||
/// Setup the player using Animated Image.
|
||||
/// After setup, you can always check `isValid` status, or call `startPlaying` to play the animation.
|
||||
/// - Parameter image: animated image
|
||||
public func setupPlayer(animatedImage: SDAnimatedImageProvider) {
|
||||
public func setupPlayer(animatedImage: PlatformImage & SDAnimatedImageProvider) {
|
||||
if isValid {
|
||||
return
|
||||
}
|
||||
currentAnimatedImage = animatedImage
|
||||
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
|
||||
imagePlayer.animationFrameHandler = { [weak self] (index, frame) in
|
||||
self?.currentFrameIndex = index
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* This file is part of the SDWebImage package.
|
||||
* (c) DreamPiggy <lizhuoli1126@126.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(macOS)
|
||||
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
struct PlatformAppear: PlatformViewRepresentable {
|
||||
let appearAction: () -> Void
|
||||
let disappearAction: () -> Void
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
func makeUIView(context: Context) -> some UIView {
|
||||
let view = PlatformAppearView()
|
||||
view.appearAction = appearAction
|
||||
view.disappearAction = disappearAction
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIViewType, context: Context) {}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
func makeNSView(context: Context) -> some NSView {
|
||||
let view = PlatformAppearView()
|
||||
view.appearAction = appearAction
|
||||
view.disappearAction = disappearAction
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSViewType, context: Context) {}
|
||||
#endif
|
||||
}
|
||||
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
class PlatformAppearView: PlatformView {
|
||||
var appearAction: () -> Void = {}
|
||||
var disappearAction: () -> Void = {}
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
override func willMove(toWindow newWindow: UIWindow?) {
|
||||
if newWindow != nil {
|
||||
DispatchQueue.main.async {
|
||||
self.appearAction()
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.disappearAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
override func viewWillMove(toWindow newWindow: NSWindow?) {
|
||||
if newWindow != nil {
|
||||
DispatchQueue.main.async {
|
||||
self.appearAction()
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.disappearAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
extension View {
|
||||
/// Used UIKit/AppKit behavior to detect the SwiftUI view's visibility.
|
||||
/// This hack is because of SwiftUI 1.0/2.0 buggy behavior. The built-in `onAppear` and `onDisappear` is so massive on some cases. Where UIKit/AppKit is solid.
|
||||
/// - Parameters:
|
||||
/// - appear: The action when view appears
|
||||
/// - disappear: The action when view disappears
|
||||
/// - Returns: Some view
|
||||
func onPlatformAppear(appear: @escaping () -> Void = {}, disappear: @escaping () -> Void = {}) -> some View {
|
||||
#if os(iOS) || os(tvOS) || os(macOS)
|
||||
return self.background(PlatformAppear(appearAction: appear, disappearAction: disappear))
|
||||
#else
|
||||
return self.onAppear(perform: appear).onDisappear(perform: disappear)
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -9,6 +9,15 @@
|
|||
import SwiftUI
|
||||
import SDWebImage
|
||||
|
||||
/// Data Binding Object, only properties in this object can support changes from user with @State and refresh
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
final class WebImageModel : ObservableObject {
|
||||
/// URL image
|
||||
@Published var url: URL?
|
||||
@Published var options: SDWebImageOptions = []
|
||||
@Published var context: [SDWebImageContextOption : Any]? = nil
|
||||
}
|
||||
|
||||
/// Completion Handler Binding Object, supports dynamic @State changes
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
final class WebImageHandler: ObservableObject {
|
||||
|
@ -43,34 +52,22 @@ public struct WebImage : View {
|
|||
/// True to start animation, false to stop animation.
|
||||
@Binding public var isAnimating: Bool
|
||||
|
||||
/// A observed object to pass through the image model to manager
|
||||
@ObservedObject var imageModel: WebImageModel
|
||||
|
||||
/// A observed object to pass through the image handler to manager
|
||||
@ObservedObject var imageHandler = WebImageHandler()
|
||||
|
||||
/// A observed object to pass through the image configuration to player
|
||||
@ObservedObject var imageConfiguration = WebImageConfiguration()
|
||||
|
||||
/// A observed object to pass through the image manager loading status to indicator
|
||||
@ObservedObject var indicatorStatus = IndicatorStatus()
|
||||
@ObservedObject var indicatorStatus : IndicatorStatus
|
||||
|
||||
@SwiftUI.StateObject var imagePlayer_SwiftUI = ImagePlayer()
|
||||
@Backport.StateObject var imagePlayer_Backport = ImagePlayer()
|
||||
var imagePlayer: ImagePlayer {
|
||||
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
|
||||
return imagePlayer_SwiftUI
|
||||
} else {
|
||||
return imagePlayer_Backport
|
||||
}
|
||||
}
|
||||
// FIXME: Use SwiftUI StateObject and remove onPlatformAppear once drop iOS 13 support
|
||||
@Backport.StateObject var imagePlayer = ImagePlayer()
|
||||
|
||||
@SwiftUI.StateObject var imageManager_SwiftUI = ImageManager()
|
||||
@Backport.StateObject var imageManager_Backport = ImageManager()
|
||||
var imageManager: ImageManager {
|
||||
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
|
||||
return imageManager_SwiftUI
|
||||
} else {
|
||||
return imageManager_Backport
|
||||
}
|
||||
}
|
||||
// FIXME: Use SwiftUI StateObject and remove onPlatformAppear once drop iOS 13 support
|
||||
@Backport.StateObject var imageManager : ImageManager
|
||||
|
||||
/// Create a web image with url, placeholder, custom options and context. Optional can support animated image using Binding.
|
||||
/// - Parameter url: The image url
|
||||
|
@ -86,11 +83,14 @@ public struct WebImage : View {
|
|||
context[.animatedImageClass] = SDAnimatedImage.self
|
||||
}
|
||||
}
|
||||
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
|
||||
_imageManager_SwiftUI = SwiftUI.StateObject(wrappedValue: ImageManager(url: url, options: options, context: context))
|
||||
} else {
|
||||
_imageManager_Backport = Backport.StateObject(wrappedValue: ImageManager(url: url, options: options, context: context))
|
||||
}
|
||||
let imageModel = WebImageModel()
|
||||
imageModel.url = url
|
||||
imageModel.options = options
|
||||
imageModel.context = context
|
||||
_imageModel = ObservedObject(wrappedValue: imageModel)
|
||||
let imageManager = ImageManager()
|
||||
_imageManager = Backport.StateObject(wrappedValue: imageManager)
|
||||
_indicatorStatus = ObservedObject(wrappedValue: imageManager.indicatorStatus)
|
||||
}
|
||||
|
||||
/// Create a web image with url, placeholder, custom options and context.
|
||||
|
@ -103,52 +103,38 @@ public struct WebImage : View {
|
|||
|
||||
public var body: some View {
|
||||
return Group {
|
||||
if let image = imageManager.image {
|
||||
// Render Logic
|
||||
if imageManager.image != nil && imageModel.url == imageManager.currentURL {
|
||||
if isAnimating && !imageManager.isIncremental {
|
||||
setupPlayer()
|
||||
.onDisappear {
|
||||
// Only stop the player which is not intermediate status
|
||||
if !imagePlayer.isWaiting {
|
||||
if self.imageConfiguration.pausable {
|
||||
self.imagePlayer.pausePlaying()
|
||||
} else {
|
||||
self.imagePlayer.stopPlaying()
|
||||
}
|
||||
if self.imageConfiguration.purgeable {
|
||||
self.imagePlayer.clearFrameBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let currentFrame = imagePlayer.currentFrame {
|
||||
configure(image: currentFrame)
|
||||
} else {
|
||||
configure(image: image)
|
||||
configure(image: imageManager.image!)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Load Logic
|
||||
setupPlaceholder()
|
||||
.onAppear {
|
||||
self.imageManager.successBlock = self.imageHandler.successBlock
|
||||
self.imageManager.failureBlock = self.imageHandler.failureBlock
|
||||
self.imageManager.progressBlock = self.imageHandler.progressBlock
|
||||
// Load remote image when first appear
|
||||
self.imageManager.load()
|
||||
.onPlatformAppear(appear: {
|
||||
setupManager()
|
||||
if (self.imageManager.error == nil) {
|
||||
// Load remote image when first appear
|
||||
self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context)
|
||||
}
|
||||
guard self.imageConfiguration.retryOnAppear else { return }
|
||||
// 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()
|
||||
if self.imageManager.error != nil && !self.imageManager.isIncremental {
|
||||
self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context)
|
||||
}
|
||||
}.onDisappear {
|
||||
}, disappear: {
|
||||
guard self.imageConfiguration.cancelOnDisappear else { return }
|
||||
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
|
||||
if self.imageManager.image == nil && !self.imageManager.isIncremental {
|
||||
if self.imageManager.error != nil && !self.imageManager.isIncremental {
|
||||
self.imageManager.cancel()
|
||||
}
|
||||
}.onReceive(imageManager.objectWillChange) { _ in
|
||||
indicatorStatus.isLoading = imageManager.isLoading
|
||||
indicatorStatus.progress = imageManager.progress
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -202,40 +188,85 @@ public struct WebImage : View {
|
|||
}
|
||||
}
|
||||
|
||||
/// Image Manager status
|
||||
func setupManager() {
|
||||
self.imageManager.successBlock = self.imageHandler.successBlock
|
||||
self.imageManager.failureBlock = self.imageHandler.failureBlock
|
||||
self.imageManager.progressBlock = self.imageHandler.progressBlock
|
||||
if imageModel.url != imageManager.currentURL {
|
||||
imageManager.cancel()
|
||||
imageManager.image = nil
|
||||
imageManager.imageData = nil
|
||||
imageManager.cacheType = .none
|
||||
imageManager.error = nil
|
||||
imageManager.isIncremental = false
|
||||
imageManager.indicatorStatus.isLoading = false
|
||||
imageManager.indicatorStatus.progress = 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Animated Image Support
|
||||
func setupPlayer() -> some View {
|
||||
if let currentFrame = imagePlayer.currentFrame {
|
||||
return configure(image: currentFrame).onAppear {
|
||||
self.imagePlayer.startPlaying()
|
||||
let disappearAction = {
|
||||
// Only stop the player which is not intermediate status
|
||||
if !imagePlayer.isWaiting {
|
||||
if self.imageConfiguration.pausable {
|
||||
self.imagePlayer.pausePlaying()
|
||||
} else {
|
||||
self.imagePlayer.stopPlaying()
|
||||
}
|
||||
if self.imageConfiguration.purgeable {
|
||||
self.imagePlayer.clearFrameBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
if let currentFrame = imagePlayer.currentFrame, imagePlayer.currentAnimatedImage == imageManager.image! {
|
||||
return configure(image: currentFrame).onPlatformAppear(appear: {
|
||||
self.imagePlayer.startPlaying()
|
||||
}, disappear: {
|
||||
disappearAction()
|
||||
})
|
||||
} else {
|
||||
return configure(image: imageManager.image!).onAppear {
|
||||
if let animatedImage = imageManager.image as? SDAnimatedImageProvider {
|
||||
return configure(image: imageManager.image!).onPlatformAppear(appear: {
|
||||
self.imagePlayer.stopPlaying()
|
||||
if let animatedImage = imageManager.image as? PlatformImage & SDAnimatedImageProvider {
|
||||
// Clear previous status
|
||||
self.imagePlayer.player = nil;
|
||||
self.imagePlayer.currentFrame = nil;
|
||||
self.imagePlayer.currentFrameIndex = 0;
|
||||
self.imagePlayer.currentLoopCount = 0;
|
||||
self.imagePlayer.customLoopCount = self.imageConfiguration.customLoopCount
|
||||
self.imagePlayer.maxBufferSize = self.imageConfiguration.maxBufferSize
|
||||
self.imagePlayer.runLoopMode = self.imageConfiguration.runLoopMode
|
||||
self.imagePlayer.playbackMode = self.imageConfiguration.playbackMode
|
||||
self.imagePlayer.playbackRate = self.imageConfiguration.playbackRate
|
||||
// Setup new player
|
||||
self.imagePlayer.setupPlayer(animatedImage: animatedImage)
|
||||
self.imagePlayer.startPlaying()
|
||||
}
|
||||
}
|
||||
}, disappear: {
|
||||
disappearAction()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder View Support
|
||||
func setupPlaceholder() -> some View {
|
||||
// Don't use `Group` because it will trigger `.onAppear` and `.onDisappear` when condition view removed, treat placeholder as an entire component
|
||||
let result: AnyView
|
||||
if let placeholder = placeholder {
|
||||
// If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :)
|
||||
if imageManager.options.contains(.delayPlaceholder) && imageManager.isLoading {
|
||||
return AnyView(configure(image: .empty))
|
||||
if imageModel.options.contains(.delayPlaceholder) && imageManager.error == nil {
|
||||
result = AnyView(configure(image: .empty))
|
||||
} else {
|
||||
return placeholder
|
||||
result = placeholder
|
||||
}
|
||||
} else {
|
||||
return AnyView(configure(image: .empty))
|
||||
result = AnyView(configure(image: .empty))
|
||||
}
|
||||
// UUID to avoid SwiftUI engine cache the status, and does not call `onAppear` when placeholder not changed (See `ContentView.swift/ContentView2` case)
|
||||
// Because we load the image url in `onAppear`, it should be called to sync with state changes :)
|
||||
return result.id(UUID())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -182,7 +182,7 @@ class AnimatedImageTests: XCTestCase {
|
|||
.animation(.easeInOut)
|
||||
_ = try introspectView.inspect()
|
||||
ViewHosting.host(view: introspectView)
|
||||
self.waitForExpectations(timeout: 5, handler: nil)
|
||||
self.waitForExpectations(timeout: 10, handler: nil)
|
||||
ViewHosting.expel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ class ImageManagerTests: XCTestCase {
|
|||
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)
|
||||
let imageManager = ImageManager()
|
||||
imageManager.setOnSuccess { image, cacheType, data in
|
||||
XCTAssertNotNil(image)
|
||||
expectation.fulfill()
|
||||
|
@ -29,7 +29,7 @@ class ImageManagerTests: XCTestCase {
|
|||
imageManager.setOnProgress { receivedSize, expectedSize in
|
||||
|
||||
}
|
||||
imageManager.load()
|
||||
imageManager.load(url: imageUrl)
|
||||
XCTAssertNotNil(imageManager.currentOperation)
|
||||
let sub = imageManager.objectWillChange
|
||||
.subscribe(on: RunLoop.main)
|
||||
|
@ -38,6 +38,6 @@ class ImageManagerTests: XCTestCase {
|
|||
print(value)
|
||||
}
|
||||
sub.cancel()
|
||||
self.waitForExpectations(timeout: 5, handler: nil)
|
||||
self.waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class WebImageTests: XCTestCase {
|
|||
let imageView = WebImage(url: imageUrl)
|
||||
let introspectView = imageView.onSuccess { image, data, cacheType in
|
||||
#if os(macOS)
|
||||
let displayImage = try? imageView.inspect().group().image(0).actualImage.nsImage()
|
||||
let displayImage = try? imageView.inspect().group().image(0).actualImage().nsImage()
|
||||
#else
|
||||
let displayImage = try? imageView.inspect().group().image(0).actualImage().cgImage()
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue