Skip to content

Commit 6e5f180

Browse files
authored
Merge pull request #72 from SDWebImage/api_webimage_animation
Using the isAnimating arg, instead of protocol extention method to control the `WebImage`'s animation supports
2 parents 7e92d42 + fe54241 commit 6e5f180

File tree

5 files changed

+139
-55
lines changed

5 files changed

+139
-55
lines changed

Example/SDWebImageSwiftUIDemo/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2626
// Dynamic check to support both WebImage/AnimatedImage
2727
SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
2828
var context = context
29-
if let _ = context?[.animatedImageClass] as? SDAnimatedImageProtocol {
29+
if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type {
3030
// AnimatedImage supports vector rendering
3131
} else {
3232
// WebImage supports bitmap rendering only

Example/SDWebImageSwiftUIDemo/ContentView.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,8 @@ struct ContentView: View {
105105
.scaledToFit()
106106
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
107107
#else
108-
WebImage(url: URL(string:url))
108+
WebImage(url: URL(string:url), isAnimating: self.$animated)
109109
.resizable()
110-
.animated()
111110
.indicator { _, _ in
112111
ActivityBar()
113112
.foregroundColor(Color.white)

Example/SDWebImageSwiftUIDemo/DetailView.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,8 @@ struct DetailView: View {
9595
.resizable()
9696
.scaledToFit()
9797
#else
98-
WebImage(url: URL(string:url), options: [.progressiveLoad])
98+
WebImage(url: URL(string:url), options: [.progressiveLoad], isAnimating: $isAnimating)
9999
.resizable()
100-
.animated(isAnimating)
101100
.indicator { isAnimating, progress in
102101
ProgressBar(value: progress)
103102
.foregroundColor(.blue)

SDWebImageSwiftUI/Classes/AnimatedImage.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ final class AnimatedImageLayout : ObservableObject {
6666
final class AnimatedImageConfiguration: ObservableObject {
6767
var incrementalLoad: Bool?
6868
var maxBufferSize: UInt?
69-
var customLoopCount: Int?
69+
var customLoopCount: UInt?
7070
var runLoopMode: RunLoop.Mode?
7171
var pausable: Bool?
7272
var purgeable: Bool?
73-
var playBackRate: Double?
73+
var playbackRate: Double?
7474
// These configurations only useful for web image loading
7575
var indicator: SDWebImageIndicator?
7676
var transition: SDWebImageTransition?
@@ -415,7 +415,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
415415
// CustomLoopCount
416416
if let customLoopCount = imageConfiguration.customLoopCount {
417417
view.wrapped.shouldCustomLoopCount = true
418-
view.wrapped.animationRepeatCount = customLoopCount
418+
view.wrapped.animationRepeatCount = Int(customLoopCount)
419419
} else {
420420
// disable custom loop count
421421
view.wrapped.shouldCustomLoopCount = false
@@ -443,8 +443,8 @@ public struct AnimatedImage : PlatformViewRepresentable {
443443
}
444444

445445
// Playback Rate
446-
if let playBackRate = imageConfiguration.playBackRate {
447-
view.wrapped.playbackRate = playBackRate
446+
if let playbackRate = imageConfiguration.playbackRate {
447+
view.wrapped.playbackRate = playbackRate
448448
} else {
449449
view.wrapped.playbackRate = 1.0
450450
}
@@ -557,7 +557,7 @@ extension AnimatedImage {
557557
/// Total loop count for animated image rendering. Defaults to nil.
558558
/// - Note: Pass nil to disable customization, use the image itself loop count (`animatedImageLoopCount`) instead
559559
/// - Parameter loopCount: The animation loop count
560-
public func customLoopCount(_ loopCount: Int?) -> AnimatedImage {
560+
public func customLoopCount(_ loopCount: UInt?) -> AnimatedImage {
561561
self.imageConfiguration.customLoopCount = loopCount
562562
return self
563563
}
@@ -613,9 +613,9 @@ extension AnimatedImage {
613613
/// `0.0-1.0` means the slow speed.
614614
/// `> 1.0` means the fast speed.
615615
/// `< 0.0` is not supported currently and stop animation. (may support reverse playback in the future)
616-
/// - Parameter playBackRate: The animation playback rate.
617-
public func playBackRate(_ playBackRate: Double) -> AnimatedImage {
618-
self.imageConfiguration.playBackRate = playBackRate
616+
/// - Parameter playbackRate: The animation playback rate.
617+
public func playbackRate(_ playbackRate: Double) -> AnimatedImage {
618+
self.imageConfiguration.playbackRate = playbackRate
619619
return self
620620
}
621621
}

SDWebImageSwiftUI/Classes/WebImage.swift

Lines changed: 127 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,42 @@ public struct WebImage : View {
2121

2222
@ObservedObject var imageManager: ImageManager
2323

24-
// Animated Image support (Beta)
25-
var animated: Bool = false
24+
/// A Binding to control the animation. You can bind external logic to control the animation status.
25+
/// True to start animation, false to stop animation.
26+
@Binding public var isAnimating: Bool
27+
2628
@State var currentFrame: PlatformImage? = nil
2729
@State var imagePlayer: SDAnimatedImagePlayer? = nil
2830

31+
var maxBufferSize: UInt?
32+
var customLoopCount: UInt?
33+
var runLoopMode: RunLoop.Mode = .common
34+
var pausable: Bool = true
35+
var purgeable: Bool = false
36+
var playbackRate: Double = 1.0
37+
2938
/// Create a web image with url, placeholder, custom options and context.
3039
/// - Parameter url: The image url
3140
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
3241
/// - 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.
3342
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
43+
self.init(url: url, options: options, context: context, isAnimating: .constant(false))
44+
}
45+
46+
/// Create a web image with url, placeholder, custom options and context. Optional can support animated image using Binding.
47+
/// - Parameter url: The image url
48+
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
49+
/// - 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.
50+
/// - Parameter isAnimating: The binding for animation control. The binding value should be `true` when initialized to setup the correct animated image class. If not, you must provide the `.animatedImageClass` explicitly. When the animation started, this binding can been used to start / stop the animation.
51+
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
52+
self._isAnimating = isAnimating
53+
var context = context ?? [:]
54+
// provide animated image class if the initialized `isAnimating` is true, user can still custom the image class if they want
55+
if isAnimating.wrappedValue {
56+
if context[.animatedImageClass] == nil {
57+
context[.animatedImageClass] = SDAnimatedImage.self
58+
}
59+
}
3460
self.imageManager = ImageManager(url: url, options: options, context: context)
3561
// load remote image here, SwiftUI sometimes will create a new View struct without calling `onAppear` (like enter EditMode) :)
3662
// this can ensure we load the image, SDWebImage take care of the duplicated query
@@ -40,7 +66,7 @@ public struct WebImage : View {
4066
public var body: some View {
4167
Group {
4268
if imageManager.image != nil {
43-
if animated {
69+
if isAnimating && !self.imageManager.isIncremental {
4470
if currentFrame != nil {
4571
configurations.reduce(Image(platformImage: currentFrame!)) { (previous, configuration) in
4672
configuration(previous)
@@ -49,7 +75,14 @@ public struct WebImage : View {
4975
self.imagePlayer?.startPlaying()
5076
}
5177
.onDisappear {
52-
self.imagePlayer?.pausePlaying()
78+
if self.pausable {
79+
self.imagePlayer?.pausePlaying()
80+
} else {
81+
self.imagePlayer?.stopPlaying()
82+
}
83+
if self.purgeable {
84+
self.imagePlayer?.clearFrameBuffer()
85+
}
5386
}
5487
} else {
5588
configurations.reduce(Image(platformImage: imageManager.image!)) { (previous, configuration) in
@@ -60,8 +93,14 @@ public struct WebImage : View {
6093
}
6194
}
6295
} else {
63-
configurations.reduce(Image(platformImage: imageManager.image!)) { (previous, configuration) in
64-
configuration(previous)
96+
if currentFrame != nil {
97+
configurations.reduce(Image(platformImage: currentFrame!)) { (previous, configuration) in
98+
configuration(previous)
99+
}
100+
} else {
101+
configurations.reduce(Image(platformImage: imageManager.image!)) { (previous, configuration) in
102+
configuration(previous)
103+
}
65104
}
66105
}
67106
} else {
@@ -93,6 +132,32 @@ public struct WebImage : View {
93132
}
94133
}
95134
}
135+
136+
/// Animated Image Support
137+
func setupPlayer(image: PlatformImage?) {
138+
if imagePlayer != nil {
139+
return
140+
}
141+
if let animatedImage = image as? SDAnimatedImageProvider {
142+
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
143+
imagePlayer.animationFrameHandler = { (_, frame) in
144+
self.currentFrame = frame
145+
}
146+
// Setup configuration
147+
if let maxBufferSize = maxBufferSize {
148+
imagePlayer.maxBufferSize = maxBufferSize
149+
}
150+
if let customLoopCount = customLoopCount {
151+
imagePlayer.totalLoopCount = UInt(customLoopCount)
152+
}
153+
imagePlayer.runLoopMode = runLoopMode
154+
imagePlayer.playbackRate = playbackRate
155+
156+
self.imagePlayer = imagePlayer
157+
imagePlayer.startPlaying()
158+
}
159+
}
160+
}
96161
}
97162

98163
// Layout
@@ -223,49 +288,70 @@ extension WebImage {
223288
}
224289
}
225290

226-
// Animated Image support (Beta)
291+
// Animated Image
227292
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
228293
extension WebImage {
229294

230-
/// Make the image to support animated images. The animation will start when view appears, and pause when disappears.
231-
/// - Note: Currently we do not have advanced control like binding, reset frame index, playback rate, etc. For those use case, it's recommend to use `AnimatedImage` type instead. (support iOS/tvOS/macOS)
232-
/// - Warning: This API need polishing. In the future we may choose to create a new View type instead.
295+
/// Total loop count for animated image rendering. Defaults to nil.
296+
/// - Note: Pass nil to disable customization, use the image itself loop count (`animatedImageLoopCount`) instead
297+
/// - Parameter loopCount: The animation loop count
298+
public func customLoopCount(_ loopCount: UInt?) -> WebImage {
299+
var result = self
300+
result.customLoopCount = loopCount
301+
return result
302+
}
303+
304+
/// Provide a max buffer size by bytes. This is used to adjust frame buffer count and can be useful when the decoding cost is expensive (such as Animated WebP software decoding). Default is nil.
233305
///
234-
/// - Parameter animated: Whether or not to enable animationn.
235-
public func animated(_ animated: Bool = true) -> WebImage {
306+
/// `0` or nil means automatically adjust by calculating current memory usage.
307+
/// `1` means without any buffer cache, each of frames will be decoded and then be freed after rendering. (Lowest Memory and Highest CPU)
308+
/// `UInt.max` means cache all the buffer. (Lowest CPU and Highest Memory)
309+
/// - Parameter bufferSize: The max buffer size
310+
public func maxBufferSize(_ bufferSize: UInt?) -> WebImage {
236311
var result = self
237-
result.animated = animated
238-
if animated {
239-
// Update Image Manager
240-
result.imageManager.cancel()
241-
var context = result.imageManager.context ?? [:]
242-
context[.animatedImageClass] = SDAnimatedImage.self
243-
result.imageManager.context = context
244-
result.imageManager.load()
245-
} else {
246-
// Update Image Manager
247-
result.imageManager.cancel()
248-
var context = result.imageManager.context ?? [:]
249-
context[.animatedImageClass] = nil
250-
result.imageManager.context = context
251-
result.imageManager.load()
252-
}
312+
result.maxBufferSize = bufferSize
253313
return result
254314
}
255315

256-
func setupPlayer(image: PlatformImage?) {
257-
if imagePlayer != nil {
258-
return
259-
}
260-
if let animatedImage = image as? SDAnimatedImageProvider {
261-
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
262-
imagePlayer.animationFrameHandler = { (_, frame) in
263-
self.currentFrame = frame
264-
}
265-
self.imagePlayer = imagePlayer
266-
imagePlayer.startPlaying()
267-
}
268-
}
316+
/// The runLoopMode when animation is playing on. Defaults is `.common`
317+
/// You can specify a runloop mode to let it rendering.
318+
/// - Note: This is useful for some cases, for example, always specify NSDefaultRunLoopMode, if you want to pause the animation when user scroll (for Mac user, drag the mouse or touchpad)
319+
/// - Parameter runLoopMode: The runLoopMode for animation
320+
public func runLoopMode(_ runLoopMode: RunLoop.Mode) -> WebImage {
321+
var result = self
322+
result.runLoopMode = runLoopMode
323+
return result
324+
}
325+
326+
/// Whether or not to pause the animation (keep current frame), instead of stop the animation (frame index reset to 0). When `isAnimating` binding value changed to false. Defaults is true.
327+
/// - Note: For some of use case, you may want to reset the frame index to 0 when stop, but some other want to keep the current frame index.
328+
/// - Parameter pausable: Whether or not to pause the animation instead of stop the animation.
329+
public func pausable(_ pausable: Bool) -> WebImage {
330+
var result = self
331+
result.pausable = pausable
332+
return result
333+
}
334+
335+
/// Whether or not to clear frame buffer cache when stopped. Defaults is false.
336+
/// Note: This is useful when you want to limit the memory usage during frequently visibility changes (such as image view inside a list view, then push and pop)
337+
/// - Parameter purgeable: Whether or not to clear frame buffer cache when stopped.
338+
public func purgeable(_ purgeable: Bool) -> WebImage {
339+
var result = self
340+
result.purgeable = purgeable
341+
return result
342+
}
343+
344+
/// Control the animation playback rate. Default is 1.0.
345+
/// `1.0` means the normal speed.
346+
/// `0.0` means stopping the animation.
347+
/// `0.0-1.0` means the slow speed.
348+
/// `> 1.0` means the fast speed.
349+
/// `< 0.0` is not supported currently and stop animation. (may support reverse playback in the future)
350+
/// - Parameter playbackRate: The animation playback rate.
351+
public func playbackRate(_ playbackRate: Double) -> WebImage {
352+
var result = self
353+
result.playbackRate = playbackRate
354+
return result
269355
}
270356
}
271357

0 commit comments

Comments
 (0)