diff --git a/Demo/Demo-iOS/ViewController.swift b/Demo/Demo-iOS/ViewController.swift index 5eb0978..8e68a46 100755 --- a/Demo/Demo-iOS/ViewController.swift +++ b/Demo/Demo-iOS/ViewController.swift @@ -43,6 +43,8 @@ class ViewController: UIViewController { DispatchQueue.main.async { self.imageDataLabel.text = self.currentGIFName.capitalized + " (\(self.imageView.frameCount) frames / \(String(format: "%.2f", self.imageView.gifLoopDuration))s)" } + }, loopBlock: { + print("Loop finished") }) } } diff --git a/Sources/Gifu/Classes/Animator.swift b/Sources/Gifu/Classes/Animator.swift index d386444..e3d70ed 100644 --- a/Sources/Gifu/Classes/Animator.swift +++ b/Sources/Gifu/Classes/Animator.swift @@ -23,9 +23,13 @@ public class Animator { /// A delegate responsible for displaying the GIF frames. private weak var delegate: GIFAnimatable! - + + /// Callback for when all the loops of the animation are done (never called for infinite loops) private var animationBlock: (() -> Void)? = nil + /// Callback for when a loop is done (at the end of each loop) + private var loopBlock: (() -> Void)? = nil + /// Responsible for starting and stopping the animation. private lazy var displayLink: CADisplayLink = { [unowned self] in self.displayLinkInitialized = true @@ -65,7 +69,12 @@ public class Animator { } store.shouldChangeFrame(with: displayLink.duration) { - if $0 { delegate.animatorHasNewFrame() } + if $0 { + delegate.animatorHasNewFrame() + if store.isLoopFinished, let loopBlock = loopBlock { + loopBlock() + } + } } } @@ -136,9 +145,12 @@ public class Animator { /// - parameter size: The target size of the individual frames. /// - parameter contentMode: The view content mode to use for the individual frames. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. - /// - parameter completionHandler: Completion callback function - func animate(withGIFNamed imageName: String, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil) { + /// - parameter preparationBlock: Callback for when preparation is done + /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) + /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) + func animate(withGIFNamed imageName: String, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, loopBlock: (() -> Void)? = nil) { self.animationBlock = animationBlock + self.loopBlock = loopBlock prepareForAnimation(withGIFNamed: imageName, size: size, contentMode: contentMode, @@ -153,9 +165,12 @@ public class Animator { /// - parameter size: The target size of the individual frames. /// - parameter contentMode: The view content mode to use for the individual frames. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. - /// - parameter completionHandler: Completion callback function - func animate(withGIFData imageData: Data, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil) { + /// - parameter preparationBlock: Callback for when preparation is done + /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) + /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) + func animate(withGIFData imageData: Data, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, loopBlock: (() -> Void)? = nil) { self.animationBlock = animationBlock + self.loopBlock = loopBlock prepareForAnimation(withGIFData: imageData, size: size, contentMode: contentMode, diff --git a/Sources/Gifu/Classes/FrameStore.swift b/Sources/Gifu/Classes/FrameStore.swift index fec63ac..2585fd4 100644 --- a/Sources/Gifu/Classes/FrameStore.swift +++ b/Sources/Gifu/Classes/FrameStore.swift @@ -7,8 +7,11 @@ class FrameStore { /// Total duration of one animation loop var loopDuration: TimeInterval = 0 - - /// Flag indicating if number of loops has been reached + + /// Flag indicating that a single loop has finished + var isLoopFinished: Bool = false + + /// Flag indicating if number of loops has been reached (never true for infinite loop) var isFinished: Bool = false /// Desired number of loops, <= 0 for infinite loop @@ -212,10 +215,16 @@ private extension FrameStore { /// Increments the `currentFrameIndex` property. func incrementCurrentFrameIndex() { currentFrameIndex = increment(frameIndex: currentFrameIndex) - if isLastLoop(loopIndex: currentLoop) && isLastFrame(frameIndex: currentFrameIndex) { + if isLastFrame(frameIndex: currentFrameIndex) { + isLoopFinished = true + if isLastLoop(loopIndex: currentLoop) { isFinished = true - } else if currentFrameIndex == 0 { + } + } else { + isLoopFinished = false + if currentFrameIndex == 0 { currentLoop = currentLoop + 1 + } } } diff --git a/Sources/Gifu/Classes/GIFAnimatable.swift b/Sources/Gifu/Classes/GIFAnimatable.swift index c380a28..8a95ed2 100644 --- a/Sources/Gifu/Classes/GIFAnimatable.swift +++ b/Sources/Gifu/Classes/GIFAnimatable.swift @@ -56,36 +56,44 @@ extension GIFAnimatable { /// /// - parameter imageName: The file name of the GIF in the main bundle. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. - /// - parameter completionHandler: Completion callback function - public func animate(withGIFNamed imageName: String, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil) { + /// - parameter preparationBlock: Callback for when preparation is done + /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) + /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) + public func animate(withGIFNamed imageName: String, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, loopBlock: (() -> Void)? = nil) { animator?.animate(withGIFNamed: imageName, size: frame.size, contentMode: contentMode, loopCount: loopCount, preparationBlock: preparationBlock, - animationBlock: animationBlock) + animationBlock: animationBlock, + loopBlock: loopBlock) } /// Prepare for animation and start animating immediately. /// /// - parameter imageData: GIF image data. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. - /// - parameter completionHandler: Completion callback function - public func animate(withGIFData imageData: Data, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil) { + /// - parameter preparationBlock: Callback for when preparation is done + /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) + /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) + public func animate(withGIFData imageData: Data, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, loopBlock: (() -> Void)? = nil) { animator?.animate(withGIFData: imageData, size: frame.size, contentMode: contentMode, loopCount: loopCount, preparationBlock: preparationBlock, - animationBlock: animationBlock) + animationBlock: animationBlock, + loopBlock: loopBlock) } /// Prepare for animation and start animating immediately. /// /// - parameter imageURL: GIF image url. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. - /// - parameter completionHandler: Completion callback function - public func animate(withGIFURL imageURL: URL, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil) { + /// - parameter preparationBlock: Callback for when preparation is done + /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) + /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) + public func animate(withGIFURL imageURL: URL, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, loopBlock: (() -> Void)? = nil) { let session = URLSession.shared let task = session.dataTask(with: imageURL) { (data, response, error) in @@ -94,7 +102,7 @@ extension GIFAnimatable { print("Error downloading gif:", error.localizedDescription, "at url:", imageURL.absoluteString) case (let data?, _, _): DispatchQueue.main.async { - self.animate(withGIFData: data, loopCount: loopCount, preparationBlock: preparationBlock, animationBlock: animationBlock) + self.animate(withGIFData: data, loopCount: loopCount, preparationBlock: preparationBlock, animationBlock: animationBlock, loopBlock: loopBlock) } default: () } @@ -107,6 +115,7 @@ extension GIFAnimatable { /// /// - parameter imageName: The file name of the GIF in the main bundle. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. + /// - parameter completionHandler: Callback for when preparation is done public func prepareForAnimation(withGIFNamed imageName: String, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) { @@ -117,10 +126,11 @@ extension GIFAnimatable { completionHandler: completionHandler) } - /// Prepare for animation and start animating immediately. + /// Prepares the animator instance for animation. /// /// - parameter imageData: GIF image data. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. + /// - parameter completionHandler: Callback for when preparation is done public func prepareForAnimation(withGIFData imageData: Data, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) { @@ -135,10 +145,11 @@ extension GIFAnimatable { completionHandler: completionHandler) } - /// Prepare for animation and start animating immediately. + /// Prepares the animator instance for animation. /// /// - parameter imageURL: GIF image url. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. + /// - parameter completionHandler: Callback for when preparation is done public func prepareForAnimation(withGIFURL imageURL: URL, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) { diff --git a/Tests/GifuTests.swift b/Tests/GifuTests.swift index ee56241..06272f0 100644 --- a/Tests/GifuTests.swift +++ b/Tests/GifuTests.swift @@ -90,6 +90,61 @@ class GifuTests: XCTestCase { } } } + + func testFinishedStates() { + animator = Animator(withDelegate: delegate) + animator.prepareForAnimation(withGIFData: imageData, size: staticImage.size, contentMode: .scaleToFill, loopCount: 2) + + XCTAssertNotNil(animator.frameStore) + guard let store = animator.frameStore else { return } + + let expectation = self.expectation(description: "testFinishedStatesAreSetCorrectly") + + store.prepareFrames { + let animatedFrameCount = store.animatedFrames.count + XCTAssertEqual(store.currentFrameIndex, 0) + + // Animate through all the frames (first loop) + for frame in 1.. Data {