Refactor WebImage implementation
1. Use SwiftUIBackport to use StateObject/OnChange/Overlay 2. Change the Indicator API to match the transform for ImageManager 3. Remove the unused PlatformApear hack
This commit is contained in:
parent
336d3f6d3b
commit
2398f563a5
|
@ -892,7 +892,7 @@
|
|||
ENABLE_PREVIEWS = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = SDWebImageSwiftUIDemo/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
|
@ -926,7 +926,7 @@
|
|||
ENABLE_PREVIEWS = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = SDWebImageSwiftUIDemo/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dreampiggy.SDWebImageSwiftUIDemo;
|
||||
|
|
|
@ -46,7 +46,6 @@ struct ContentView: View {
|
|||
"https://isparta.github.io/compare-webp/image/gif_webp/webp/2.webp",
|
||||
"https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic",
|
||||
"https://nokiatech.github.io/heif/content/image_sequences/starfield_animation.heic",
|
||||
"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",
|
||||
"https://via.placeholder.com/200x200.jpg",
|
||||
|
@ -56,7 +55,7 @@ struct ContentView: View {
|
|||
"https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf",
|
||||
"https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/smartphone_tablet.pdf"
|
||||
]
|
||||
@State var animated: Bool = true // You can change between WebImage/AnimatedImage
|
||||
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
|
||||
@EnvironmentObject var settings: UserSettings
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "e2285181a62daf4d1d3caf66d6d776b667092303",
|
||||
"version": "5.7.0"
|
||||
"revision": "3e48cb68d8e668d146dc59c73fb98cb628616236",
|
||||
"version": "5.13.2"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -87,6 +87,14 @@
|
|||
32D26A032446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; };
|
||||
32D26A042446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; };
|
||||
32D26A052446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; };
|
||||
32E5C96628D1C25B006948E4 /* StateObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5C96428D1C25B006948E4 /* StateObject.swift */; };
|
||||
32E5C96728D1C25B006948E4 /* StateObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5C96428D1C25B006948E4 /* StateObject.swift */; };
|
||||
32E5C96828D1C25B006948E4 /* StateObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5C96428D1C25B006948E4 /* StateObject.swift */; };
|
||||
32E5C96928D1C25B006948E4 /* StateObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5C96428D1C25B006948E4 /* StateObject.swift */; };
|
||||
32E5C96A28D1C25B006948E4 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5C96528D1C25B006948E4 /* Backport.swift */; };
|
||||
32E5C96B28D1C25B006948E4 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5C96528D1C25B006948E4 /* Backport.swift */; };
|
||||
32E5C96C28D1C25B006948E4 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5C96528D1C25B006948E4 /* Backport.swift */; };
|
||||
32E5C96D28D1C25B006948E4 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5C96528D1C25B006948E4 /* Backport.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 */; };
|
||||
|
@ -148,6 +156,8 @@
|
|||
32CBA77E25E4D7D800C6A8DC /* ImagePlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePlayer.swift; sourceTree = "<group>"; };
|
||||
32CBA77F25E4D7D800C6A8DC /* SwiftUICompatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUICompatibility.swift; sourceTree = "<group>"; };
|
||||
32D26A012446B546005905DA /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
|
||||
32E5C96428D1C25B006948E4 /* StateObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateObject.swift; sourceTree = "<group>"; };
|
||||
32E5C96528D1C25B006948E4 /* Backport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
32ED4825242A13030053338E /* ImageManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManagerTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -288,6 +298,7 @@
|
|||
children = (
|
||||
32B933E323659A0700BB7CAD /* Transition */,
|
||||
326099472362E09E006EBB22 /* Indicator */,
|
||||
32E5C96328D1C25B006948E4 /* Backports */,
|
||||
32C43DDC22FD54C600BE87F5 /* ImageManager.swift */,
|
||||
32CBA77E25E4D7D800C6A8DC /* ImagePlayer.swift */,
|
||||
32CBA77F25E4D7D800C6A8DC /* SwiftUICompatibility.swift */,
|
||||
|
@ -311,6 +322,15 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
32E5C96328D1C25B006948E4 /* Backports */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
32E5C96428D1C25B006948E4 /* StateObject.swift */,
|
||||
32E5C96528D1C25B006948E4 /* Backport.swift */,
|
||||
);
|
||||
path = Backports;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
@ -708,12 +728,14 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
32B933E523659A1900BB7CAD /* Transition.swift in Sources */,
|
||||
32E5C96A28D1C25B006948E4 /* Backport.swift in Sources */,
|
||||
32CBA78025E4D7D800C6A8DC /* ImagePlayer.swift in Sources */,
|
||||
32CBA78425E4D7D800C6A8DC /* SwiftUICompatibility.swift in Sources */,
|
||||
32C43E1722FD583700BE87F5 /* WebImage.swift in Sources */,
|
||||
326B848C236335400011BDFB /* ProgressIndicator.swift in Sources */,
|
||||
326B84822363350C0011BDFB /* Indicator.swift in Sources */,
|
||||
32C43E3222FD5DE100BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
|
||||
32E5C96628D1C25B006948E4 /* StateObject.swift in Sources */,
|
||||
326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
|
||||
326B8487236335110011BDFB /* ActivityIndicator.swift in Sources */,
|
||||
32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */,
|
||||
|
@ -727,12 +749,14 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
32B933E623659A1900BB7CAD /* Transition.swift in Sources */,
|
||||
32E5C96B28D1C25B006948E4 /* Backport.swift in Sources */,
|
||||
32CBA78125E4D7D800C6A8DC /* ImagePlayer.swift in Sources */,
|
||||
32CBA78525E4D7D800C6A8DC /* SwiftUICompatibility.swift in Sources */,
|
||||
32C43E1A22FD583700BE87F5 /* WebImage.swift in Sources */,
|
||||
326B848D236335400011BDFB /* ProgressIndicator.swift in Sources */,
|
||||
326B84832363350C0011BDFB /* Indicator.swift in Sources */,
|
||||
32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
|
||||
32E5C96728D1C25B006948E4 /* StateObject.swift in Sources */,
|
||||
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
|
||||
326B8488236335110011BDFB /* ActivityIndicator.swift in Sources */,
|
||||
32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */,
|
||||
|
@ -746,12 +770,14 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
32B933E723659A1900BB7CAD /* Transition.swift in Sources */,
|
||||
32E5C96C28D1C25B006948E4 /* Backport.swift in Sources */,
|
||||
32CBA78225E4D7D800C6A8DC /* ImagePlayer.swift in Sources */,
|
||||
32CBA78625E4D7D800C6A8DC /* SwiftUICompatibility.swift in Sources */,
|
||||
32C43E1D22FD583800BE87F5 /* WebImage.swift in Sources */,
|
||||
326B848E236335400011BDFB /* ProgressIndicator.swift in Sources */,
|
||||
326B84842363350C0011BDFB /* Indicator.swift in Sources */,
|
||||
32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
|
||||
32E5C96828D1C25B006948E4 /* StateObject.swift in Sources */,
|
||||
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
|
||||
326B8489236335110011BDFB /* ActivityIndicator.swift in Sources */,
|
||||
32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */,
|
||||
|
@ -765,12 +791,14 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
32B933E823659A1900BB7CAD /* Transition.swift in Sources */,
|
||||
32E5C96D28D1C25B006948E4 /* Backport.swift in Sources */,
|
||||
32CBA78325E4D7D800C6A8DC /* ImagePlayer.swift in Sources */,
|
||||
32CBA78725E4D7D800C6A8DC /* SwiftUICompatibility.swift in Sources */,
|
||||
32C43E2022FD583800BE87F5 /* WebImage.swift in Sources */,
|
||||
326B848F236335400011BDFB /* ProgressIndicator.swift in Sources */,
|
||||
326B84852363350C0011BDFB /* Indicator.swift in Sources */,
|
||||
32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
|
||||
32E5C96928D1C25B006948E4 /* StateObject.swift in Sources */,
|
||||
326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
|
||||
326B848A236335110011BDFB /* ActivityIndicator.swift in Sources */,
|
||||
32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */,
|
||||
|
|
|
@ -39,10 +39,8 @@ final class AnimatedImageModel : ObservableObject {
|
|||
|
||||
/// Loading 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 AnimatedLoadingModel : ObservableObject, IndicatorReportable {
|
||||
final class AnimatedLoadingModel : ObservableObject {
|
||||
@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
|
||||
|
||||
/// Used for loading status recording to avoid recursive `updateView`. There are 3 types of loading (Name/Data/URL)
|
||||
@Published var imageName: String?
|
||||
|
@ -99,11 +97,14 @@ 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 {
|
||||
@ObservedObject var imageModel = AnimatedImageModel()
|
||||
@ObservedObject var imageLoading = AnimatedLoadingModel()
|
||||
@ObservedObject var imageHandler = AnimatedImageHandler()
|
||||
@ObservedObject var imageLayout = AnimatedImageLayout()
|
||||
@ObservedObject var imageConfiguration = AnimatedImageConfiguration()
|
||||
@Backport.StateObject var imageModel = AnimatedImageModel()
|
||||
@Backport.StateObject var imageLoading = AnimatedLoadingModel()
|
||||
@Backport.StateObject var imageHandler = AnimatedImageHandler()
|
||||
@Backport.StateObject var imageLayout = AnimatedImageLayout()
|
||||
@Backport.StateObject var imageConfiguration = AnimatedImageConfiguration()
|
||||
|
||||
/// A observed object to pass through the image manager loading status to indicator
|
||||
@ObservedObject var indicatorStatus = IndicatorStatus()
|
||||
|
||||
static var viewDestroyBlock: ((PlatformView, Coordinator) -> Void)?
|
||||
|
||||
|
@ -228,7 +229,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
}
|
||||
|
||||
func loadImage(_ view: AnimatedImageViewWrapper, context: Context) {
|
||||
self.imageLoading.isLoading = true
|
||||
self.indicatorStatus.isLoading = true
|
||||
let options = imageModel.webOptions
|
||||
if options.contains(.delayPlaceholder) {
|
||||
self.imageConfiguration.placeholderView?.isHidden = true
|
||||
|
@ -245,7 +246,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
progress = 0
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.imageLoading.progress = progress
|
||||
self.indicatorStatus.progress = progress
|
||||
}
|
||||
self.imageHandler.progressBlock?(receivedSize, expectedSize)
|
||||
}) { (image, data, error, cacheType, finished, _) in
|
||||
|
@ -265,8 +266,8 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
}
|
||||
}
|
||||
self.imageLoading.image = image
|
||||
self.imageLoading.isLoading = false
|
||||
self.imageLoading.progress = 1
|
||||
self.indicatorStatus.isLoading = false
|
||||
self.indicatorStatus.progress = 1
|
||||
if let image = image {
|
||||
self.imageConfiguration.placeholderView?.isHidden = true
|
||||
self.imageHandler.successBlock?(image, data, cacheType)
|
||||
|
@ -309,7 +310,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
imageLoading.imageURL = url
|
||||
} else {
|
||||
// Same URL, check if already loaded
|
||||
if imageLoading.isLoading {
|
||||
if indicatorStatus.isLoading {
|
||||
shouldLoad = false
|
||||
} else if let image = imageLoading.image {
|
||||
shouldLoad = false
|
||||
|
@ -831,7 +832,7 @@ extension AnimatedImage {
|
|||
/// Associate a indicator when loading image with url
|
||||
/// - Parameter indicator: The indicator type, see `Indicator`
|
||||
public func indicator<T>(_ indicator: Indicator<T>) -> some View where T : View {
|
||||
return self.modifier(IndicatorViewModifier(reporter: self.imageLoading, indicator: indicator))
|
||||
return self.modifier(IndicatorViewModifier(status: indicatorStatus, indicator: indicator))
|
||||
}
|
||||
|
||||
/// Associate a indicator when loading image with url, convenient method with block
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import SwiftUI
|
||||
import ObjectiveC
|
||||
|
||||
/// Provides a convenient method for backporting API,
|
||||
/// including types, functions, properties, property wrappers and more.
|
||||
///
|
||||
/// To backport a SwiftUI Label for example, you could apply the
|
||||
/// following extension:
|
||||
///
|
||||
/// extension Backport where Content == Any {
|
||||
/// public struct Label<Title, Icon> { }
|
||||
/// }
|
||||
///
|
||||
/// Now if we want to provide further extensions to our backport type,
|
||||
/// we need to ensure we retain the `Content == Any` generic requirement:
|
||||
///
|
||||
/// extension Backport.Label where Content == Any, Title == Text, Icon == Image {
|
||||
/// public init<S: StringProtocol>(_ title: S, systemName: String) { }
|
||||
/// }
|
||||
///
|
||||
/// In addition to types, we can also provide backports for properties
|
||||
/// and methods:
|
||||
///
|
||||
/// extension Backport.Label where Content: View {
|
||||
/// func onChange<Value: Equatable>(of value: Value, perform action: (Value) -> Void) -> some View {
|
||||
/// // `content` provides access to the extended type
|
||||
/// content.modifier(OnChangeModifier(value, action))
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
public struct Backport<Wrapped> {
|
||||
|
||||
/// The underlying content this backport represents.
|
||||
public let content: Wrapped
|
||||
|
||||
/// Initializes a new Backport for the specified content.
|
||||
/// - Parameter content: The content (type) that's being backported
|
||||
public init(_ content: Wrapped) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension View {
|
||||
/// Wraps a SwiftUI `View` that can be extended to provide backport functionality.
|
||||
var backport: Backport<Self> { .init(self) }
|
||||
}
|
||||
|
||||
public extension NSObjectProtocol {
|
||||
/// Wraps an `NSObject` that can be extended to provide backport functionality.
|
||||
var backport: Backport<Self> { .init(self) }
|
||||
}
|
||||
|
||||
public extension AnyTransition {
|
||||
/// Wraps an `AnyTransition` that can be extended to provide backport functionality.
|
||||
static var backport: Backport<AnyTransition>{
|
||||
Backport(.identity)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS, deprecated: 14.0)
|
||||
@available(macOS, deprecated: 11.0)
|
||||
@available(tvOS, deprecated: 14.0)
|
||||
@available(watchOS, deprecated: 7.0)
|
||||
public extension Backport where Wrapped: View {
|
||||
|
||||
/// Adds a modifier for this view that fires an action when a specific
|
||||
/// value changes.
|
||||
///
|
||||
/// `onChange` is called on the main thread. Avoid performing long-running
|
||||
/// tasks on the main thread. If you need to perform a long-running task in
|
||||
/// response to `value` changing, you should dispatch to a background queue.
|
||||
///
|
||||
/// The new value is passed into the closure.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: The value to observe for changes
|
||||
/// - action: A closure to run when the value changes.
|
||||
/// - newValue: The new value that changed
|
||||
///
|
||||
/// - Returns: A view that fires an action when the specified value changes.
|
||||
@ViewBuilder
|
||||
func onChange<Value: Equatable>(of value: Value, perform action: @escaping (Value) -> Void) -> some View {
|
||||
content.modifier(ChangeModifier(value: value, action: action))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct ChangeModifier<Value: Equatable>: ViewModifier {
|
||||
let value: Value
|
||||
let action: (Value) -> Void
|
||||
|
||||
@State var oldValue: Value?
|
||||
|
||||
init(value: Value, action: @escaping (Value) -> Void) {
|
||||
self.value = value
|
||||
self.action = action
|
||||
_oldValue = .init(initialValue: value)
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onReceive(Just(value)) { newValue in
|
||||
guard newValue != oldValue else { return }
|
||||
action(newValue)
|
||||
oldValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import SwiftUI
|
||||
|
||||
public extension Backport where Wrapped: View {
|
||||
|
||||
/// Layers the views that you specify in front of this view.
|
||||
///
|
||||
/// Use this modifier to place one or more views in front of another view.
|
||||
/// For example, you can place a group of stars on a ``RoundedRectangle``:
|
||||
///
|
||||
/// RoundedRectangle(cornerRadius: 8)
|
||||
/// .frame(width: 200, height: 100)
|
||||
/// .overlay(alignment: .topLeading) { Star(color: .red) }
|
||||
/// .overlay(alignment: .topTrailing) { Star(color: .yellow) }
|
||||
/// .overlay(alignment: .bottomLeading) { Star(color: .green) }
|
||||
/// .overlay(alignment: .bottomTrailing) { Star(color: .blue) }
|
||||
///
|
||||
/// The example above assumes that you've defined a `Star` view with a
|
||||
/// parameterized color:
|
||||
///
|
||||
/// struct Star: View {
|
||||
/// var color = Color.yellow
|
||||
///
|
||||
/// var body: some View {
|
||||
/// Image(systemName: "star.fill")
|
||||
/// .foregroundStyle(color)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// By setting different `alignment` values for each modifier, you make the
|
||||
/// stars appear in different places on the rectangle:
|
||||
///
|
||||
/// ![A screenshot of a rounded rectangle with a star in each corner. The
|
||||
/// star in the upper-left is red; the start in the upper-right is yellow;
|
||||
/// the star in the lower-left is green; the star the lower-right is
|
||||
/// blue.](View-overlay-2)
|
||||
///
|
||||
/// If you specify more than one view in the `content` closure, the modifier
|
||||
/// collects all of the views in the closure into an implicit ``ZStack``,
|
||||
/// taking them in order from back to front. For example, you can place a
|
||||
/// star and a ``Circle`` on a field of ``ShapeStyle/blue``:
|
||||
///
|
||||
/// Color.blue
|
||||
/// .frame(width: 200, height: 200)
|
||||
/// .overlay {
|
||||
/// Circle()
|
||||
/// .frame(width: 100, height: 100)
|
||||
/// Star()
|
||||
/// }
|
||||
///
|
||||
/// Both the overlay modifier and the implicit ``ZStack`` composed from the
|
||||
/// overlay content --- the circle and the star --- use a default
|
||||
/// ``Alignment/center`` alignment. The star appears centered on the circle,
|
||||
/// and both appear as a composite view centered in front of the square:
|
||||
///
|
||||
/// ![A screenshot of a star centered on a circle, which is
|
||||
/// centered on a square.](View-overlay-3)
|
||||
///
|
||||
/// If you specify an alignment for the overlay, it applies to the implicit
|
||||
/// stack rather than to the individual views in the closure. You can see
|
||||
/// this if you add the ``Alignment/bottom`` alignment:
|
||||
///
|
||||
/// Color.blue
|
||||
/// .frame(width: 200, height: 200)
|
||||
/// .overlay(alignment: .bottom) {
|
||||
/// Circle()
|
||||
/// .frame(width: 100, height: 100)
|
||||
/// Star()
|
||||
/// }
|
||||
///
|
||||
/// The circle and the star move down as a unit to align the stack's bottom
|
||||
/// edge with the bottom edge of the square, while the star remains
|
||||
/// centered on the circle:
|
||||
///
|
||||
/// ![A screenshot of a star centered on a circle, which is on a square.
|
||||
/// The circle's bottom edge is aligned with the square's bottom
|
||||
/// edge.](View-overlay-3a)
|
||||
///
|
||||
/// To control the placement of individual items inside the `content`
|
||||
/// closure, either use a different overlay modifier for each item, as the
|
||||
/// earlier example of stars in the corners of a rectangle demonstrates, or
|
||||
/// add an explicit ``ZStack`` inside the content closure with its own
|
||||
/// alignment:
|
||||
///
|
||||
/// Color.blue
|
||||
/// .frame(width: 200, height: 200)
|
||||
/// .overlay(alignment: .bottom) {
|
||||
/// ZStack(alignment: .bottom) {
|
||||
/// Circle()
|
||||
/// .frame(width: 100, height: 100)
|
||||
/// Star()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// The stack alignment ensures that the star's bottom edge aligns with the
|
||||
/// circle's, while the overlay aligns the composite view with the square:
|
||||
///
|
||||
/// ![A screenshot of a star, a circle, and a square with all their
|
||||
/// bottom edges aligned.](View-overlay-4)
|
||||
///
|
||||
/// You can achieve layering without an overlay modifier by putting both the
|
||||
/// modified view and the overlay content into a ``ZStack``. This can
|
||||
/// produce a simpler view hierarchy, but changes the layout priority that
|
||||
/// SwiftUI applies to the views. Use the overlay modifier when you want the
|
||||
/// modified view to dominate the layout.
|
||||
///
|
||||
/// If you want to specify a ``ShapeStyle`` like a ``Color`` or a
|
||||
/// ``Material`` as the overlay, use
|
||||
/// ``View/overlay(_:ignoresSafeAreaEdges:)`` instead. To specify a
|
||||
/// ``Shape``, use ``View/overlay(_:in:fillStyle:)``.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - alignment: The alignment that the modifier uses to position the
|
||||
/// implicit ``ZStack`` that groups the foreground views. The default
|
||||
/// is ``Alignment/center``.
|
||||
/// - content: A ``ViewBuilder`` that you use to declare the views to
|
||||
/// draw in front of this view, stacked in the order that you list them.
|
||||
/// The last view that you list appears at the front of the stack.
|
||||
///
|
||||
/// - Returns: A view that uses the specified content as a foreground.
|
||||
func overlay<Content: View>(alignment: Alignment = .center, @ViewBuilder _ content: () -> Content) -> some View {
|
||||
self.content.overlay(content(), alignment: alignment)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS, deprecated: 14.0)
|
||||
@available(macOS, deprecated: 11.0)
|
||||
@available(tvOS, deprecated: 14.0)
|
||||
@available(watchOS, deprecated: 7.0)
|
||||
public extension Backport where Wrapped: ObservableObject {
|
||||
|
||||
/// A property wrapper type that instantiates an observable object.
|
||||
///
|
||||
/// Create a state object in a ``SwiftUI/View``, ``SwiftUI/App``, or
|
||||
/// ``SwiftUI/Scene`` by applying the `@Backport.StateObject` attribute to a property
|
||||
/// declaration and providing an initial value that conforms to the
|
||||
/// <doc://com.apple.documentation/documentation/Combine/ObservableObject>
|
||||
/// protocol:
|
||||
///
|
||||
/// @Backport.StateObject var model = DataModel()
|
||||
///
|
||||
/// SwiftUI creates a new instance of the object only once for each instance of
|
||||
/// the structure that declares the object. When published properties of the
|
||||
/// observable object change, SwiftUI updates the parts of any view that depend
|
||||
/// on those properties:
|
||||
///
|
||||
/// Text(model.title) // Updates the view any time `title` changes.
|
||||
///
|
||||
/// You can pass the state object into a property that has the
|
||||
/// ``SwiftUI/ObservedObject`` attribute. You can alternatively add the object
|
||||
/// to the environment of a view hierarchy by applying the
|
||||
/// ``SwiftUI/View/environmentObject(_:)`` modifier:
|
||||
///
|
||||
/// ContentView()
|
||||
/// .environmentObject(model)
|
||||
///
|
||||
/// If you create an environment object as shown in the code above, you can
|
||||
/// read the object inside `ContentView` or any of its descendants
|
||||
/// using the ``SwiftUI/EnvironmentObject`` attribute:
|
||||
///
|
||||
/// @EnvironmentObject var model: DataModel
|
||||
///
|
||||
/// Get a ``SwiftUI/Binding`` to one of the state object's properties using the
|
||||
/// `$` operator. Use a binding when you want to create a two-way connection to
|
||||
/// one of the object's properties. For example, you can let a
|
||||
/// ``SwiftUI/Toggle`` control a Boolean value called `isEnabled` stored in the
|
||||
/// model:
|
||||
///
|
||||
/// Toggle("Enabled", isOn: $model.isEnabled)
|
||||
@propertyWrapper struct StateObject: DynamicProperty {
|
||||
private final class Wrapper: ObservableObject {
|
||||
private var subject = PassthroughSubject<Void, Never>()
|
||||
|
||||
var value: Wrapped? {
|
||||
didSet {
|
||||
cancellable = nil
|
||||
cancellable = value?.objectWillChange
|
||||
.sink { [subject] _ in subject.send() }
|
||||
}
|
||||
}
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
var objectWillChange: AnyPublisher<Void, Never> {
|
||||
subject.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
@State private var state = Wrapper()
|
||||
|
||||
@ObservedObject private var observedObject = Wrapper()
|
||||
|
||||
private var thunk: () -> Wrapped
|
||||
|
||||
/// The underlying value referenced by the state object.
|
||||
///
|
||||
/// The wrapped value property provides primary access to the value's data.
|
||||
/// However, you don't access `wrappedValue` directly. Instead, use the
|
||||
/// property variable created with the `@Backport.StateObject` attribute:
|
||||
///
|
||||
/// @Backport.StateObject var contact = Contact()
|
||||
///
|
||||
/// var body: some View {
|
||||
/// Text(contact.name) // Accesses contact's wrapped value.
|
||||
/// }
|
||||
///
|
||||
/// When you change a property of the wrapped value, you can access the new
|
||||
/// value immediately. However, SwiftUI updates views displaying the value
|
||||
/// asynchronously, so the user interface might not update immediately.
|
||||
public var wrappedValue: Wrapped {
|
||||
if let object = state.value {
|
||||
return object
|
||||
} else {
|
||||
let object = thunk()
|
||||
state.value = object
|
||||
return object
|
||||
}
|
||||
}
|
||||
|
||||
/// A projection of the state object that creates bindings to its
|
||||
/// properties.
|
||||
///
|
||||
/// Use the projected value to pass a binding value down a view hierarchy.
|
||||
/// To get the projected value, prefix the property variable with `$`. For
|
||||
/// example, you can get a binding to a model's `isEnabled` Boolean so that
|
||||
/// a ``SwiftUI/Toggle`` view can control the value:
|
||||
///
|
||||
/// struct MyView: View {
|
||||
/// @Backport.StateObject var model = DataModel()
|
||||
///
|
||||
/// var body: some View {
|
||||
/// Toggle("Enabled", isOn: $model.isEnabled)
|
||||
/// }
|
||||
/// }
|
||||
public var projectedValue: ObservedObject<Wrapped>.Wrapper {
|
||||
ObservedObject(wrappedValue: wrappedValue).projectedValue
|
||||
}
|
||||
|
||||
/// Creates a new state object with an initial wrapped value.
|
||||
///
|
||||
/// You don’t call this initializer directly. Instead, declare a property
|
||||
/// with the `@Backport.StateObject` attribute in a ``SwiftUI/View``,
|
||||
/// ``SwiftUI/App``, or ``SwiftUI/Scene``, and provide an initial value:
|
||||
///
|
||||
/// struct MyView: View {
|
||||
/// @Backport.StateObject var model = DataModel()
|
||||
///
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// SwiftUI creates only one instance of the state object for each
|
||||
/// container instance that you declare. In the code above, SwiftUI
|
||||
/// creates `model` only the first time it initializes a particular instance
|
||||
/// of `MyView`. On the other hand, each different instance of `MyView`
|
||||
/// receives a distinct copy of the data model.
|
||||
///
|
||||
/// - Parameter thunk: An initial value for the state object.
|
||||
public init(wrappedValue thunk: @autoclosure @escaping () -> Wrapped) {
|
||||
self.thunk = thunk
|
||||
}
|
||||
|
||||
public mutating func update() {
|
||||
if state.value == nil {
|
||||
state.value = thunk()
|
||||
}
|
||||
if observedObject.value !== state.value {
|
||||
observedObject.value = state.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -28,13 +28,12 @@ public final class ImageManager : ObservableObject {
|
|||
/// true means during incremental loading
|
||||
@Published public var isIncremental: Bool = false
|
||||
|
||||
var manager: SDWebImageManager
|
||||
var manager: SDWebImageManager?
|
||||
weak var currentOperation: SDWebImageOperation? = nil
|
||||
var isFirstLoad: Bool = true // false after first call `load()`
|
||||
|
||||
var url: URL?
|
||||
var options: SDWebImageOptions
|
||||
var context: [SDWebImageContextOption : Any]?
|
||||
var options: SDWebImageOptions = []
|
||||
var context: [SDWebImageContextOption : Any]? = nil
|
||||
var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)?
|
||||
var failureBlock: ((Error) -> Void)?
|
||||
var progressBlock: ((Int, Int) -> Void)?
|
||||
|
@ -54,9 +53,29 @@ public final class ImageManager : ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
public var isValid: Bool {
|
||||
manager != nil
|
||||
}
|
||||
|
||||
/// Update the manager with new url, options and context. This is not designed to be used outsize, only provided for `@StateObject`. Must call setup after `init()`
|
||||
func setup(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
|
||||
} else {
|
||||
self.manager = .shared
|
||||
}
|
||||
}
|
||||
|
||||
/// Start to load the url operation
|
||||
public func load() {
|
||||
isFirstLoad = false
|
||||
guard let manager = manager else {
|
||||
return
|
||||
}
|
||||
if currentOperation != nil {
|
||||
return
|
||||
}
|
||||
|
@ -138,7 +157,3 @@ extension ImageManager {
|
|||
self.progressBlock = action
|
||||
}
|
||||
}
|
||||
|
||||
// Indicator Reportor
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
extension ImageManager: IndicatorReportable {}
|
||||
|
|
|
@ -14,6 +14,10 @@ import SDWebImage
|
|||
public final class ImagePlayer : ObservableObject {
|
||||
var player: SDAnimatedImagePlayer?
|
||||
|
||||
var waitingPlaying = false
|
||||
|
||||
public var currentView: Image?
|
||||
|
||||
/// Max buffer size
|
||||
public var maxBufferSize: UInt?
|
||||
|
||||
|
@ -48,6 +52,14 @@ public final class ImagePlayer : ObservableObject {
|
|||
player != nil
|
||||
}
|
||||
|
||||
/// The player is preparing to resume from previous stop state. This is intermediate status when previous frame disappear and new frame appear
|
||||
public var isWaiting: Bool {
|
||||
if let player = player {
|
||||
return player.isPlaying && waitingPlaying
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Current playing status
|
||||
public var isPlaying: Bool {
|
||||
player?.isPlaying ?? false
|
||||
|
@ -56,6 +68,12 @@ public final class ImagePlayer : ObservableObject {
|
|||
/// Start the animation
|
||||
public func startPlaying() {
|
||||
player?.startPlaying()
|
||||
waitingPlaying = true
|
||||
DispatchQueue.main.async {
|
||||
// This workaround `WebImage` caller
|
||||
// Which previous frame onDisappear and new frame onAppear, cause player status wrong
|
||||
self.waitingPlaying = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Pause the animation
|
||||
|
|
|
@ -24,23 +24,23 @@ public struct Indicator<T> where T : View {
|
|||
}
|
||||
}
|
||||
|
||||
/// A protocol to report indicator progress
|
||||
/// A observable model to report indicator loading status
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
public protocol IndicatorReportable : ObservableObject {
|
||||
public class IndicatorStatus : ObservableObject {
|
||||
/// whether indicator is loading or not
|
||||
var isLoading: Bool { get set }
|
||||
@Published var isLoading: Bool = false
|
||||
/// indicator progress, should only be used for indicator binding, value between [0.0, 1.0]
|
||||
var progress: Double { get set }
|
||||
@Published var progress: Double = 0
|
||||
}
|
||||
|
||||
/// A implementation detail View Modifier with indicator
|
||||
/// SwiftUI View Modifier construced by using a internal View type which modify the `body`
|
||||
/// It use type system to represent the view hierarchy, and Swift `some View` syntax to hide the type detail for users
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
public struct IndicatorViewModifier<T, V> : ViewModifier where T : View, V : IndicatorReportable {
|
||||
public struct IndicatorViewModifier<T> : ViewModifier where T : View {
|
||||
|
||||
/// The progress reporter
|
||||
@ObservedObject public var reporter: V
|
||||
/// The loading status
|
||||
@ObservedObject public var status: IndicatorStatus
|
||||
|
||||
/// The indicator
|
||||
public var indicator: Indicator<T>
|
||||
|
@ -48,8 +48,11 @@ public struct IndicatorViewModifier<T, V> : ViewModifier where T : View, V : Ind
|
|||
public func body(content: Content) -> some View {
|
||||
ZStack {
|
||||
content
|
||||
if reporter.isLoading {
|
||||
indicator.content($reporter.isLoading, $reporter.progress)
|
||||
.backport
|
||||
.overlay {
|
||||
if status.isLoading {
|
||||
indicator.content($status.isLoading, $status.progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
appearAction()
|
||||
} else {
|
||||
disappearAction()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
override func viewWillMove(toWindow newWindow: NSWindow?) {
|
||||
if newWindow != nil {
|
||||
appearAction()
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -20,20 +20,31 @@ public struct WebImage : View {
|
|||
var pausable: Bool = true
|
||||
var purgeable: Bool = false
|
||||
|
||||
@ObservedObject var imageManager: ImageManager
|
||||
|
||||
/// A Binding to control the animation. You can bind external logic to control the animation status.
|
||||
/// True to start animation, false to stop animation.
|
||||
@Binding public var isAnimating: Bool
|
||||
|
||||
@ObservedObject var imagePlayer: ImagePlayer
|
||||
/// A observed object to pass through the image manager loading status to indicator
|
||||
@ObservedObject var indicatorStatus = IndicatorStatus()
|
||||
|
||||
/// Create a web image with url, placeholder, 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.init(url: url, options: options, context: context, isAnimating: .constant(true))
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a web image with url, placeholder, custom options and context. Optional can support animated image using Binding.
|
||||
|
@ -50,31 +61,39 @@ public struct WebImage : View {
|
|||
context[.animatedImageClass] = SDAnimatedImage.self
|
||||
}
|
||||
}
|
||||
self.imageManager = ImageManager(url: url, options: options, context: context)
|
||||
self.imagePlayer = ImagePlayer()
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a web image with url, placeholder, 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.init(url: url, options: options, context: context, isAnimating: .constant(true))
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
// This solve the case when WebImage created with new URL, but `onAppear` not been called, for example, some transaction indeterminate state, SwiftUI :)
|
||||
if imageManager.isFirstLoad {
|
||||
imageManager.load()
|
||||
}
|
||||
return Group {
|
||||
if let image = imageManager.image {
|
||||
if isAnimating && !imageManager.isIncremental {
|
||||
setupPlayer()
|
||||
.onPlatformAppear(appear: {
|
||||
self.imagePlayer.startPlaying()
|
||||
}, disappear: {
|
||||
if self.pausable {
|
||||
self.imagePlayer.pausePlaying()
|
||||
} else {
|
||||
self.imagePlayer.stopPlaying()
|
||||
.onDisappear {
|
||||
// Only stop the player which is not intermediate status
|
||||
if !imagePlayer.isWaiting {
|
||||
if self.pausable {
|
||||
self.imagePlayer.pausePlaying()
|
||||
} else {
|
||||
self.imagePlayer.stopPlaying()
|
||||
}
|
||||
if self.purgeable {
|
||||
self.imagePlayer.clearFrameBuffer()
|
||||
}
|
||||
}
|
||||
if self.purgeable {
|
||||
self.imagePlayer.clearFrameBuffer()
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if let currentFrame = imagePlayer.currentFrame {
|
||||
configure(image: currentFrame)
|
||||
|
@ -84,24 +103,24 @@ public struct WebImage : View {
|
|||
}
|
||||
} else {
|
||||
setupPlaceholder()
|
||||
.onPlatformAppear(appear: {
|
||||
.onAppear {
|
||||
// Load remote image when first appear
|
||||
if self.imageManager.isFirstLoad {
|
||||
self.imageManager.load()
|
||||
return
|
||||
}
|
||||
self.imageManager.load()
|
||||
guard self.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()
|
||||
}
|
||||
}, disappear: {
|
||||
}.onDisappear {
|
||||
guard self.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 {
|
||||
self.imageManager.cancel()
|
||||
}
|
||||
})
|
||||
}.onReceive(imageManager.objectWillChange) { _ in
|
||||
indicatorStatus.isLoading = imageManager.isLoading
|
||||
indicatorStatus.progress = imageManager.progress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -158,13 +177,16 @@ public struct WebImage : View {
|
|||
/// Animated Image Support
|
||||
func setupPlayer() -> some View {
|
||||
if let currentFrame = imagePlayer.currentFrame {
|
||||
return configure(image: currentFrame)
|
||||
} else {
|
||||
if let animatedImage = imageManager.image as? SDAnimatedImageProvider {
|
||||
self.imagePlayer.setupPlayer(animatedImage: animatedImage)
|
||||
return configure(image: currentFrame).onAppear {
|
||||
self.imagePlayer.startPlaying()
|
||||
}
|
||||
return configure(image: imageManager.image!)
|
||||
} else {
|
||||
return configure(image: imageManager.image!).onAppear {
|
||||
if let animatedImage = imageManager.image as? SDAnimatedImageProvider {
|
||||
self.imagePlayer.setupPlayer(animatedImage: animatedImage)
|
||||
self.imagePlayer.startPlaying()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,7 +324,7 @@ extension WebImage {
|
|||
/// Associate a indicator when loading image with url
|
||||
/// - Parameter indicator: The indicator type, see `Indicator`
|
||||
public func indicator<T>(_ indicator: Indicator<T>) -> some View where T : View {
|
||||
return self.modifier(IndicatorViewModifier(reporter: imageManager, indicator: indicator))
|
||||
return self.modifier(IndicatorViewModifier(status: indicatorStatus, indicator: indicator))
|
||||
}
|
||||
|
||||
/// Associate a indicator when loading image with url, convenient method with block
|
||||
|
|
Loading…
Reference in New Issue