diff --git a/.travis.yml b/.travis.yml index 07dd4d5..d0ff764 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ -language: objective-c +language: swift script: ./bin/test -osx_image: xcode7.3 +osx_image: xcode8 diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 2dcbcb2..bbec3ac 100755 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 0009FCE71D16A4AB0038DC85 /* earth.gif in Resources */ = {isa = PBXBuildFile; fileRef = 0009FCE61D16A4AB0038DC85 /* earth.gif */; }; + 002A1BFC1D1624D0005ABBD0 /* mugen.gif in Resources */ = {isa = PBXBuildFile; fileRef = 002A1BFB1D1624D0005ABBD0 /* mugen.gif */; }; + 002FF5141D3A91340069CA56 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 002A1BF91D161D33005ABBD0 /* Gifu.framework */; }; 007380231B279644008DAD5C /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 007380221B279644008DAD5C /* Default-568h@2x.png */; }; 9D98823D19BC69CA00B790C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823C19BC69CA00B790C6 /* AppDelegate.swift */; }; 9D98823F19BC69CA00B790C6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823E19BC69CA00B790C6 /* ViewController.swift */; }; @@ -15,6 +18,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 0009FCE61D16A4AB0038DC85 /* earth.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = earth.gif; path = GIFs/earth.gif; sourceTree = SOURCE_ROOT; }; + 002A1BF91D161D33005ABBD0 /* Gifu.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Gifu.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Gifu-gwslszlwadetledwfipapwloqzrw/Build/Products/Debug-iphonesimulator/Gifu.framework"; sourceTree = ""; }; + 002A1BFB1D1624D0005ABBD0 /* mugen.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = mugen.gif; path = ../GIFs/mugen.gif; sourceTree = ""; }; 007380221B279644008DAD5C /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; 9D25870519BCCB0F00A55A18 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D98823719BC69CA00B790C6 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -29,17 +35,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 002FF5141D3A91340069CA56 /* Gifu.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 002A1BF81D161D32005ABBD0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 002A1BF91D161D33005ABBD0 /* Gifu.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9D98822E19BC69CA00B790C6 = { isa = PBXGroup; children = ( 9D98823919BC69CA00B790C6 /* Source */, 9D98823819BC69CA00B790C6 /* Products */, + 002A1BF81D161D32005ABBD0 /* Frameworks */, ); sourceTree = ""; }; @@ -67,6 +83,8 @@ 9D98823A19BC69CA00B790C6 /* Supporting Files */ = { isa = PBXGroup; children = ( + 0009FCE61D16A4AB0038DC85 /* earth.gif */, + 002A1BFB1D1624D0005ABBD0 /* mugen.gif */, 007380221B279644008DAD5C /* Default-568h@2x.png */, 9D25870519BCCB0F00A55A18 /* Info.plist */, ); @@ -100,11 +118,13 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0700; + LastUpgradeCheck = 0800; ORGANIZATIONNAME = "Kaishin & Co"; TargetAttributes = { 9D98823619BC69CA00B790C6 = { CreatedOnToolsVersion = 6.0; + LastSwiftMigration = 0800; + ProvisioningStyle = Manual; }; }; }; @@ -131,7 +151,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0009FCE71D16A4AB0038DC85 /* earth.gif in Resources */, 9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */, + 002A1BFC1D1624D0005ABBD0 /* mugen.gif in Resources */, 007380231B279644008DAD5C /* Default-568h@2x.png in Resources */, 9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */, ); @@ -175,6 +197,7 @@ ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -217,6 +240,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -236,10 +260,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = demo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = co.kaishin.gifu.demo; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -250,10 +275,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = demo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = co.kaishin.gifu.demo; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; diff --git a/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Demo/demo/Images.xcassets/earth.dataset/earth.gif b/Demo/GIFs/earth.gif similarity index 100% rename from Demo/demo/Images.xcassets/earth.dataset/earth.gif rename to Demo/GIFs/earth.gif diff --git a/Demo/demo/Images.xcassets/mugen.dataset/mugen.gif b/Demo/GIFs/mugen.gif similarity index 100% rename from Demo/demo/Images.xcassets/mugen.dataset/mugen.gif rename to Demo/GIFs/mugen.gif diff --git a/Demo/demo/Images.xcassets/nailed.dataset/nailed.gif b/Demo/GIFs/nailed.gif similarity index 100% rename from Demo/demo/Images.xcassets/nailed.dataset/nailed.gif rename to Demo/GIFs/nailed.gif diff --git a/Demo/demo/Images.xcassets/AppIcon.appiconset/Contents.json b/Demo/demo/Images.xcassets/AppIcon.appiconset/Contents.json index 400567d..a252a3e 100755 --- a/Demo/demo/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Demo/demo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "size" : "29x29", "idiom" : "iphone", diff --git a/Demo/demo/Images.xcassets/earth.dataset/Contents.json b/Demo/demo/Images.xcassets/earth.dataset/Contents.json deleted file mode 100644 index 42b0809..0000000 --- a/Demo/demo/Images.xcassets/earth.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - }, - "data" : [ - { - "idiom" : "universal", - "filename" : "earth.gif" - } - ] -} \ No newline at end of file diff --git a/Demo/demo/Images.xcassets/mugen.dataset/Contents.json b/Demo/demo/Images.xcassets/mugen.dataset/Contents.json deleted file mode 100644 index 10d518f..0000000 --- a/Demo/demo/Images.xcassets/mugen.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - }, - "data" : [ - { - "idiom" : "universal", - "filename" : "mugen.gif" - } - ] -} \ No newline at end of file diff --git a/Demo/demo/Images.xcassets/nailed.dataset/Contents.json b/Demo/demo/Images.xcassets/nailed.dataset/Contents.json deleted file mode 100644 index 41c1a00..0000000 --- a/Demo/demo/Images.xcassets/nailed.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - }, - "data" : [ - { - "idiom" : "universal", - "filename" : "nailed.gif" - } - ] -} diff --git a/Demo/demo/Main.storyboard b/Demo/demo/Main.storyboard index 624da18..ca738ab 100755 --- a/Demo/demo/Main.storyboard +++ b/Demo/demo/Main.storyboard @@ -1,8 +1,9 @@ - - + + - - + + + @@ -14,31 +15,31 @@ - + - + + + - - + + diff --git a/Demo/demo/classes/AppDelegate.swift b/Demo/demo/classes/AppDelegate.swift index 708917c..1c846fe 100755 --- a/Demo/demo/classes/AppDelegate.swift +++ b/Demo/demo/classes/AppDelegate.swift @@ -5,7 +5,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { return true } } diff --git a/Demo/demo/classes/ViewController.swift b/Demo/demo/classes/ViewController.swift index cfb5db7..5a0fb8d 100755 --- a/Demo/demo/classes/ViewController.swift +++ b/Demo/demo/classes/ViewController.swift @@ -6,11 +6,10 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - - imageView.animateWithImage(named: "mugen.gif") + imageView.animate(withGIFNamed: "mugen") } - @IBAction func toggleAnimation(sender: AnyObject) { + @IBAction func toggleAnimation(_ sender: AnyObject) { if imageView.isAnimatingGIF { imageView.stopAnimatingGIF() } else { diff --git a/Gifu.xcodeproj/project.pbxproj b/Gifu.xcodeproj/project.pbxproj index 6efe86a..998adda 100644 --- a/Gifu.xcodeproj/project.pbxproj +++ b/Gifu.xcodeproj/project.pbxproj @@ -32,8 +32,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0036ABB61BBD1D0B00C6CC3D /* mugen.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = mugen.gif; path = Demo/demo/Images.xcassets/mugen.dataset/mugen.gif; sourceTree = SOURCE_ROOT; }; - 0036ABB81BBD1D1400C6CC3D /* nailed.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = nailed.gif; path = Demo/demo/Images.xcassets/nailed.dataset/nailed.gif; sourceTree = SOURCE_ROOT; }; + 0036ABB61BBD1D0B00C6CC3D /* mugen.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = mugen.gif; path = Demo/GIFs/mugen.gif; sourceTree = SOURCE_ROOT; }; + 0036ABB81BBD1D1400C6CC3D /* nailed.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = nailed.gif; path = Demo/GIFs/nailed.gif; sourceTree = SOURCE_ROOT; }; 005656EC1A6F14D6008A0ED1 /* Animator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = ""; }; 005656EE1A6F1C26008A0ED1 /* AnimatableImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatableImageView.swift; sourceTree = ""; }; 007E08431BD95E6200883D0C /* ArrayExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = ""; }; @@ -212,14 +212,17 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0700; + LastUpgradeCheck = 0800; ORGANIZATIONNAME = "Kaishin & Co"; TargetAttributes = { 009BD1351BBC7F6500FC982B = { CreatedOnToolsVersion = 7.0.1; + LastSwiftMigration = 0800; }; 00B8C73D1A364DA400C188E7 = { CreatedOnToolsVersion = 6.1.1; + LastSwiftMigration = 0800; + ProvisioningStyle = Manual; }; }; }; @@ -321,6 +324,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = co.kaishin.GifuTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -335,6 +339,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = co.kaishin.GifuTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; }; name = Release; }; @@ -362,6 +368,7 @@ ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -408,6 +415,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -436,11 +444,13 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = co.kaishin.gifu; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -456,10 +466,13 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = co.kaishin.gifu; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/Gifu.xcodeproj/xcshareddata/xcschemes/Gifu.xcscheme b/Gifu.xcodeproj/xcshareddata/xcschemes/Gifu.xcscheme index f45d4c6..fbf1b13 100644 --- a/Gifu.xcodeproj/xcshareddata/xcschemes/Gifu.xcscheme +++ b/Gifu.xcodeproj/xcshareddata/xcschemes/Gifu.xcscheme @@ -1,6 +1,6 @@ NSData { - let testBundle = NSBundle(forClass: GifuTests.self) - let imagePath = testBundle.bundleURL.URLByAppendingPathComponent(name) - return NSData(contentsOfURL: imagePath)! +private func testImageDataNamed(_ name: String) -> Data { + let testBundle = Bundle(for: GifuTests.self) + let imagePath = testBundle.bundleURL.appendingPathComponent(name) + return (try! Data(contentsOf: imagePath)) } diff --git a/Source/AnimatableImageView.swift b/Source/AnimatableImageView.swift index 83d966a..0168f77 100644 --- a/Source/AnimatableImageView.swift +++ b/Source/AnimatableImageView.swift @@ -4,7 +4,7 @@ import UIKit public class AnimatableImageView: UIImageView { /// Proxy object for preventing a reference cycle between the CADisplayLink and AnimatableImageView. /// Source: http://merowing.info/2015/11/the-beauty-of-imperfection/ - class TargetProxy { + fileprivate class TargetProxy { private weak var target: AnimatableImageView? init(target: AnimatableImageView) { @@ -23,10 +23,10 @@ public class AnimatableImageView: UIImageView { private var displayLinkInitialized: Bool = false /// A display link that keeps calling the `updateFrame` method on every screen refresh. - lazy var displayLink: CADisplayLink = { + lazy var displayLink: CADisplayLink = { [unowned self] in self.displayLinkInitialized = true let display = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate)) - display.paused = true + display.isPaused = true return display }() @@ -38,7 +38,7 @@ public class AnimatableImageView: UIImageView { /// A computed property that returns whether the image view is animating. public var isAnimatingGIF: Bool { - return !displayLink.paused + return !displayLink.isPaused } /// A computed property that returns the total number of frames in the GIF. @@ -50,18 +50,20 @@ public class AnimatableImageView: UIImageView { /// The file name should include the `.gif` extension. /// /// - parameter imageName: The name of the GIF file. The method looks for the file in the app bundle. - public func prepareForAnimation(imageNamed imageName: String) { - let imagePath = NSBundle.mainBundle().bundleURL.URLByAppendingPathComponent(imageName) - guard let data = NSData(contentsOfURL: imagePath) else { return } - prepareForAnimation(imageData: data) + public func prepareForAnimation(withGIFNamed imageName: String) { + guard let extensionRemoved = imageName.components(separatedBy: ".")[safe: 0], + let imagePath = Bundle.main.url(forResource: extensionRemoved, withExtension: "gif"), + let data = try? Data(contentsOf: imagePath) else { return } + + prepareForAnimation(withGIFData: data) } /// Prepares the frames using raw GIF image data, without starting the animation. /// /// - parameter data: GIF image data. - public func prepareForAnimation(imageData data: NSData) { - image = UIImage(data: data) - animator = Animator(data: data, size: frame.size, contentMode: contentMode, framePreloadCount: framePreloadCount) + public func prepareForAnimation(withGIFData imageData: Data) { + image = UIImage(data: imageData) + animator = Animator(data: imageData, size: frame.size, contentMode: contentMode, framePreloadCount: framePreloadCount) animator?.needsPrescaling = needsPrescaling animator?.prepareFrames() attachDisplayLink() @@ -70,34 +72,34 @@ public class AnimatableImageView: UIImageView { /// Prepares the frames using a GIF image file name and starts animating the image view. /// /// - parameter imageName: The name of the GIF file. The method looks for the file in the app bundle. - public func animateWithImage(named imageName: String) { - prepareForAnimation(imageNamed: imageName) + public func animate(withGIFNamed imageName: String) { + prepareForAnimation(withGIFNamed: imageName) startAnimatingGIF() } /// Prepares the frames using raw GIF image data and starts animating the image view. /// /// - parameter data: GIF image data. - public func animateWithImageData(data: NSData) { - prepareForAnimation(imageData: data) + public func animate(withGIFData data: Data) { + prepareForAnimation(withGIFData: data) startAnimatingGIF() } /// Updates the `image` property of the image view if necessary. This method should not be called manually. - override public func displayLayer(layer: CALayer) { + override public func display(_ layer: CALayer) { image = animator?.currentFrameImage ?? image } /// Starts the image view animation. public func startAnimatingGIF() { if animator?.isAnimatable ?? false { - displayLink.paused = false + displayLink.isPaused = false } } /// Stops the image view animation. public func stopAnimatingGIF() { - displayLink.paused = true + displayLink.isPaused = true } /// Reset the image view values. @@ -109,7 +111,7 @@ public class AnimatableImageView: UIImageView { /// Update the current frame if needed. func updateFrameIfNeeded() { guard let animator = animator else { return } - animator.shouldChangeFrame(displayLink.duration) { hasNewFrame in + animator.shouldChangeFrame(with: displayLink.duration) { hasNewFrame in if hasNewFrame { self.layer.setNeedsDisplay() } } } @@ -123,6 +125,6 @@ public class AnimatableImageView: UIImageView { /// Attaches the display link. func attachDisplayLink() { - displayLink.addToRunLoop(.mainRunLoop(), forMode: NSRunLoopCommonModes) + displayLink.add(to: .main, forMode: RunLoopMode.commonModes) } } diff --git a/Source/AnimatedFrame.swift b/Source/AnimatedFrame.swift index 2462b55..c89f6d1 100644 --- a/Source/AnimatedFrame.swift +++ b/Source/AnimatedFrame.swift @@ -3,7 +3,7 @@ struct AnimatedFrame { /// The image that should be used for this frame. let image: UIImage? /// The duration that the frame image should be displayed. - let duration: NSTimeInterval + let duration: TimeInterval /// A placeholder frame with no image assigned. /// Used to replace frames that are no longer needed in the animation. @@ -13,15 +13,15 @@ struct AnimatedFrame { /// Whether the AnimatedFrame instance contains an image or not. var isPlaceholder: Bool { - return image == .None + return image == .none } /// Takes an optional image and returns an non-placeholder `AnimatedFrame`. /// /// - parameter image: An optional `UIImage` instance to be assigned to the new frame. /// - returns: A non-placeholder `AnimatedFrame` instance. - func frameWithImage(image: UIImage?) -> AnimatedFrame { - return AnimatedFrame(image: image, duration: duration) + func animatedFrame(with newImage: UIImage?) -> AnimatedFrame { + return AnimatedFrame(image: newImage, duration: duration) } } diff --git a/Source/Animator.swift b/Source/Animator.swift index 014a79f..896ca1a 100644 --- a/Source/Animator.swift +++ b/Source/Animator.swift @@ -16,7 +16,7 @@ class Animator { /// The total number of frames in the GIF. var frameCount = 0 /// A reference to the original image source. - var imageSource: CGImageSourceRef + var imageSource: CGImageSource /// The index of the current GIF frame. var currentFrameIndex = 0 { @@ -28,27 +28,28 @@ class Animator { /// The index of the previous GIF frame. var previousFrameIndex = 0 { didSet { - dispatch_async(preloadFrameQueue) { + preloadFrameQueue.async { self.updatePreloadedFrames() } } } /// Time elapsed since the last frame change. Used to determine when the frame should be updated. - var timeSinceLastFrameChange: NSTimeInterval = 0.0 + var timeSinceLastFrameChange: TimeInterval = 0.0 /// Specifies whether GIF frames should be pre-scaled. /// - seealso: `needsPrescaling` in AnimatableImageView. var needsPrescaling = true /// Dispatch queue used for preloading images. - private lazy var preloadFrameQueue = dispatch_queue_create("co.kaishin.Gifu.preloadQueue", DISPATCH_QUEUE_SERIAL) - + private lazy var preloadFrameQueue: DispatchQueue = { + return DispatchQueue(label: "co.kaishin.Gifu.preloadQueue") + }() /// The current image frame to show. var currentFrameImage: UIImage? { - return frameAtIndex(currentFrameIndex) + return frame(at: currentFrameIndex) } /// The current frame duration - var currentFrameDuration: NSTimeInterval { - return durationAtIndex(currentFrameIndex) + var currentFrameDuration: TimeInterval { + return duration(at: currentFrameIndex) } /// Is this image animatable? @@ -60,9 +61,9 @@ class Animator { /// /// - parameter data: The raw GIF image data. /// - parameter delegate: An `Animatable` delegate. - init(data: NSData, size: CGSize, contentMode: UIViewContentMode, framePreloadCount: Int) { - let options = [String(kCGImageSourceShouldCache): kCFBooleanFalse] - self.imageSource = CGImageSourceCreateWithData(data, options) ?? CGImageSourceCreateIncremental(options) + init(data: Data, size: CGSize, contentMode: UIViewContentMode, framePreloadCount: Int) { + let options = [String(kCGImageSourceShouldCache): kCFBooleanFalse] as CFDictionary + self.imageSource = CGImageSourceCreateWithData(data as CFData, options) ?? CGImageSourceCreateIncremental(options) self.size = size self.contentMode = contentMode self.preloadFrameCount = framePreloadCount @@ -70,10 +71,10 @@ class Animator { // MARK: - Frames /// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`. - func prepareFrames(completionHandler: (Void -> Void)? = .None) { + func prepareFrames(_ completionHandler: ((Void) -> Void)? = .none) { frameCount = Int(CGImageSourceGetCount(imageSource)) animatedFrames.reserveCapacity(frameCount) - dispatch_async(preloadFrameQueue) { + preloadFrameQueue.async { self.setupAnimatedFrames() if let handler = completionHandler { handler() } } @@ -83,7 +84,7 @@ class Animator { /// /// - parameter index: The index of the frame. /// - returns: An optional image at a given frame. - func frameAtIndex(index: Int) -> UIImage? { + func frame(at index: Int) -> UIImage? { return animatedFrames[safe: index]?.image } @@ -91,16 +92,16 @@ class Animator { /// /// - parameter index: The index of the duration. /// - returns: The duration of the given frame. - func durationAtIndex(index: Int) -> NSTimeInterval { - return animatedFrames[safe: index]?.duration ?? NSTimeInterval.infinity + func duration(at index: Int) -> TimeInterval { + return animatedFrames[safe: index]?.duration ?? TimeInterval.infinity } /// Checks whether the frame should be changed and calls a handler with the results. /// /// - parameter duration: A `CFTimeInterval` value that will be used to determine whether frame should be changed. /// - parameter handler: A function that takes a `Bool` and returns nothing. It will be called with the frame change result. - func shouldChangeFrame(duration: CFTimeInterval, handler: Bool -> Void) { - incrementTimeSinceLastFrameChangeWithDuration(duration) + func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) { + incrementTimeSinceLastFrameChange(with: duration) if currentFrameDuration > timeSinceLastFrameChange { handler(false) @@ -122,16 +123,16 @@ private extension Animator { /// /// - parameter index: The index of the frame to load. /// - returns: An optional `UIImage` instance. - func loadFrameAtIndex(index: Int) -> UIImage? { - guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else { return .None } - let image = UIImage(CGImage: imageRef) + func loadFrame(at index: Int) -> UIImage? { + guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else { return .none } + let image = UIImage(cgImage: imageRef) let scaledImage: UIImage? if needsPrescaling { switch self.contentMode { - case .ScaleAspectFit: scaledImage = image.resizeAspectFit(size) - case .ScaleAspectFill: scaledImage = image.resizeAspectFill(size) - default: scaledImage = image.resize(size) + case .scaleAspectFit: scaledImage = image.constrained(by: size) + case .scaleAspectFill: scaledImage = image.filling(size: size) + default: scaledImage = image.resized(to: size) } } else { scaledImage = image @@ -145,17 +146,17 @@ private extension Animator { if !preloadingIsNeeded { return } animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex].placeholderFrame - preloadIndexesWithStartingIndex(currentFrameIndex).forEach { index in + preloadIndexes(withStartingIndex: currentFrameIndex).forEach { index in let currentAnimatedFrame = animatedFrames[index] if !currentAnimatedFrame.isPlaceholder { return } - animatedFrames[index] = currentAnimatedFrame.frameWithImage(loadFrameAtIndex(index)) + animatedFrames[index] = currentAnimatedFrame.animatedFrame(with: loadFrame(at: index)) } } /// Increments the `timeSinceLastFrameChange` property with a given duration. /// /// - parameter duration: An `NSTimeInterval` value to increment the `timeSinceLastFrameChange` property with. - func incrementTimeSinceLastFrameChangeWithDuration(duration: NSTimeInterval) { + func incrementTimeSinceLastFrameChange(with duration: TimeInterval) { timeSinceLastFrameChange += min(maxTimeStep, duration) } @@ -166,7 +167,7 @@ private extension Animator { /// Increments the `currentFrameIndex` property. func incrementCurrentFrameIndex() { - currentFrameIndex = incrementFrameIndex(currentFrameIndex) + currentFrameIndex = increment(index: currentFrameIndex) } /// Increments a given frame index, taking into account the `frameCount` and looping when necessary. @@ -174,7 +175,7 @@ private extension Animator { /// - parameter index: The `Int` value to increment. /// - parameter byValue: The `Int` value to increment with. /// - returns: A new `Int` value. - func incrementFrameIndex(index: Int, byValue value: Int = 1) -> Int { + func increment(index: Int, by value: Int = 1) -> Int { return (index + value) % frameCount } @@ -182,9 +183,9 @@ private extension Animator { /// /// - parameter index: Starting index. /// - returns: An array of indexes to preload. - func preloadIndexesWithStartingIndex(index: Int) -> [Int] { - let nextIndex = incrementFrameIndex(index) - let lastIndex = incrementFrameIndex(index, byValue: preloadFrameCount) + func preloadIndexes(withStartingIndex index: Int) -> [Int] { + let nextIndex = increment(index: index) + let lastIndex = increment(index: index, by: preloadFrameCount) if lastIndex >= nextIndex { return [Int](nextIndex...lastIndex) @@ -198,11 +199,11 @@ private extension Animator { resetAnimatedFrames() (0.. preloadFrameCount { return } - animatedFrames[index] = animatedFrames[index].frameWithImage(loadFrameAtIndex(index)) + animatedFrames[index] = animatedFrames[index].animatedFrame(with: loadFrame(at: index)) } } diff --git a/Source/ArrayExtension.swift b/Source/ArrayExtension.swift index dbbc371..079a3f6 100644 --- a/Source/ArrayExtension.swift +++ b/Source/ArrayExtension.swift @@ -1,5 +1,5 @@ extension Array { subscript(safe index: Int) -> Element? { - return indices ~= index ? self[index] : .None + return indices ~= index ? self[index] : .none } } diff --git a/Source/CGSizeExtension.swift b/Source/CGSizeExtension.swift index 8f66266..9a314e6 100644 --- a/Source/CGSizeExtension.swift +++ b/Source/CGSizeExtension.swift @@ -11,7 +11,7 @@ extension CGSize { /// /// - parameter size: The contraining size. /// - returns: size A new size that fits inside the contraining size with the same aspect ratio. - func sizeConstrainedBySize(size: CGSize) -> CGSize { + func constrained(by size: CGSize) -> CGSize { let aspectWidth = round(aspectRatio * size.height) let aspectHeight = round(size.width / aspectRatio) @@ -26,7 +26,7 @@ extension CGSize { /// /// - parameter size: The contraining size. /// - returns: size A new size that fills the contraining size keeping the same aspect ratio. - func sizeFillingSize(size: CGSize) -> CGSize { + func filling(size: CGSize) -> CGSize { let aspectWidth = round(aspectRatio * size.height) let aspectHeight = round(size.width / aspectRatio) diff --git a/Source/ImageSourceHelpers.swift b/Source/ImageSourceHelpers.swift index bf7fb65..23f6473 100755 --- a/Source/ImageSourceHelpers.swift +++ b/Source/ImageSourceHelpers.swift @@ -2,18 +2,18 @@ import ImageIO import MobileCoreServices import UIKit -typealias GIFProperties = [String : Double] +typealias GIFProperties = [String: Double] let defaultDuration: Double = 0 /// Retruns the duration of a frame at a specific index using an image source (an `CGImageSource` instance). /// /// - returns: A frame duration. -func CGImageSourceGIFFrameDuration(imageSource: CGImageSource, index: Int) -> NSTimeInterval { +func CGImageFrameDuration(with imageSource: CGImageSource, atIndex index: Int) -> TimeInterval { if !imageSource.isAnimatedGIF { return 0.0 } - guard let properties = imageSource.GIFPropertiesAtIndex(index), - let duration = durationFromGIFProperties(properties), - let cappedDuration = capDuration(duration) + guard let GIFProperties = imageSource.properties(at: index), + let duration = frameDuration(with: GIFProperties), + let cappedDuration = capDuration(with: duration) else { return defaultDuration } return cappedDuration @@ -22,8 +22,8 @@ func CGImageSourceGIFFrameDuration(imageSource: CGImageSource, index: Int) -> NS /// Ensures that a duration is never smaller than a threshold value. /// /// - returns: A capped frame duration. -func capDuration(duration: Double) -> Double? { - if duration < 0 { return .None } +func capDuration(with duration: Double) -> Double? { + if duration < 0 { return .none } let threshold = 0.02 - Double(FLT_EPSILON) let cappedDuration = duration < threshold ? 0.1 : duration return cappedDuration @@ -32,36 +32,29 @@ func capDuration(duration: Double) -> Double? { /// Returns a frame duration from a `GIFProperties` dictionary. /// /// - returns: A frame duration. -func durationFromGIFProperties(properties: GIFProperties) -> Double? { +func frameDuration(with properties: GIFProperties) -> Double? { guard let unclampedDelayTime = properties[String(kCGImagePropertyGIFUnclampedDelayTime)], let delayTime = properties[String(kCGImagePropertyGIFDelayTime)] - else { return .None } + else { return .none } - return duration(unclampedDelayTime, delayTime: delayTime) + return duration(withUnclampedTime: unclampedDelayTime, andClampedTime: delayTime) } /// Calculates frame duration based on both clamped and unclamped times. /// /// - returns: A frame duration. -func duration(unclampedDelayTime: Double, delayTime: Double) -> Double { +func duration(withUnclampedTime unclampedDelayTime: Double, andClampedTime delayTime: Double) -> Double { let delayArray = [unclampedDelayTime, delayTime] - return delayArray.filter(isPositive).first ?? defaultDuration -} - -/// Checks if a `Double` value is positive. -/// -/// - returns: A boolean value that is `true` if the tested value is positive. -func isPositive(value: Double) -> Bool { - return value >= 0 + return delayArray.filter({ $0 >= 0 }).first ?? defaultDuration } /// An extension of `CGImageSourceRef` that add GIF introspection and easier property retrieval. -extension CGImageSourceRef { +extension CGImageSource { /// Returns whether the image source contains an animated GIF. /// /// - returns: A boolean value that is `true` if the image source contains animated GIF data. var isAnimatedGIF: Bool { - let isTypeGIF = UTTypeConformsTo(CGImageSourceGetType(self) ?? "", kUTTypeGIF) + let isTypeGIF = UTTypeConformsTo(CGImageSourceGetType(self) ?? "" as CFString, kUTTypeGIF) let imageCount = CGImageSourceGetCount(self) return isTypeGIF != false && imageCount > 1 } @@ -70,8 +63,8 @@ extension CGImageSourceRef { /// /// - parameter index: The index of the GIF properties to retrieve. /// - returns: A dictionary containing the GIF properties at the passed in index. - func GIFPropertiesAtIndex(index: Int) -> GIFProperties? { - let imageProperties = CGImageSourceCopyPropertiesAtIndex(self, index, nil) as Dictionary? - return imageProperties?[String(kCGImagePropertyGIFDictionary)] as? GIFProperties + func properties(at index: Int) -> GIFProperties? { + guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(self, index, nil) as? [String: AnyObject] else { return nil } + return imageProperties[String(kCGImagePropertyGIFDictionary)] as? GIFProperties } } diff --git a/Source/Info.plist b/Source/Info.plist index b4c2361..207e821 100644 --- a/Source/Info.plist +++ b/Source/Info.plist @@ -19,7 +19,7 @@ CFBundleSignature ???? CFBundleVersion - 94 + 100 NSPrincipalClass diff --git a/Source/UIImageExtension.swift b/Source/UIImageExtension.swift index bb501fa..d7beb6c 100644 --- a/Source/UIImageExtension.swift +++ b/Source/UIImageExtension.swift @@ -5,9 +5,9 @@ extension UIImage { /// /// - parameter size: The new size of the image. /// - returns: A new resized image instance. - func resize(size: CGSize) -> UIImage { + func resized(to size: CGSize) -> UIImage { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - self.drawInRect(CGRect(origin: CGPointZero, size: size)) + self.draw(in: CGRect(origin: CGPoint.zero, size: size)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage ?? self @@ -17,18 +17,18 @@ extension UIImage { /// /// - parameter size: The constraining size of the image. /// - returns: A new resized image instance. - func resizeAspectFit(size: CGSize) -> UIImage { - let newSize = self.size.sizeConstrainedBySize(size) - return resize(newSize) + func constrained(by size: CGSize) -> UIImage { + let newSize = size.constrained(by: size) + return resized(to: newSize) } /// Resizes an image instance to fill a constraining size while keeping the aspect ratio. /// /// - parameter size: The constraining size of the image. /// - returns: A new resized image instance. - func resizeAspectFill(size: CGSize) -> UIImage { - let newSize = self.size.sizeFillingSize(size) - return resize(newSize) + func filling(size: CGSize) -> UIImage { + let newSize = size.filling(size: size) + return resized(to: newSize) } /// Returns a new `UIImage` instance using raw image data and a size. @@ -36,15 +36,15 @@ extension UIImage { /// - parameter data: Raw image data. /// - parameter size: The size to be used to resize the new image instance. /// - returns: A new image instance from the passed in data. - class func imageWithData(data: NSData, size: CGSize) -> UIImage? { - return UIImage(data: data)?.resize(size) + class func image(with data: Data, size: CGSize) -> UIImage? { + return UIImage(data: data)?.resized(to: size) } /// Returns an image size from raw image data. /// /// - parameter data: Raw image data. /// - returns: The size of the image contained in the data. - class func sizeForImageData(data: NSData) -> CGSize? { + class func size(withImageData data: Data) -> CGSize? { return UIImage(data: data)?.size } } diff --git a/bin/test b/bin/test index cd952d0..9f3dd07 100755 --- a/bin/test +++ b/bin/test @@ -2,4 +2,4 @@ set -o pipefail -xcodebuild test -project Gifu.xcodeproj -scheme Gifu -sdk iphonesimulator BUILD_ACTIVE_ARCH=NO | xcpretty -t -c +xcodebuild test -project Gifu.xcodeproj -scheme Gifu -destination "platform=iOS Simulator,name=iPhone 6,OS=10.0" BUILD_ACTIVE_ARCH=NO | xcpretty -t -c