Use image view subclass

This commit is contained in:
Reda Lemeden 2015-01-22 11:54:27 +01:00
parent d6a7b29aaf
commit b15e7e3399
7 changed files with 101 additions and 138 deletions

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6206.9" systemVersion="13E28" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="vXZ-lx-hvc">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6254" systemVersion="14C99d" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="vXZ-lx-hvc">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7026.1"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6247"/>
</dependencies>
<scenes>
<!--Gifu-->
@ -17,13 +16,27 @@
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FSz-xF-Xds">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Gifu" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c8Y-41-BaC">
<rect key="frame" x="283" y="45" width="42" height="20.5"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="42" id="1Mg-oH-beY"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Not the Japanese prefecture." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wsv-cU-WO5">
<rect key="frame" x="217" y="69" width="175" height="13.5"/>
<constraints>
<constraint firstAttribute="width" constant="175" id="qtB-pf-agE"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="11"/>
<color key="textColor" red="1" green="1" blue="1" alpha="0.5" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="450" placeholderIntrinsicHeight="300" translatesAutoresizingMaskIntoConstraints="NO" id="FSz-xF-Xds" customClass="AnimatableImageView" customModule="Gifu">
<rect key="frame" x="75" y="150" width="450" height="300"/>
<color key="backgroundColor" red="0.7725490196" green="0.1764705882" blue="0.1843137255" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="300" id="oev-E3-JrW"/>
<constraint firstAttribute="width" secondItem="FSz-xF-Xds" secondAttribute="height" multiplier="1.5:1" id="vUc-PW-K4t"/>
</constraints>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bFY-3J-OXr" customClass="FlatButton" customModule="gifu_demo" customModuleProvider="target">
<rect key="frame" x="275" y="509" width="50" height="50"/>
@ -44,26 +57,8 @@
<action selector="toggleAnimation:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="PvH-3E-LbB"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Gifu" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c8Y-41-BaC">
<rect key="frame" x="283" y="45" width="42" height="20.5"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="42" id="1Mg-oH-beY"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Not the Japanese prefecture." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wsv-cU-WO5">
<rect key="frame" x="217" y="69" width="175" height="13.5"/>
<constraints>
<constraint firstAttribute="width" constant="175" id="qtB-pf-agE"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="11"/>
<color key="textColor" red="1" green="1" blue="1" alpha="0.5" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.2304944545" green="0.24875254929999999" blue="0.2778563201" alpha="1" colorSpace="calibratedRGB"/>
<color key="backgroundColor" red="0.24722154438495636" green="0.26659342646598816" blue="0.2988148033618927" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="centerY" secondItem="bFY-3J-OXr" secondAttribute="centerY" id="09i-ag-RTM"/>
<constraint firstItem="bFY-3J-OXr" firstAttribute="top" secondItem="FSz-xF-Xds" secondAttribute="bottom" constant="-73" id="1KN-hB-V2y"/>

View File

@ -3,17 +3,14 @@ import Gifu
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var imageView: AnimatableImageView!
@IBOutlet weak var button: FlatButton!
override func viewDidLoad() {
super.viewDidLoad()
if let image = AnimatedImage.animatedImageWithName("mugen.gif") {
imageView.setAnimatedImage(image)
imageView.startAnimatingGIF()
}
imageView.animateWithImage(named: "mugen.gif")
UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: false)
}

View File

