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:
DreamPiggy 2022-09-14 23:12:18 +08:00
parent 336d3f6d3b
commit 2398f563a5
14 changed files with 549 additions and 161 deletions

View File

@ -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;

View File

@ -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 {

View File

@ -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"
}
}
]

View File

@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,22 +61,29 @@ 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: {
.onDisappear {
// Only stop the player which is not intermediate status
if !imagePlayer.isWaiting {
if self.pausable {
self.imagePlayer.pausePlaying()
} else {
@ -74,7 +92,8 @@ public struct WebImage : View {
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
}
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)
return configure(image: currentFrame).onAppear {
self.imagePlayer.startPlaying()
}
} else {
return configure(image: imageManager.image!).onAppear {
if let animatedImage = imageManager.image as? SDAnimatedImageProvider {
self.imagePlayer.setupPlayer(animatedImage: animatedImage)
self.imagePlayer.startPlaying()
}
return configure(image: imageManager.image!)
}
}
}
@ -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