diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 7e47c06..10e3682 100755 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -7,16 +7,35 @@ objects = { /* Begin PBXBuildFile section */ - 005656EB1A6EE471008A0ED1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 005656EA1A6EE471008A0ED1 /* Gifu.framework */; }; 9D25870819BCCB0F00A55A18 /* mugen.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9D25870619BCCB0F00A55A18 /* mugen.gif */; }; 9D98823D19BC69CA00B790C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823C19BC69CA00B790C6 /* AppDelegate.swift */; }; 9D98823F19BC69CA00B790C6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823E19BC69CA00B790C6 /* ViewController.swift */; }; 9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D98824319BC69CA00B790C6 /* Images.xcassets */; }; 9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D98825919BC69F600B790C6 /* Main.storyboard */; }; 9D98826719BC874C00B790C6 /* FlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98826619BC874C00B790C6 /* FlatButton.swift */; }; + EA5789A01B20C5B100A9F7D1 /* almost_nailed_it.gif in Resources */ = {isa = PBXBuildFile; fileRef = EA57899F1B20C5B100A9F7D1 /* almost_nailed_it.gif */; }; + EA5789A71B20C65E00A9F7D1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA5789A61B20C65800A9F7D1 /* Gifu.framework */; }; + EA5789A91B20C68B00A9F7D1 /* Gifu.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA5789A61B20C65800A9F7D1 /* Gifu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; EA9299291AE99E2900E22976 /* Runes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA9299281AE99E2900E22976 /* Runes.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + EA5789A51B20C65800A9F7D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 00B8C73E1A364DA400C188E7; + remoteInfo = Gifu; + }; + EA5789AA1B20C68B00A9F7D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 00B8C73D1A364DA400C188E7; + remoteInfo = Gifu; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 00B8C7331A364D4C00C188E7 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -24,6 +43,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + EA5789A91B20C68B00A9F7D1 /* Gifu.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -31,7 +51,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 005656EA1A6EE471008A0ED1 /* Gifu.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Gifu.framework; path = "../../build/Debug-iphoneos/Gifu.framework"; sourceTree = ""; }; 9D25870519BCCB0F00A55A18 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D25870619BCCB0F00A55A18 /* mugen.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = mugen.gif; sourceTree = ""; }; 9D98823719BC69CA00B790C6 /* gifu-demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gifu-demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -40,6 +59,8 @@ 9D98824319BC69CA00B790C6 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 9D98825919BC69F600B790C6 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 9D98826619BC874C00B790C6 /* FlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FlatButton.swift; path = classes/FlatButton.swift; sourceTree = ""; }; + EA57899F1B20C5B100A9F7D1 /* almost_nailed_it.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = almost_nailed_it.gif; sourceTree = ""; }; + EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Gifu.xcodeproj; path = ../Gifu.xcodeproj; sourceTree = ""; }; EA9299281AE99E2900E22976 /* Runes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Runes.framework; path = ../Carthage/Build/iOS/Runes.framework; sourceTree = ""; }; /* End PBXFileReference section */ @@ -48,8 +69,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 005656EB1A6EE471008A0ED1 /* Gifu.framework in Frameworks */, EA9299291AE99E2900E22976 /* Runes.framework in Frameworks */, + EA5789A71B20C65E00A9F7D1 /* Gifu.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,16 +111,25 @@ 9D98823A19BC69CA00B790C6 /* Supporting Files */ = { isa = PBXGroup; children = ( - 005656EA1A6EE471008A0ED1 /* Gifu.framework */, 9D25870519BCCB0F00A55A18 /* Info.plist */, + EA57899F1B20C5B100A9F7D1 /* almost_nailed_it.gif */, 9D25870619BCCB0F00A55A18 /* mugen.gif */, ); name = "Supporting Files"; sourceTree = ""; }; + EA5789A21B20C65800A9F7D1 /* Products */ = { + isa = PBXGroup; + children = ( + EA5789A61B20C65800A9F7D1 /* Gifu.framework */, + ); + name = Products; + sourceTree = ""; + }; EA92992C1AE9AB2100E22976 /* Frameworks */ = { isa = PBXGroup; children = ( + EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */, EA9299281AE99E2900E22976 /* Runes.framework */, ); name = Frameworks; @@ -121,6 +151,7 @@ buildRules = ( ); dependencies = ( + EA5789AB1B20C68B00A9F7D1 /* PBXTargetDependency */, ); name = "gifu-demo"; productName = "gifu-demo"; @@ -152,6 +183,12 @@ mainGroup = 9D98822E19BC69CA00B790C6; productRefGroup = 9D98823819BC69CA00B790C6 /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = EA5789A21B20C65800A9F7D1 /* Products */; + ProjectRef = EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */; + }, + ); projectRoot = ""; targets = ( 9D98823619BC69CA00B790C6 /* gifu-demo */, @@ -159,11 +196,22 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + EA5789A61B20C65800A9F7D1 /* Gifu.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Gifu.framework; + remoteRef = EA5789A51B20C65800A9F7D1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXResourcesBuildPhase section */ 9D98823519BC69CA00B790C6 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EA5789A01B20C5B100A9F7D1 /* almost_nailed_it.gif in Resources */, 9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */, 9D25870819BCCB0F00A55A18 /* mugen.gif in Resources */, 9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */, @@ -202,6 +250,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + EA5789AB1B20C68B00A9F7D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Gifu; + targetProxy = EA5789AA1B20C68B00A9F7D1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 9D98825119BC69CA00B790C6 /* Debug */ = { isa = XCBuildConfiguration; diff --git a/Demo/demo/almost_nailed_it.gif b/Demo/demo/almost_nailed_it.gif new file mode 100644 index 0000000..7d4f54e Binary files /dev/null and b/Demo/demo/almost_nailed_it.gif differ diff --git a/Demo/demo/classes/ViewController.swift b/Demo/demo/classes/ViewController.swift index ce68108..83732ad 100755 --- a/Demo/demo/classes/ViewController.swift +++ b/Demo/demo/classes/ViewController.swift @@ -9,7 +9,7 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - imageView.animateWithImage(named: "mugen.gif") + imageView.animateWithImage(named: "almost_nailed_it.gif") UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: false) } diff --git a/Source/AnimatableImageView.swift b/Source/AnimatableImageView.swift index 6185ca6..4f4f0d9 100644 --- a/Source/AnimatableImageView.swift +++ b/Source/AnimatableImageView.swift @@ -7,6 +7,10 @@ public class AnimatableImageView: UIImageView, Animatable { /// An `Animator` instance that holds the frames of a specific image in memory. var animator: Animator? + deinit { + println("deinit animatable view") + } + /// A computed property that returns whether the image view is animating. public var isAnimatingGIF: Bool { return animator?.isAnimating ?? isAnimating() @@ -59,5 +63,9 @@ public class AnimatableImageView: UIImageView, Animatable { public func stopAnimatingGIF() { animator?.pauseAnimation() ?? stopAnimating() } + + public func cleanup() { + animator = .None + } } diff --git a/Source/Animator.swift b/Source/Animator.swift index 6cdc0dd..9d734b0 100644 --- a/Source/Animator.swift +++ b/Source/Animator.swift @@ -8,10 +8,14 @@ class Animator: NSObject { let delegate: Animatable /// Maximum duration to increment the frame timer with. private let maxTimeStep = 1.0 - /// The total duration of the GIF image. - private var totalDuration: NSTimeInterval = 0.0 /// An array of animated frames from a single GIF image. private var animatedFrames = [AnimatedFrame]() + /// Maximum number of frames to load at once + private let maxNumberOfFrames = 50 + /// The total number of frames in the GIF. + private var numberOfFrames = 0 + /// A reference to the original image source. + private var imageSource: CGImageSourceRef /// The index of the current GIF frame. private var currentFrameIndex = 0 /// Time elapsed since the last frame change. Used to determine when the frame should be updated. @@ -34,43 +38,46 @@ class Animator: NSObject { /// :param: data The raw GIF image data. /// :param: delegate An `Animatable` delegate. required init(data: NSData, delegate: Animatable) { - let imageSource = CGImageSourceCreateWithData(data, nil) + imageSource = CGImageSourceCreateWithData(data, nil) self.delegate = delegate super.init() attachDisplayLink() - curry(prepareFrames) <^> imageSource <*> delegate.frame.size + prepareFrames() pauseAnimation() } + deinit { + println("deinit animator") + } + // MARK: - Frames /// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`. + private func prepareFrames() { + numberOfFrames = Int(CGImageSourceGetCount(imageSource)) + let framesToProcess = numberOfFrames > maxNumberOfFrames ? maxNumberOfFrames : numberOfFrames + animatedFrames.reserveCapacity(framesToProcess) + animatedFrames = reduce(0.. AnimatedFrame { + let frameDuration = CGImageSourceGIFFrameDuration(imageSource, index) + let frameImageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) + let size = delegate.frame.size - (animatedFrames, totalDuration) = reduce(0.. UIImage? { - if index >= animatedFrames.count { return .None } - return animatedFrames[index].image + return animatedFrames[index % animatedFrames.count].image } /// Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`. /// /// :returns: An optional image at a given frame. func updateCurrentFrame() { - if totalDuration == 0 { return } + if animatedFrames.count <= 1 { return } timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration) - var frameDuration = animatedFrames[currentFrameIndex].duration + var frameDuration = animatedFrames[currentFrameIndex % animatedFrames.count].duration if timeSinceLastFrameChange >= frameDuration { timeSinceLastFrameChange -= frameDuration - currentFrameIndex = ++currentFrameIndex % animatedFrames.count + let lastFrameIndex = currentFrameIndex + currentFrameIndex = ++currentFrameIndex % numberOfFrames delegate.layer.setNeedsDisplay() + + // load the next needed frame for progressive loading + if animatedFrames.count < numberOfFrames { + let nextFrameToLoad = (lastFrameIndex + animatedFrames.count) % numberOfFrames + animatedFrames[lastFrameIndex % animatedFrames.count] = prepareFrame(nextFrameToLoad) + } } } @@ -106,7 +119,7 @@ class Animator: NSObject { /// Resumes the display link. func resumeAnimation() { - if totalDuration > 0 { + if animatedFrames.count > 1 { displayLink.paused = false } }