@ -7,10 +7,11 @@
objects = {
/* Begin PBXBuildFile section */
005656ED1A6F14D6008A0ED1 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005656EC1A6F14D6008A0ED1 /* Animator.swift */; };
005656EF1A6F1C26008A0ED1 /* AnimatableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005656EE1A6F1C26008A0ED1 /* AnimatableImageView.swift */; };
005656F11A7042E9008A0ED1 /* Animatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005656F01A7042E9008A0ED1 /* Animatable.swift */; };
006F97011A6EDE7900CB5CE8 /* Runes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 006F97001A6EDE7900CB5CE8 /* Runes.framework */; };
00B8C75E1A364DCE00C188E7 /* AnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B8C75B1A364DCE00C188E7 /* AnimatedImage.swift */; };
00B8C75F1A364DCE00C188E7 /* ImageSourceHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B8C75C1A364DCE00C188E7 /* ImageSourceHelpers.swift */; };
00B8C7601A364DCE00C188E7 /* UIImageView+Gifu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B8C75D1A364DCE00C188E7 /* UIImageView+Gifu.swift */; };
00B8C7961A3650EE00C188E7 /* Gifu.h in Headers */ = {isa = PBXBuildFile; fileRef = 00B8C7951A3650EE00C188E7 /* Gifu.h */; settings = {ATTRIBUTES = (Public, ); }; };
EAF49C7F1A3A4DE000B395DF /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF49C7E1A3A4DE000B395DF /* UIImageExtension.swift */; };
EAF49C811A3A4FAA00B395DF /* Curry.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF49C801A3A4FAA00B395DF /* Curry.swift */; };
@ -18,12 +19,13 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
005656EC1A6F14D6008A0ED1 /* Animator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = "<group>"; };
005656EE1A6F1C26008A0ED1 /* AnimatableImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatableImageView.swift; sourceTree = "<group>"; };
005656F01A7042E9008A0ED1 /* Animatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animatable.swift; sourceTree = "<group>"; };
006F97001A6EDE7900CB5CE8 /* Runes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Runes.framework; path = "../Carthage/Checkouts/runes/build/Debug-iphoneos/Runes.framework"; sourceTree = "<group>"; };
00B8C73E1A364DA400C188E7 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; };
00B8C7421A364DA400C188E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Source/Info.plist; sourceTree = "<group>"; };
00B8C75B1A364DCE00C188E7 /* AnimatedImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImage.swift; sourceTree = "<group>"; };
00B8C75C1A364DCE00C188E7 /* ImageSourceHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSourceHelpers.swift; sourceTree = "<group>"; };
00B8C75D1A364DCE00C188E7 /* UIImageView+Gifu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+Gifu.swift"; sourceTree = "<group>"; };
00B8C7951A3650EE00C188E7 /* Gifu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Gifu.h; sourceTree = "<group>"; };
EAF49C7E1A3A4DE000B395DF /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = "<group>"; };
EAF49C801A3A4FAA00B395DF /* Curry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Curry.swift; sourceTree = "<group>"; };
@ -72,13 +74,14 @@
00B8C75A1A364DBE00C188E7 /* Source */ = {
isa = PBXGroup;
children = (
005656EE1A6F1C26008A0ED1 /* AnimatableImageView.swift */,
EAF49CB01A3B6EEB00B395DF /* AnimatedFrame.swift */,
005656EC1A6F14D6008A0ED1 /* Animator.swift */,
005656F01A7042E9008A0ED1 /* Animatable.swift */,
00B8C7951A3650EE00C188E7 /* Gifu.h */,
00B8C75B1A364DCE00C188E7 /* AnimatedImage.swift */,
00B8C75C1A364DCE00C188E7 /* ImageSourceHelpers.swift */,
00B8C75D1A364DCE00C188E7 /* UIImageView+Gifu.swift */,
EAF49C7E1A3A4DE000B395DF /* UIImageExtension.swift */,
EAF49C801A3A4FAA00B395DF /* Curry.swift */,
EAF49CB01A3B6EEB00B395DF /* AnimatedFrame.swift */,
);
path = Source;
sourceTree = "<group>";
@ -161,11 +164,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
005656EF1A6F1C26008A0ED1 /* AnimatableImageView.swift in Sources */,
005656ED1A6F14D6008A0ED1 /* Animator.swift in Sources */,
EAF49CB11A3B6EEB00B395DF /* AnimatedFrame.swift in Sources */,
00B8C7601A364DCE00C188E7 /* UIImageView+Gifu.swift in Sources */,
005656F11A7042E9008A0ED1 /* Animatable.swift in Sources */,
00B8C75F1A364DCE00C188E7 /* ImageSourceHelpers.swift in Sources */,
EAF49C811A3A4FAA00B395DF /* Curry.swift in Sources */,
00B8C75E1A364DCE00C188E7 /* AnimatedImage.swift in Sources */,
EAF49C7F1A3A4DE000B395DF /* UIImageExtension.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

4
Source/Animatable.swift Normal file
View File

@ -0,0 +1,4 @@
protocol Animatable {
var layer: CALayer { get }
var frame: CGRect { get }
}

View File

@ -0,0 +1,44 @@
import UIKit
import ImageIO
import Runes
public class AnimatableImageView: UIImageView, Animatable {
var animator: Animator?
public var isAnimatingGIF: Bool {
return animator?.isAnimating ?? isAnimating()
}
public func prepareForAnimation(imageNamed imageName: String) {
let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(imageName)
prepareForAnimation <^> NSData(contentsOfFile: path)
}
public func prepareForAnimation(imageData data: NSData) {
image = UIImage(data: data)
animator = Animator(data: data, delegate: self)
}
public func animateWithImage(named imageName: String) {
prepareForAnimation(imageNamed: imageName)
startAnimatingGIF()
}
public func animateWithImageData(#data: NSData) {
prepareForAnimation(imageData: data)
startAnimatingGIF()
}
override public func displayLayer(layer: CALayer!) {
image = animator?.currentFrame?
}
public func startAnimatingGIF() {
animator?.resumeAnimation() ?? startAnimating()
}
public func stopAnimatingGIF() {
animator?.pauseAnimation() ?? stopAnimating()
}
}

74
Source/AnimatedImage.swift → Source/Animator.swift Executable file → Normal file
View File

@ -2,77 +2,33 @@ import UIKit
import ImageIO
import Runes
public class AnimatedImage: UIImage {
// MARK: - Constants
class Animator: NSObject {
let maxTimeStep = 1.0
// MARK: - Public Properties
var delegate: UIImageView?
var animatedFrames = [AnimatedFrame]()
var totalDuration: NSTimeInterval = 0.0
override public var size: CGSize {
return frameAtIndex(0)?.size ?? CGSizeZero
}
// MARK: - Private Properties
private lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: "updateCurrentFrame")
let delegate: Animatable
private var currentFrameIndex = 0
private var timeSinceLastFrameChange: NSTimeInterval = 0.0
private lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: "updateCurrentFrame")
// MARK: - Computed Properties
var currentFrame: UIImage? {
return frameAtIndex(currentFrameIndex)
}
private var isAnimated: Bool {
return totalDuration != 0.0
var isAnimating: Bool {
return !displayLink.paused
}
// MARK: - Initializers
required public init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override convenience init(data: NSData) {
self.init(data: data, size: CGSizeZero)
}
required public init(data: NSData, size: CGSize) {
super.init()
required init(data: NSData, delegate: Animatable) {
let imageSource = CGImageSourceCreateWithData(data, nil)
self.delegate = delegate
super.init()
attachDisplayLink()
curry(prepareFrames) <^> imageSource <*> size
curry(prepareFrames) <^> imageSource <*> delegate.frame.size
pauseAnimation()
}
// MARK: - Factories
public class func animatedImageWithName(name: String) -> AnimatedImage? {
let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(name)
return animatedImageWithData <^> NSData(contentsOfFile: path)
}
public class func animatedImageWithData(data: NSData) -> AnimatedImage {
let size = UIImage.sizeForImageData(data) ?? CGSizeZero
return self(data: data, size: size)
}
public class func animatedImageWithName(name: String, size: CGSize) -> AnimatedImage? {
let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(name)
return curry(animatedImageWithData) <^> NSData(contentsOfFile: path) <*> size
}
public class func animatedImageWithData(data: NSData, size: CGSize) -> AnimatedImage {
return self(data: data, size: size)
}
// MARK: - Display Link Helpers
func attachDisplayLink() {
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
// MARK: - Frame Methods
// MARK: - Frames
private func prepareFrames(imageSource: CGImageSourceRef, size: CGSize) {
let numberOfFrames = Int(CGImageSourceGetCount(imageSource))
animatedFrames.reserveCapacity(numberOfFrames)
@ -96,7 +52,7 @@ public class AnimatedImage: UIImage {
}
func updateCurrentFrame() {
if !isAnimated { return }
if totalDuration == 0 { return }
timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration)
var frameDuration = animatedFrames[currentFrameIndex].duration
@ -104,7 +60,7 @@ public class AnimatedImage: UIImage {
if timeSinceLastFrameChange >= frameDuration {
timeSinceLastFrameChange -= frameDuration
currentFrameIndex = ++currentFrameIndex % animatedFrames.count
delegate?.layer.setNeedsDisplay()
delegate.layer.setNeedsDisplay()
}
}
@ -114,12 +70,12 @@ public class AnimatedImage: UIImage {
}
func resumeAnimation() {
if isAnimated {
if totalDuration > 0 {
displayLink.paused = false
}
}
func isAnimating() -> Bool {
return !displayLink.paused
func attachDisplayLink() {
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
}

View File

@ -1,37 +0,0 @@
import UIKit
public extension UIImageView {
// MARK: - Computed Properties
var animatableImage: AnimatedImage? {
return image as? AnimatedImage
}
var isAnimatingGIF: Bool {
return animatableImage?.isAnimating() ?? isAnimating()
}
var animatable: Bool {
return animatableImage != .None
}
// MARK: - Method Overrides
override public func displayLayer(layer: CALayer!) {
layer.contents = animatableImage?.currentFrame?.CGImage
}
// MARK: - Setter Methods
public func setAnimatedImage(image: AnimatedImage) {
image.delegate = self
self.image = image
layer.setNeedsDisplay()
}
// MARK: - Animation
func startAnimatingGIF() {
animatableImage?.resumeAnimation() ?? startAnimating()
}
func stopAnimatingGIF() {
animatableImage?.pauseAnimation() ?? stopAnimating()
}
}