├── .readme.d ├── animations-timing-example.gif ├── interpolate-block-animations-example.gif └── interpolate-spring-animations-example.gif ├── UIAnimationToolboxCategory-iOS ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── UIView+Additions.swift ├── Info.plist ├── Base.lproj │ └── LaunchScreen.storyboard ├── CategoryViewController.swift ├── AppDelegate.swift ├── AnimationsTimingControlSegments.xib ├── AnimationInterpolatorViewController.swift ├── AnimationsTimingControlSwitch.xib ├── AnimationsTimingControlStepper.xib ├── CustomPropertiesViewController.swift ├── AnimationsTimingAnimationView.swift ├── AnimationsTimingControl.swift └── AnimationsTimingViewController.swift ├── UIAnimationToolbox.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── UIAnimationToolbox.xcscheme ├── UIAnimationToolbox ├── UIKit │ ├── Actions │ │ ├── _UIAnimationFactoryAction.swift │ │ ├── _UIAnimationFactoryAdditiveAction.swift │ │ ├── UIAnimationActionPreset.swift │ │ ├── UIAnimationActionInferred.swift │ │ └── UIAnimationAction.swift │ ├── AnimationFactory │ │ ├── _UIKeyframeAnimationFactory.swift │ │ ├── _UIConventionalAnimationFactory.swift │ │ ├── _UIAnimationTemplate.swift │ │ ├── _UIBlockAnimationFactory2.swift │ │ ├── _UIBlockAnimationFactory3.swift │ │ ├── _UIBlockAnimationFactory5.swift │ │ ├── _UISpringAnimationFactory.swift │ │ ├── _UIAnimationFactory.swift │ │ └── _UIBasicAnimationFactory.swift │ ├── ToolboxAdditions │ │ └── UIViewAnimationOptions+ToolboxAdditions.swift │ ├── UIView │ │ ├── UIView+AnimationInterpolator.swift │ │ ├── UIView+AnimationContext.swift │ │ ├── UIView+AnimationsTiming.swift │ │ ├── UIView+LayerAction.swift │ │ └── UIView+Animate.swift │ ├── AnimationInterpolator │ │ ├── _UIAnimationInterpolatorLayer.swift │ │ ├── _UIAnimationInterpolateWindow.swift │ │ ├── _UIAnimationInterpolator.swift │ │ └── _UIAnimationInterpolatorView.swift │ ├── AnimationContext │ │ ├── _UIAnimationContextInternal.swift │ │ └── UIAnimationContext.swift │ └── AnimationsTiming │ │ └── UIAnimationTiming.swift ├── Supported Files │ ├── ___UIAnimationToolbox.m │ ├── ___UIAnimationToolbox.swift │ └── Info.plist └── QuartzCore │ ├── ToolboxAdditions │ ├── CAMediaTiming+ToolboxAdditions.swift │ ├── CATransaction+ToolboxAdditions.swift │ ├── CALayer+ToolboxAdditions.swift │ └── CAMediaTimingFunction+ToolboxAdditions.swift │ ├── EmulatingQuartzCoreHierarchy │ ├── _CAProtocols.swift │ └── _CAAnimationPrototypes.swift │ └── Actions │ └── CABasicAnimationAdditiveAction.swift ├── UIAnimationToolbox.podspec ├── UIAnimationToolboxTests ├── Info.plist ├── UIPropertyAnimationTests.swift ├── UIAnimationActionInferredTests.swift ├── UIAnimationActionPresetTests.swift ├── UIAnimationActionTests.swift └── UIView_AnimationsTimingTests.swift ├── LICENSE ├── .travis.yml ├── .gitignore ├── 使用說明.md └── README.md /.readme.d/animations-timing-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeZZard/UIAnimationToolbox/HEAD/.readme.d/animations-timing-example.gif -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /.readme.d/interpolate-block-animations-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeZZard/UIAnimationToolbox/HEAD/.readme.d/interpolate-block-animations-example.gif -------------------------------------------------------------------------------- /.readme.d/interpolate-spring-animations-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeZZard/UIAnimationToolbox/HEAD/.readme.d/interpolate-spring-animations-example.gif -------------------------------------------------------------------------------- /UIAnimationToolbox.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/Actions/_UIAnimationFactoryAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationFactoryAction.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 1/29/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | internal protocol _UIAnimationFactoryAction: CAAction { 12 | associatedtype Animation: CAAnimation 13 | } 14 | -------------------------------------------------------------------------------- /UIAnimationToolbox.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/UIView+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Additions.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | extension UIView { 12 | static func makeFromNib() -> T { 13 | return Bundle.main.loadNibNamed(String(describing: T.self), owner: self, options: nil)![0] as! T 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /UIAnimationToolbox/Supported Files/___UIAnimationToolbox.m: -------------------------------------------------------------------------------- 1 | // 2 | // ___UIAnimationToolbox.m 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 2019/3/13. 6 | // 7 | 8 | #import 9 | 10 | __attribute__((constructor)) 11 | void _UIAnimationToolboxLoad(void) { 12 | __attribute__((unused)) ___UIAnimationToolbox * _ = [[___UIAnimationToolbox alloc] init]; 13 | } 14 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UIKeyframeAnimationFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIKeyframeAnimationFactory.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/4/10. 6 | // 7 | 8 | import UIKit 9 | 10 | internal class _UIKeyframeAnimationFactory< 11 | A: CAKeyframeAnimation, P: _CAKeyframeAnimationProtocol 12 | >: _UIAnimationFactory where 13 | P.Animation == A 14 | { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UIConventionalAnimationFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIConventionalAnimationFactory.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/4/10. 6 | // 7 | 8 | import UIKit 9 | 10 | internal class _UIConventionalAnimationFactory< 11 | A: CABasicAnimation, P: _CABasicAnimationProtocol 12 | >: _UIBasicAnimationFactory where 13 | P.Animation == A 14 | { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/ToolboxAdditions/UIViewAnimationOptions+ToolboxAdditions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewAnimationOptions+ToolboxAdditions.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 8/26/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView.AnimationOptions { 12 | public init(animationCurve: UIView.AnimationCurve) { 13 | self.init(rawValue: UInt(animationCurve.rawValue) << 16) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/UIView/UIView+AnimationInterpolator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+AnimationInterpolator.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 17/02/2017. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | public static func addAnimationInterpolator( 13 | _ interpolator: @escaping (_ progress : CGFloat) -> Void 14 | ) 15 | { 16 | _ = _UIAnimationInterpolator(interpolator: interpolator) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationInterpolator/_UIAnimationInterpolatorLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationInterpolatorLayer.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | 10 | internal class _UIAnimationInterpolatorLayer: CALayer { 11 | @NSManaged 12 | internal var progress: CGFloat 13 | 14 | internal override class func needsDisplay(forKey event: String) -> Bool { 15 | if event == "progress" { 16 | return true 17 | } 18 | return super.needsDisplay(forKey: event) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationContext/_UIAnimationContextInternal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationContextInternal.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 1/29/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | internal protocol _UIAnimationContextInternal: UIAnimationContext { 12 | func action(for layer: CALayer, forKey event: String, style: UIAnimationActionStyle) -> CAAction? 13 | 14 | var animationTimings: [UIAnimationTiming] { get set } 15 | } 16 | 17 | extension _UIAnimationContextInternal { 18 | internal var currentAnimationTiming: UIAnimationTiming? { 19 | return animationTimings.last 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /UIAnimationToolbox/QuartzCore/ToolboxAdditions/CAMediaTiming+ToolboxAdditions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CAMediaTiming+ToolboxAdditions.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 11/20/15. 6 | // 7 | // 8 | 9 | import QuartzCore 10 | 11 | extension CAMediaTiming { 12 | /// Return an animation duration deducted by rules described in Apple's Core 13 | /// Animation Programming Guide. 14 | public var deducedDuration: CFTimeInterval { 15 | if self.duration == 0 { 16 | if let duration = CATransaction.duration { 17 | return duration 18 | } else { 19 | return 0.25 20 | } 21 | } else { 22 | return self.duration 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /UIAnimationToolbox.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "UIAnimationToolbox" 3 | spec.version = "0.0.3" 4 | spec.license = { :type => "MIT", :file => "LICENSE" } 5 | spec.homepage = 'https://github.com/WeZZard/UIAnimationToolbox' 6 | spec.author = { "WeZZard" => "https://wezzard.com" } 7 | spec.summary = "A framework makes use of advanced Core Animation features without leaving UIKit." 8 | spec.source = { :git => "https://github.com/WeZZard/UIAnimationToolbox.git", :tag => '0.0.3'} 9 | spec.source_files = 'UIAnimationToolbox/**/*.{swift,m}' 10 | spec.module_name = 'UIAnimationToolbox' 11 | spec.ios.deployment_target = '8.0' 12 | spec.swift_versions = ['5.1', '5.0', '4.2'] 13 | end 14 | -------------------------------------------------------------------------------- /UIAnimationToolboxTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /UIAnimationToolbox/Supported Files/___UIAnimationToolbox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___UIAnimationToolbox.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 2019/3/13. 6 | // 7 | 8 | import Foundation 9 | 10 | public class ___UIAnimationToolbox: NSObject { 11 | public override init() { 12 | super.init() 13 | struct Token { 14 | static var once: Bool = { 15 | _swizzleCALayerDelegateActionForLayerForKey() 16 | _swizzleAnimateWithDuration5() 17 | _swizzleAnimateWithDuration3() 18 | _swizzleAnimateWithDuration2() 19 | if #available(iOS 9.0, *) { 20 | _swizzleSpringAnimationAPI() 21 | } 22 | return true 23 | }() 24 | } 25 | _ = Token.once 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /UIAnimationToolbox/Supported Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/UIView/UIView+AnimationContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+AnimationContext.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | public static var isInAnimationsBlock: Bool { 12 | return _currentAnimationContext != nil 13 | } 14 | 15 | public static var currentAnimationContext: UIAnimationContext? { 16 | return _animationContexts.last 17 | } 18 | 19 | public var currentAnimationContext: UIAnimationContext? { 20 | return UIView.currentAnimationContext 21 | } 22 | } 23 | 24 | extension UIView { 25 | internal static var _animationContexts = [_UIAnimationContextInternal]() 26 | 27 | internal static var _currentAnimationContext: _UIAnimationContextInternal? { 28 | return UIView._animationContexts.last 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationInterpolator/_UIAnimationInterpolateWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationInterpolateWindow.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/16. 6 | // 7 | 8 | import UIKit 9 | 10 | internal class _UIAnimationInterpolateWindow: UIWindow { 11 | internal static let shared = _UIAnimationInterpolateWindow(frame: .zero) 12 | 13 | private override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | _commonInit() 16 | } 17 | 18 | required init?(coder aDecoder: NSCoder) { 19 | super.init(coder: aDecoder) 20 | _commonInit() 21 | } 22 | 23 | private func _commonInit() { 24 | UIView.performWithoutAnimation { 25 | windowLevel = UIWindow.Level(-1) 26 | alpha = 0 27 | isHidden = false 28 | isUserInteractionEnabled = true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UIAnimationTemplate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationTemplate.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 1/29/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | internal class _UIAnimationTemplate< 12 | A: CAAnimation, P: _CAAnimationProtocol 13 | >: UIView where A == P.Animation 14 | { 15 | internal typealias Animation = A 16 | internal typealias AnimationPrototype = P 17 | 18 | internal init() { super.init(frame: UIScreen.main.bounds) } 19 | 20 | internal required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | internal lazy var animationPrototype: AnimationPrototype? = { 25 | let action = self.layer.action(forKey: "opacity") 26 | guard let applicableAnimation = action as? Animation else { 27 | return nil 28 | } 29 | return AnimationPrototype(animation: applicableAnimation) 30 | }() 31 | } 32 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/Actions/_UIAnimationFactoryAdditiveAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationFactoryAdditiveAction.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/16. 6 | // 7 | 8 | import UIKit 9 | 10 | internal class _UIAnimationFactoryAdditiveAction: 11 | _UIAnimationFactoryAction 12 | { 13 | internal typealias Animation = A 14 | 15 | internal var pendingAnimation: Animation { 16 | return action.pendingAnimation 17 | } 18 | 19 | internal let action: CABasicAnimationAdditiveAction 20 | 21 | internal init(layer: CALayer, event: String, pendingAnimation: Animation) { 22 | action = CABasicAnimationAdditiveAction( 23 | layer: layer, event: event, pendingAnimation: pendingAnimation 24 | ) 25 | } 26 | 27 | @objc 28 | internal func run( 29 | forKey event: String, 30 | object anObject: Any, 31 | arguments dict: [AnyHashable : Any]? 32 | ) 33 | { 34 | action.run(forKey: event, object: anObject, arguments: dict) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 WeZZard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationInterpolator/_UIAnimationInterpolator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationInterpolator.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/16. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | internal class _UIAnimationInterpolator: 12 | _UIAnimationInterpolatorViewDelegate 13 | { 14 | private var _impl: _UIAnimationInterpolatorView! 15 | 16 | private let _interpolator: (_ progress : CGFloat) -> Void 17 | 18 | internal init(interpolator: @escaping (_ progress : CGFloat) -> Void) { 19 | _interpolator = interpolator 20 | 21 | _impl = _UIAnimationInterpolatorView(delegate: self) 22 | 23 | UIView.performWithoutAnimation { 24 | _impl.progress = 0 25 | _UIAnimationInterpolateWindow.shared.addSubview(self._impl) 26 | } 27 | 28 | CATransaction.withCoordinatedTransaction { (transaction) in 29 | transaction.completionHandler = { 30 | let retainedSelf = self 31 | retainedSelf._impl.removeFromSuperview() 32 | } 33 | _impl.progress = 1 34 | } 35 | } 36 | 37 | internal func animationInterpolatorView( 38 | _ sender: _UIAnimationInterpolatorView, 39 | didChangeProgress progress: CGFloat 40 | ) 41 | { 42 | _interpolator(progress) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/Actions/UIAnimationActionPreset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAnimationActionPreset.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/16. 6 | // 7 | 8 | import UIKit 9 | 10 | @available(*, renamed: "UIAnimationAction", message: "Use UIAnimationAction with .preset style specified instead.") 11 | public class UIAnimationActionPreset: CAAction { 12 | 13 | private let _presetAction: CAAction 14 | 15 | internal init(presetAction: CAAction) { 16 | _presetAction = presetAction 17 | } 18 | 19 | public convenience init?(layer: CALayer, event: String) { 20 | if let action = UIView._currentAnimationContext? 21 | .action(for: layer, forKey: event, style: .preset) 22 | { 23 | self.init(presetAction: action) 24 | } else { 25 | return nil 26 | } 27 | } 28 | 29 | public static func make(layer: CALayer, event: String) -> CAAction { 30 | if let action = UIAnimationActionPreset(layer: layer, event: event) { 31 | return action 32 | } 33 | return NSNull() 34 | } 35 | 36 | @objc 37 | public func run( 38 | forKey event: String, 39 | object anObject: Any, 40 | arguments dict: [AnyHashable : Any]? 41 | ) 42 | { 43 | _presetAction.run(forKey: event, object: anObject, arguments: dict) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/Actions/UIAnimationActionInferred.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAnimationActionInferred.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/16. 6 | // 7 | 8 | import UIKit 9 | 10 | @available(*, renamed: "UIAnimationAction", message: "Use UIAnimationAction with .inferred style specified instead.") 11 | public class UIAnimationActionInferred: CAAction { 12 | private let _inferredAction: CAAction 13 | 14 | internal init(inferredAction: CAAction) { 15 | _inferredAction = inferredAction 16 | } 17 | 18 | public convenience init?(layer: CALayer, event: String) { 19 | if let action = UIView._currentAnimationContext? 20 | .action(for: layer, forKey: event, style: .inferred) 21 | { 22 | self.init(inferredAction: action) 23 | } else { 24 | return nil 25 | } 26 | } 27 | 28 | public static func make(layer: CALayer, event: String) -> CAAction { 29 | if let action = UIAnimationActionInferred(layer: layer, event: event) { 30 | return action 31 | } 32 | return NSNull() 33 | } 34 | 35 | @objc 36 | public func run( 37 | forKey event: String, 38 | object anObject: Any, 39 | arguments dict: [AnyHashable : Any]? 40 | ) 41 | { 42 | _inferredAction.run(forKey: event, object: anObject, arguments: dict) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode10.2 3 | branches: 4 | only: 5 | - master 6 | cache: bundler 7 | env: 8 | global: 9 | - LC_CTYPE=en_US.UTF-8 10 | - LANG=en_US.UTF-8 11 | - PROJECT=UIAnimationToolbox.xcodeproj 12 | - IOS_FRAMEWORK_SCHEME="UIAnimationToolbox" 13 | - TVOS_FRAMEWORK_SCHEME="UIAnimationToolbox" 14 | matrix: 15 | - DESTINATION="OS=12.2,name=iPhone XS" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" 16 | - DESTINATION="OS=11.4,name=iPhone X" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" 17 | - DESTINATION="OS=10.3.1,name=iPhone 7 Plus" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" 18 | 19 | - DESTINATION="OS=12.2,name=Apple TV 4K" SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" 20 | - DESTINATION="OS=11.4,name=Apple TV 4K" SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" 21 | - DESTINATION="OS=10.2,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" 22 | script: 23 | - set -o pipefail 24 | - xcodebuild -version 25 | - xcodebuild -showsdks 26 | 27 | # Build Framework in Release and Run Tests if specified 28 | - if [ $RUN_TESTS == "YES" ]; then 29 | xcodebuild -project "$PROJECT" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty; 30 | else 31 | xcodebuild -project "$PROJECT" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty; 32 | fi 33 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UIBlockAnimationFactory2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIBlockAnimationFactory2.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 2019/3/13. 6 | // 7 | 8 | import UIKit 9 | 10 | internal class _UIBlockAnimationFactory2: 11 | _UIBasicAnimationFactory< 12 | CABasicAnimation, 13 | _CABasicAnimationPrototype 14 | > 15 | { 16 | internal typealias OriginalImplementation = _UIViewAnimateWithDuration2 17 | 18 | private let _selector: Selector 19 | private let _originalImpl: OriginalImplementation 20 | 21 | private var animationTemplate_ : AnimationTemplate! 22 | 23 | internal override var animationTemplate: AnimationTemplate { 24 | if animationTemplate_ == nil { 25 | var template: AnimationTemplate! 26 | 27 | _originalImpl( 28 | UIView.self, 29 | _selector, 30 | duration, 31 | {template = AnimationTemplate()} 32 | ) 33 | 34 | animationTemplate_ = template 35 | } 36 | return animationTemplate_ 37 | } 38 | 39 | internal init( 40 | duration: TimeInterval, 41 | selector: Selector, 42 | originalImpl: @escaping OriginalImplementation 43 | ) 44 | { 45 | _selector = selector 46 | _originalImpl = originalImpl 47 | super.init(duration: duration, delay: 0, options: []) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UIBlockAnimationFactory3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIBlockAnimationFactory3.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 2019/3/13. 6 | // 7 | 8 | import UIKit 9 | 10 | internal class _UIBlockAnimationFactory3: 11 | _UIBasicAnimationFactory< 12 | CABasicAnimation, 13 | _CABasicAnimationPrototype 14 | > 15 | { 16 | internal typealias OriginalImplementation = _UIViewAnimateWithDuration3 17 | 18 | private let _selector: Selector 19 | private let _originalImpl: OriginalImplementation 20 | 21 | private var animationTemplate_ : AnimationTemplate! 22 | 23 | internal override var animationTemplate: AnimationTemplate { 24 | if animationTemplate_ == nil { 25 | var template: AnimationTemplate! 26 | 27 | _originalImpl( 28 | UIView.self, 29 | _selector, 30 | duration, 31 | {template = AnimationTemplate()}, 32 | nil 33 | ) 34 | 35 | animationTemplate_ = template 36 | } 37 | return animationTemplate_ 38 | } 39 | 40 | internal init( 41 | duration: TimeInterval, 42 | selector: Selector, 43 | originalImpl: @escaping OriginalImplementation 44 | ) 45 | { 46 | _selector = selector 47 | _originalImpl = originalImpl 48 | super.init(duration: duration, delay: 0, options: []) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationContext/UIAnimationContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAnimationContext.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 1/31/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol UIAnimationContext: NSObjectProtocol { 12 | /// Use this function to generate a custom animation in UIKit 13 | /// animation block. 14 | /// 15 | /// - Notes: Both UIView custom animatable properties and preset 16 | /// animatable properties do not use this function to generate 17 | /// animation. 18 | func createAnimation() -> CAAnimation 19 | 20 | /// Use this function to configure the animation generated by 21 | /// `createAnimation()` with the parameters derived from UIKit 22 | /// animation block. 23 | /// 24 | /// - Notes: UIView custom animatable properties also use this 25 | /// function to configure the animation with the parameters derived 26 | /// from UIKit animation API. 27 | func configureAnimation(_ animation: CAAnimation) -> Bool 28 | 29 | /// Tells the type of animations generated by `createAnimation()`. 30 | /// 31 | /// Returns `CABasicAnimation` in block of regular UIKit animation, 32 | /// `CASpringAnimation` in block of spring UIKit animation. Doesn't 33 | /// support keyframe animation currently. 34 | var animationType: CAAnimation.Type { get } 35 | 36 | /// Returns the duration setting of the UIKit animation block 37 | var duration: TimeInterval { get } 38 | 39 | /// Returns the delay setting of the UIKit animation block 40 | var delay: TimeInterval { get } 41 | } 42 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UIBlockAnimationFactory5.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIBlockAnimationFactory5.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 03/04/2017. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | internal class _UIBlockAnimationFactory5: 12 | _UIBasicAnimationFactory< 13 | CABasicAnimation, 14 | _CABasicAnimationPrototype 15 | > 16 | { 17 | internal typealias OriginalImplementation = _UIViewAnimateWithDuration5 18 | 19 | private let _selector: Selector 20 | private let _originalImpl: OriginalImplementation 21 | 22 | private var animationTemplate_ : AnimationTemplate! 23 | 24 | internal override var animationTemplate: AnimationTemplate { 25 | if animationTemplate_ == nil { 26 | var template: AnimationTemplate! 27 | 28 | _originalImpl( 29 | UIView.self, 30 | _selector, 31 | duration, 32 | delay, 33 | options, 34 | {template = AnimationTemplate()}, 35 | nil 36 | ) 37 | 38 | animationTemplate_ = template 39 | } 40 | return animationTemplate_ 41 | } 42 | 43 | internal init( 44 | duration: TimeInterval, 45 | delay: TimeInterval, 46 | options: UIView.AnimationOptions, 47 | selector: Selector, 48 | originalImpl: @escaping OriginalImplementation 49 | ) 50 | { 51 | _selector = selector 52 | _originalImpl = originalImpl 53 | super.init(duration: duration, delay: delay, options: options) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | AnimationToolbox 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /UIAnimationToolbox/QuartzCore/ToolboxAdditions/CATransaction+ToolboxAdditions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CATransaction+ToolboxAdditions.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 23/01/2017. 6 | // 7 | // 8 | 9 | import QuartzCore 10 | 11 | extension CATransaction { 12 | @objc 13 | public class var completionHandler: (() -> Void)? { 14 | @objc(uiat_completionHandler) 15 | get { return completionBlock() } 16 | @objc(uiat_setCompletionHandler:) 17 | set { setCompletionBlock(newValue) } 18 | } 19 | 20 | public class var duration: CFTimeInterval? { 21 | get { 22 | if let raw = value(forKey: kCATransactionAnimationDuration) { 23 | return (raw as! NSNumber).doubleValue 24 | } 25 | return nil 26 | } 27 | set { 28 | setValue(newValue, forKey: kCATransactionAnimationDuration) 29 | } 30 | } 31 | 32 | @objc 33 | public class var disablesActions: Bool { 34 | @objc(uiat_disablesActions) 35 | get { return disableActions() } 36 | @objc(uiat_setDisablesActions:) 37 | set { setDisableActions(newValue) } 38 | } 39 | 40 | @objc 41 | public class var timingFunction: CAMediaTimingFunction? { 42 | @objc(uiat_timingFunction) 43 | get { return animationTimingFunction() } 44 | @objc(uiat_setTimingFunction:) 45 | set { setAnimationTimingFunction(newValue) } 46 | } 47 | 48 | @objc(uiat_coordinatedTransactionWithBlock:) 49 | public class func withCoordinatedTransaction( 50 | _ closure: (_: CATransaction.Type) -> Void 51 | ) 52 | { 53 | begin() 54 | closure(self) 55 | commit() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/UIView/UIView+AnimationsTiming.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+AnimationsTiming.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | /// Shifts animations timing with `timing` and then returns the given 12 | /// timing object. 13 | /// 14 | @discardableResult 15 | public class func shiftAnimationsTiming( 16 | by timing: UIAnimationTiming, 17 | animations: () -> Void 18 | ) -> UIAnimationTiming 19 | { 20 | _currentAnimationContext?.animationTimings.append(timing) 21 | animations() 22 | _currentAnimationContext?.animationTimings.removeLast() 23 | return timing 24 | } 25 | 26 | /// Shifts animations timing with given timing setup and then returns 27 | /// the timing object being used in this timing shifting. 28 | /// 29 | @discardableResult 30 | public class func shiftAnimationsTiming( 31 | beginTime: TimeInterval?=nil, 32 | duration: TimeInterval?=nil, 33 | speed: CGFloat?=nil, 34 | timeOffset: TimeInterval?=nil, 35 | repeating: UIAnimationTimingRepeating?=nil, 36 | autoreverses: Bool?=nil, 37 | fillMode: UIAnimationTimingFillMode?=nil, 38 | animations: () -> Void 39 | ) -> UIAnimationTiming 40 | { 41 | let animationTiming = UIAnimationTiming( 42 | beginTime: beginTime, 43 | duration: duration, 44 | speed: speed, 45 | timeOffset: timeOffset, 46 | repeating: repeating, 47 | autoreverses: autoreverses, 48 | fillMode: fillMode 49 | ) 50 | _currentAnimationContext?.animationTimings.append(animationTiming) 51 | animations() 52 | _currentAnimationContext?.animationTimings.removeLast() 53 | return animationTiming 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UISpringAnimationFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UISpringAnimationFactory.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 03/04/2017. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS 9.0, *) 12 | internal class _UISpringAnimationFactory: 13 | _UIBasicAnimationFactory< 14 | CASpringAnimation, 15 | _CASpringAnimationPrototype 16 | > 17 | { 18 | internal let damping: CGFloat 19 | internal let initialVelocity: CGFloat 20 | 21 | internal typealias OriginalImplementation = _UIViewSpringAnimation 22 | 23 | private let _selector: Selector 24 | 25 | private let _originalImpl: OriginalImplementation 26 | 27 | private var animationTemplate_ : AnimationTemplate! 28 | 29 | internal override var animationTemplate: AnimationTemplate { 30 | if animationTemplate_ == nil { 31 | var template: AnimationTemplate! 32 | 33 | _originalImpl( 34 | UIView.self, 35 | _selector, 36 | duration, 37 | delay, 38 | damping, 39 | initialVelocity, 40 | options, 41 | {template = AnimationTemplate()}, 42 | nil 43 | ) 44 | 45 | animationTemplate_ = template 46 | } 47 | return animationTemplate_ 48 | } 49 | 50 | internal init( 51 | duration: TimeInterval, 52 | delay: TimeInterval, 53 | damping: CGFloat, 54 | initialVelocity: CGFloat, 55 | options: UIView.AnimationOptions, 56 | selector: Selector, 57 | originalImpl: @escaping OriginalImplementation 58 | ) 59 | { 60 | self.damping = damping 61 | self.initialVelocity = initialVelocity 62 | _selector = selector 63 | _originalImpl = originalImpl 64 | super.init(duration: duration, delay: delay, options: options) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/UIView/UIView+LayerAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+LayerAction.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 9/14/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | import ObjectiveC 11 | 12 | //MARK: - Swizzle UIView's CALayerDelegate 13 | 14 | internal typealias _CALayerDelegateActionForLayerForKey = 15 | @convention(c) (UIView, Selector, CALayer, String) 16 | -> CAAction? 17 | 18 | internal var _originalCALayerDelegateActionForLayerForKey: _CALayerDelegateActionForLayerForKey { 19 | return _originalActionForLayerForKey 20 | } 21 | 22 | private var _originalActionForLayerForKey : _CALayerDelegateActionForLayerForKey! 23 | 24 | private let _actionForLayerForKey: _CALayerDelegateActionForLayerForKey = { 25 | (aSelf, aSelector, layer, event) -> CAAction? in 26 | 27 | if let context = UIView._currentAnimationContext { 28 | if let originalAction = _originalActionForLayerForKey( 29 | aSelf, 30 | aSelector, 31 | layer, 32 | event 33 | ) 34 | { 35 | if let timing = context.currentAnimationTiming { 36 | timing._shiftAction(originalAction) 37 | } 38 | 39 | return originalAction 40 | } else { 41 | return nil 42 | } 43 | } else { 44 | return _originalActionForLayerForKey(aSelf, aSelector, layer, event) 45 | } 46 | } 47 | 48 | internal func _swizzleCALayerDelegateActionForLayerForKey() { 49 | struct Token { 50 | static let once: Bool = { 51 | let sel = #selector(CALayerDelegate.action(for:forKey:)) 52 | let method = class_getInstanceMethod(UIView.self, sel)! 53 | let impl = method_getImplementation(method) 54 | _originalActionForLayerForKey = unsafeBitCast(impl, to: _CALayerDelegateActionForLayerForKey.self) 55 | method_setImplementation(method, unsafeBitCast(_actionForLayerForKey, to: IMP.self)) 56 | return true 57 | }() 58 | } 59 | _ = Token.once 60 | } 61 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /UIAnimationToolbox/QuartzCore/EmulatingQuartzCoreHierarchy/_CAProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _CAInterconvertible.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 1/29/16. 6 | // 7 | // 8 | 9 | import QuartzCore 10 | 11 | internal protocol _CAMediaTimingProtocol { 12 | var beginTime: CFTimeInterval { get } 13 | var timeOffset: TimeInterval { get } 14 | var repeatCount: Float { get } 15 | var repeatDuration: CFTimeInterval { get } 16 | var duration: CFTimeInterval { get } 17 | var speed: Float { get } 18 | var autoreverses: Bool { get } 19 | var fillMode: CAMediaTimingFillMode { get } 20 | } 21 | 22 | internal protocol _CAAnimationProtocol: _CAMediaTimingProtocol { 23 | associatedtype Animation 24 | 25 | var isRemovedOnCompletion: Bool { get } 26 | var timingFunction: CAMediaTimingFunction? { get } 27 | var delegate: CAAnimationDelegate? { get } 28 | 29 | init(animation: Animation) 30 | func apply(on animation: Animation) 31 | } 32 | 33 | internal protocol _CATransitionProtocol: _CAAnimationProtocol { } 34 | 35 | internal protocol _CAPropertyAnimationProtocol: 36 | _CAAnimationProtocol 37 | { 38 | var cumulative: Bool { get } 39 | var additive: Bool { get } 40 | } 41 | 42 | internal protocol _CABasicAnimationProtocol: 43 | _CAPropertyAnimationProtocol 44 | { 45 | 46 | } 47 | 48 | internal protocol _CASpringAnimationProtocol: 49 | _CABasicAnimationProtocol 50 | { 51 | var mass: CGFloat { get } 52 | var stiffness: CGFloat { get } 53 | var damping: CGFloat { get } 54 | var initialVelocity: CGFloat { get } 55 | } 56 | 57 | internal protocol _CAKeyframeAnimationProtocol: 58 | _CAPropertyAnimationProtocol 59 | { 60 | var keyTimes: [NSNumber]? { get } 61 | var timingFunctions: [CAMediaTimingFunction]? { get } 62 | var calculationMode: CAAnimationCalculationMode { get } 63 | var rotationMode: CAAnimationRotationMode? { get } 64 | var tensionValues: [NSNumber]? { get } 65 | var continuityValues: [NSNumber]? { get } 66 | var biasValues: [NSNumber]? { get } 67 | } 68 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/Actions/UIAnimationAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAnimationAction.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 11/22/15. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | public enum UIAnimationActionStyle { 12 | /// Get preset animations for given layer and event. May not have 13 | /// effects on custom properties. 14 | /// 15 | case preset 16 | 17 | /// Get inferred animations for given layer and event. Returns an 18 | /// animation action when the wrapping animation API is supported by 19 | /// the framework. 20 | /// 21 | case inferred 22 | } 23 | 24 | 25 | public class UIAnimationAction: CAAction { 26 | internal let _action: CAAction 27 | 28 | internal let _style: UIAnimationActionStyle 29 | 30 | internal init(action: CAAction, style: UIAnimationActionStyle) { 31 | _action = action 32 | _style = style 33 | } 34 | 35 | public convenience init?( 36 | layer: CALayer, 37 | event: String, 38 | style: UIAnimationActionStyle 39 | ) 40 | { 41 | if let action = UIView 42 | ._currentAnimationContext? 43 | .action(for: layer, forKey: event, style: style) 44 | { 45 | self.init(action: action, style: style) 46 | } else { 47 | return nil 48 | } 49 | } 50 | 51 | public static func make( 52 | layer: CALayer, 53 | event: String, 54 | style: UIAnimationActionStyle 55 | ) -> CAAction 56 | { 57 | if let action = UIAnimationAction( 58 | layer: layer, 59 | event: event, 60 | style: style 61 | ) 62 | { 63 | return action 64 | } 65 | return NSNull() 66 | } 67 | 68 | @objc 69 | public func run( 70 | forKey event: String, 71 | object anObject: Any, 72 | arguments dict: [AnyHashable : Any]? 73 | ) 74 | { 75 | _action.run(forKey: event, object: anObject, arguments: dict) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/CategoryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryViewController.swift 3 | // UIAnimationToolboxCategory-iOS 4 | // 5 | // Created by WeZZard on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | 10 | class CategoryViewController: UITableViewController { 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | // Do any additional setup after loading the view, typically from a nib. 14 | } 15 | 16 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 17 | switch segue.identifier { 18 | case "showBeginTimeExample": 19 | let destinationVC = segue.destination as! AnimationsTimingViewController 20 | destinationVC.configuration = .beginTime 21 | case "showDurationExample": 22 | let destinationVC = segue.destination as! AnimationsTimingViewController 23 | destinationVC.configuration = .duration 24 | case "showSpeedExample": 25 | let destinationVC = segue.destination as! AnimationsTimingViewController 26 | destinationVC.configuration = .speed 27 | case "showTimeOffsetExample": 28 | let destinationVC = segue.destination as! AnimationsTimingViewController 29 | destinationVC.configuration = .timeOffset 30 | case "showRepeatingByCountExample": 31 | let destinationVC = segue.destination as! AnimationsTimingViewController 32 | destinationVC.configuration = .repeatingByCount 33 | case "showRepeatingByDurationExample": 34 | let destinationVC = segue.destination as! AnimationsTimingViewController 35 | destinationVC.configuration = .repeatingByDuration 36 | case "showAutoreversesExample": 37 | let destinationVC = segue.destination as! AnimationsTimingViewController 38 | destinationVC.configuration = .autoreverses 39 | case "showFillModeExample": 40 | let destinationVC = segue.destination as! AnimationsTimingViewController 41 | destinationVC.configuration = .fillMode 42 | default: 43 | break 44 | } 45 | super.prepare(for: segue, sender: sender) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // UIAnimationToolboxCategory-iOS 4 | // 5 | // Created by WeZZard on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationInterpolator/_UIAnimationInterpolatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationInterpolatorView.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/16. 6 | // 7 | 8 | import UIKit 9 | 10 | internal protocol _UIAnimationInterpolatorViewDelegate: class { 11 | func animationInterpolatorView( 12 | _ sender: _UIAnimationInterpolatorView, 13 | didChangeProgress progress: CGFloat 14 | ) 15 | } 16 | 17 | 18 | internal class _UIAnimationInterpolatorView: UIView { 19 | internal unowned let delegate: _UIAnimationInterpolatorViewDelegate 20 | 21 | internal init(delegate: _UIAnimationInterpolatorViewDelegate) { 22 | self.delegate = delegate 23 | super.init(frame: .zero) 24 | } 25 | 26 | internal required init?(coder aDecoder: NSCoder) { 27 | self.delegate = aDecoder.decodeObject(forKey: "delegate") as! _UIAnimationInterpolatorViewDelegate 28 | super.init(coder: aDecoder) 29 | } 30 | 31 | internal override func encode(with aCoder: NSCoder) { 32 | super.encode(with: aCoder) 33 | aCoder.encode(delegate, forKey: "delegate") 34 | } 35 | 36 | internal var progress: CGFloat { 37 | get { return _layer.progress } 38 | set { _layer.progress = newValue } 39 | } 40 | 41 | private var _layer: _UIAnimationInterpolatorLayer { 42 | return layer as! _UIAnimationInterpolatorLayer 43 | } 44 | 45 | internal override func action(for layer: CALayer, forKey event: String) 46 | -> CAAction? 47 | { 48 | if UIView.isInAnimationsBlock && UIView.areAnimationsEnabled { 49 | if layer === layer && event == "progress" { 50 | return UIAnimationActionInferred(layer: layer, event: event) 51 | } 52 | } 53 | return super.action(for: layer, forKey: event) 54 | } 55 | 56 | internal override class var layerClass: AnyClass { 57 | return _UIAnimationInterpolatorLayer.self 58 | } 59 | 60 | internal func animationInterpolatorLayer( 61 | _ sender: _UIAnimationInterpolatorLayer, 62 | didChangeProgress progress: CGFloat 63 | ) 64 | { 65 | delegate.animationInterpolatorView( 66 | self, didChangeProgress: CGFloat(sender.opacity) 67 | ) 68 | } 69 | 70 | internal override func display(_ layer: CALayer) { 71 | let interpolatorLayer = layer as! _UIAnimationInterpolatorLayer 72 | guard let presentationLayer = interpolatorLayer.presentation() 73 | else { return } 74 | 75 | let progress = presentationLayer.progress 76 | 77 | delegate.animationInterpolatorView(self, didChangeProgress: progress) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /UIAnimationToolboxTests/UIPropertyAnimationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIPropertyAnimationTests.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/16. 6 | // 7 | 8 | import XCTest 9 | import UIAnimationToolbox 10 | 11 | class UIPropertyAnimationTests: XCTestCase { 12 | func testPropertyAnimation_hasTheSameDelegateToSystemAnimations() { 13 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 14 | let subview = _UIPropertyAnimationView(animatedProperty: 0) 15 | view.addSubview(subview) 16 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: { 17 | subview.animatedProperty = 0.5 18 | subview.alpha = 0.5 19 | }) { _ in 20 | _ = view 21 | } 22 | 23 | XCTAssertNotNil(subview.layer.animation(forKey: "animatedProperty")!) 24 | XCTAssertNotNil(subview.layer.animation(forKey: "opacity")!) 25 | let animatedProperty = subview.layer.animation(forKey: "animatedProperty")! 26 | let opacity = subview.layer.animation(forKey: "opacity")! 27 | XCTAssert(animatedProperty.delegate === opacity.delegate) 28 | } 29 | } 30 | 31 | private class _UIPropertyAnimationView: UIView { 32 | var animatedProperty: CGFloat { 33 | get { return _layer.animatedProperty } 34 | set { _layer.animatedProperty = newValue } 35 | } 36 | 37 | override class var layerClass: AnyClass { 38 | return _UIPropertyAnimationLayer.self 39 | } 40 | 41 | var _layer: _UIPropertyAnimationLayer { return layer as! _UIPropertyAnimationLayer } 42 | 43 | init(animatedProperty: CGFloat) { 44 | super.init(frame: .zero) 45 | self.animatedProperty = animatedProperty 46 | } 47 | 48 | required init?(coder aDecoder: NSCoder) { 49 | super.init(coder: aDecoder) 50 | animatedProperty = 0 51 | } 52 | 53 | override func draw(_ rect: CGRect) { 54 | let ctx = UIGraphicsGetCurrentContext()! 55 | UIGraphicsPushContext(ctx) 56 | UIGraphicsPopContext() 57 | } 58 | 59 | override func action(for layer: CALayer, forKey event: String) -> CAAction? { 60 | switch event { 61 | case "animatedProperty": 62 | return UIAnimationAction(layer: layer, event: event, style: .inferred) 63 | default: 64 | return super.action(for: layer, forKey: event) 65 | } 66 | } 67 | } 68 | 69 | private class _UIPropertyAnimationLayer: CALayer { 70 | @NSManaged 71 | var animatedProperty: CGFloat 72 | 73 | override class func needsDisplay(forKey key: String) -> Bool { 74 | switch key { 75 | case "animatedProperty": return true 76 | default: return super.needsDisplay(forKey: key) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/AnimationsTimingControlSegments.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UIAnimationFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIAnimationFactory.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 03/04/2017. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | internal class _UIAnimationFactory< 12 | A: CAAnimation, P: _CAAnimationProtocol 13 | >: NSObject, _UIAnimationContextInternal where P.Animation == A 14 | { 15 | internal init(duration: TimeInterval, delay: TimeInterval) { 16 | self.duration = duration 17 | self.delay = delay 18 | } 19 | 20 | internal let duration: TimeInterval 21 | 22 | internal let delay: TimeInterval 23 | 24 | internal var animationTimings = [UIAnimationTiming]() 25 | 26 | internal var animationType: CAAnimation.Type { 27 | return Animation.self 28 | } 29 | 30 | /// Prepare the animation template. Call after the context was 31 | /// initialized and pushed to the the global context stack. 32 | /// 33 | /// - Notes: This is a dirty hack. UIKit initiates view-controller 34 | /// transition alongside animations when the original implementation 35 | /// of UIKit animation API was fired. But since we have to use an 36 | /// independent animation block to grab an animation template, the 37 | /// view-controller transition alongside animations would happen 38 | /// during generating the animation template. Thus we cannot generate 39 | /// the animation template in the initializer, else the 40 | /// view-controller transition alongside animations would not be able 41 | /// to grab the animation configure context. 42 | /// 43 | internal func prepare() { 44 | _ = animationTemplate 45 | } 46 | 47 | internal var animationTemplate: AnimationTemplate { 48 | fatalError("Abstract function") 49 | } 50 | 51 | private var _animationPrototype: AnimationPrototype? { 52 | return animationTemplate.animationPrototype 53 | } 54 | 55 | internal func action( 56 | for layer: CALayer, 57 | forKey event: String, 58 | style: UIAnimationActionStyle 59 | ) -> CAAction? 60 | { 61 | switch style { 62 | case .inferred: return inferredAction(for: layer, forKey: event) 63 | case .preset: return presetAction(for: layer, forKey: event) 64 | } 65 | } 66 | 67 | internal func inferredAction( 68 | for layer: CALayer, forKey event: String 69 | ) -> CAAction? 70 | { 71 | fatalError("You should not use this abstract class directly") 72 | } 73 | 74 | internal func presetAction( 75 | for layer: CALayer, forKey event: String 76 | ) -> CAAction? 77 | { 78 | fatalError("You should not use this abstract class directly") 79 | } 80 | 81 | internal func createAnimation() -> CAAnimation { return Animation() } 82 | 83 | @discardableResult 84 | internal func configureAnimation(_ animation: CAAnimation) -> Bool { 85 | if let appropriateAnimation = animation as? Animation, 86 | let animationPrototype = _animationPrototype 87 | { 88 | animationPrototype.apply(on:appropriateAnimation) 89 | return true 90 | } 91 | return false 92 | } 93 | 94 | internal typealias Animation = A 95 | internal typealias AnimationPrototype = P 96 | 97 | internal typealias AnimationTemplate = _UIAnimationTemplate 98 | } 99 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationFactory/_UIBasicAnimationFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _UIBasicAnimationFactory.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 03/04/2017. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | internal class _UIBasicAnimationFactory< 12 | A: CABasicAnimation, P: _CABasicAnimationProtocol 13 | >: _UIAnimationFactory where 14 | P.Animation == A 15 | { 16 | internal let options: UIView.AnimationOptions 17 | 18 | internal override func inferredAction( 19 | for layer: CALayer, forKey event: String 20 | ) -> CAAction? 21 | { 22 | if let viewLayerAction = presetAction(for: layer, forKey: event), 23 | !(viewLayerAction is NSNull) 24 | { 25 | currentAnimationTiming?._shiftAction(viewLayerAction) 26 | 27 | return viewLayerAction 28 | } else { 29 | let value = layer.value(forKeyPath: event) 30 | 31 | let pendingAnimation = Animation() 32 | 33 | if configureAnimation(pendingAnimation) { 34 | 35 | pendingAnimation.keyPath = event 36 | 37 | pendingAnimation.fromValue = value 38 | 39 | currentAnimationTiming?._shiftAction(pendingAnimation) 40 | 41 | // Layout Subviews 42 | if options.contains(.layoutSubviews) { 43 | layer.setNeedsLayout() 44 | layer.layoutIfNeeded() 45 | } 46 | 47 | // Begin from current state 48 | if options.contains(.beginFromCurrentState) { 49 | let presentationLayer = layer.presentation() ?? layer 50 | pendingAnimation.fromValue = presentationLayer.value( 51 | forKeyPath: event 52 | ) 53 | } else { 54 | pendingAnimation.fromValue = layer.value(forKeyPath: event) 55 | } 56 | 57 | // Returns the action 58 | return _UIAnimationFactoryAdditiveAction( 59 | layer: layer, 60 | event: event, 61 | pendingAnimation: pendingAnimation 62 | ) 63 | } 64 | 65 | return nil 66 | } 67 | } 68 | 69 | internal override func presetAction( 70 | for layer: CALayer, forKey event: String 71 | ) -> CAAction? 72 | { 73 | // Set the layer's delegate temporarily to `animationTemplate` 74 | CATransaction.disablesActions = true 75 | let originalLayerDelegate = layer.delegate 76 | layer.delegate = animationTemplate 77 | CATransaction.disablesActions = false 78 | 79 | defer { 80 | CATransaction.disablesActions = true 81 | layer.delegate = originalLayerDelegate 82 | CATransaction.disablesActions = false 83 | } 84 | 85 | if let action = _originalCALayerDelegateActionForLayerForKey( 86 | animationTemplate, 87 | #selector(CALayerDelegate.action(for:forKey:)), 88 | layer, 89 | event 90 | ) 91 | { 92 | currentAnimationTiming?._shiftAction(action) 93 | 94 | return action 95 | } 96 | 97 | return nil 98 | } 99 | 100 | internal init( 101 | duration: TimeInterval, 102 | delay: TimeInterval, 103 | options: UIView.AnimationOptions 104 | ) 105 | { 106 | self.options = options 107 | super.init(duration: duration, delay: delay) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/AnimationInterpolatorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationInterpolatorViewController.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | 10 | class AnimationInterpolatorViewController: UITableViewController { 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | self.blockAnimationProgressLabel.text = "0" 14 | self.springAnimationProgressLabel.text = "0" 15 | self.pseudoCodeTextView.textContainer.lineBreakMode = .byCharWrapping 16 | self.pseudoCodeTextView.text = """ 17 | UIView.animate(withDuration: duration) { 18 | UIView.addAnimationInterpolator { 19 | (progress) in 20 | self.label.text = ... 21 | } 22 | } 23 | """ 24 | } 25 | 26 | @IBOutlet weak var playButton: UIBarButtonItem! 27 | 28 | @IBAction func playButtonDidTap(_ sender: UIBarButtonItem) { 29 | sender.isEnabled = false 30 | UIView.animate(withDuration: 2.0, animations: { 31 | self.blockAnimationTralingConstraint.priority = .defaultHigh 32 | self.blockAnimationsContainerView.setNeedsUpdateConstraints() 33 | self.blockAnimationsContainerView.layoutIfNeeded() 34 | UIView.addAnimationInterpolator({ (progress) in 35 | self.blockAnimationProgressLabel.text = String(format: "%.2f", progress) 36 | }) 37 | }) { _ in 38 | UIView.animate(withDuration: 1.0, animations: { 39 | self.blockAnimationTralingConstraint.priority = .defaultLow 40 | self.blockAnimationsContainerView.setNeedsUpdateConstraints() 41 | self.blockAnimationsContainerView.layoutIfNeeded() 42 | UIView.addAnimationInterpolator({ (progress) in 43 | self.blockAnimationProgressLabel.text = String(format: "%.2f", progress) 44 | }) 45 | }, completion: { _ in 46 | self.blockAnimationProgressLabel.text = "0" 47 | }) 48 | } 49 | 50 | UIView.animate( 51 | withDuration: 2.0, 52 | delay: 0, 53 | usingSpringWithDamping: 0.5, 54 | initialSpringVelocity: 0.2, 55 | options: [], 56 | animations: { 57 | self.springAnimationTralingConstraint.priority = .defaultHigh 58 | self.springAnimationsContainerView.setNeedsUpdateConstraints() 59 | self.springAnimationsContainerView.layoutIfNeeded() 60 | UIView.addAnimationInterpolator({ (progress) in 61 | self.springAnimationProgressLabel.text = String(format: "%.2f", progress) 62 | }) 63 | }) { _ in 64 | sender.isEnabled = true 65 | UIView.animate(withDuration: 1.0, animations: { 66 | self.springAnimationTralingConstraint.priority = .defaultLow 67 | self.springAnimationsContainerView.setNeedsUpdateConstraints() 68 | self.springAnimationsContainerView.layoutIfNeeded() 69 | UIView.addAnimationInterpolator({ (progress) in 70 | self.springAnimationProgressLabel.text = String(format: "%.2f", progress) 71 | }) 72 | }, completion: { _ in 73 | self.springAnimationProgressLabel.text = "0" 74 | }) 75 | } 76 | } 77 | 78 | @IBOutlet weak var blockAnimationTralingConstraint: NSLayoutConstraint! 79 | @IBOutlet weak var blockAnimationsContainerView: UIView! 80 | @IBOutlet weak var blockAnimationProgressLabel: UILabel! 81 | 82 | @IBOutlet weak var springAnimationTralingConstraint: NSLayoutConstraint! 83 | @IBOutlet weak var springAnimationsContainerView: UIView! 84 | @IBOutlet weak var springAnimationProgressLabel: UILabel! 85 | 86 | @IBOutlet weak var pseudoCodeTextView: UITextView! 87 | } 88 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/AnimationsTimingControlSwitch.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/AnimationsTimingControlStepper.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /UIAnimationToolboxTests/UIAnimationActionInferredTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAnimationActionInferredTests.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 2019/3/13. 6 | // 7 | 8 | import XCTest 9 | import UIAnimationToolbox 10 | 11 | class UIAnimationActionInferredTests: XCTestCase { 12 | // MARK: - Init with Layer Event 13 | func testInitWithLayerEvent_returnsNil_whenCalledOutsideAnimationBlock() { 14 | let layer = CALayer() 15 | let action = UIAnimationActionInferred(layer: layer, event: "bounds.size") 16 | XCTAssertNil(action) 17 | } 18 | 19 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation5() { 20 | let layer = CALayer() 21 | var action: CAAction! 22 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: { 23 | action = UIAnimationActionInferred(layer: layer, event: "bounds.size") 24 | }, completion: nil) 25 | XCTAssertNotNil(action) 26 | } 27 | 28 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation3() { 29 | let layer = CALayer() 30 | var action: CAAction! 31 | UIView.animate(withDuration: 0.3, animations: { 32 | action = UIAnimationActionInferred(layer: layer, event: "bounds.size") 33 | }, completion: nil) 34 | XCTAssertNotNil(action) 35 | } 36 | 37 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation2() { 38 | let layer = CALayer() 39 | var action: CAAction! 40 | UIView.animate(withDuration: 0.3, animations: { 41 | action = UIAnimationActionInferred(layer: layer, event: "bounds.size") 42 | }) 43 | XCTAssertNotNil(action) 44 | } 45 | 46 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfSpringAnimation() { 47 | let layer = CALayer() 48 | var action: CAAction! 49 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: [], animations: { 50 | action = UIAnimationActionInferred(layer: layer, event: "bounds.size") 51 | }, completion: nil) 52 | XCTAssertNotNil(action) 53 | } 54 | 55 | // MARK: - Make with Layer Event 56 | func testMakeLayerEvent_returnsInstanceOfNSNull_whenCalledOutsideAnimationBlock() { 57 | let layer = CALayer() 58 | let action = UIAnimationActionInferred.make(layer: layer, event: "bounds.size") 59 | XCTAssertTrue(action is NSNull) 60 | } 61 | 62 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation5() { 63 | let layer = CALayer() 64 | var action: AnyObject! 65 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: { 66 | action = UIAnimationActionInferred.make(layer: layer, event: "bounds.size") 67 | }) 68 | XCTAssertTrue(action is CAAction) 69 | } 70 | 71 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation3() { 72 | let layer = CALayer() 73 | var action: AnyObject! 74 | UIView.animate(withDuration: 0.3, animations: { 75 | action = UIAnimationActionInferred.make(layer: layer, event: "bounds.size") 76 | }, completion: nil) 77 | XCTAssertTrue(action is CAAction) 78 | } 79 | 80 | 81 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation2() { 82 | let layer = CALayer() 83 | var action: AnyObject! 84 | UIView.animate(withDuration: 0.3, animations: { 85 | action = UIAnimationActionInferred.make(layer: layer, event: "bounds.size") 86 | }) 87 | XCTAssertTrue(action is CAAction) 88 | } 89 | 90 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfSpringAnimation() { 91 | let layer = CALayer() 92 | var action: AnyObject! 93 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: [], animations: { 94 | action = UIAnimationActionInferred.make(layer: layer, event: "bounds.size") 95 | }, completion: nil) 96 | XCTAssertTrue(action is CAAction) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /UIAnimationToolboxTests/UIAnimationActionPresetTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAnimationActionPresetTests.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 2019/3/13. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable 11 | import UIAnimationToolbox 12 | 13 | class UIAnimationActionPresetTests: XCTestCase { 14 | // MARK: - Init with Layer Event 15 | func testInitWithLayerEvent_returnsNil_whenCalledOutsideAnimationBlock() { 16 | let layer = CALayer() 17 | let action = UIAnimationActionPreset(layer: layer, event: "bounds.size") 18 | XCTAssertNil(action) 19 | } 20 | 21 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation5() { 22 | let layer = CALayer() 23 | var action: CAAction! 24 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: { 25 | action = UIAnimationActionPreset(layer: layer, event: "bounds.size") 26 | }, completion: nil) 27 | XCTAssertNotNil(action) 28 | } 29 | 30 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation3() { 31 | let layer = CALayer() 32 | var action: CAAction! 33 | UIView.animate(withDuration: 0.3, animations: { 34 | action = UIAnimationActionPreset(layer: layer, event: "bounds.size") 35 | }, completion: nil) 36 | XCTAssertNotNil(action) 37 | } 38 | 39 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation2() { 40 | let layer = CALayer() 41 | var action: CAAction! 42 | UIView.animate(withDuration: 0.3, animations: { 43 | action = UIAnimationActionPreset(layer: layer, event: "bounds.size") 44 | }) 45 | XCTAssertNotNil(action) 46 | } 47 | 48 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfSpringAnimation() { 49 | let layer = CALayer() 50 | var action: CAAction! 51 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: [], animations: { 52 | action = UIAnimationActionPreset(layer: layer, event: "bounds.size") 53 | }, completion: nil) 54 | XCTAssertNotNil(action) 55 | } 56 | 57 | // MARK: - Make with Layer Event 58 | func testMakeLayerEvent_returnsInstanceOfNSNull_whenCalledOutsideAnimationBlock() { 59 | let layer = CALayer() 60 | let action = UIAnimationActionPreset.make(layer: layer, event: "bounds.size") 61 | XCTAssertTrue(action is NSNull) 62 | } 63 | 64 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation5() { 65 | let layer = CALayer() 66 | var action: AnyObject! 67 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: { 68 | action = UIAnimationActionPreset.make(layer: layer, event: "bounds.size") 69 | }) 70 | XCTAssertTrue(action is CAAction) 71 | } 72 | 73 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation3() { 74 | let layer = CALayer() 75 | var action: AnyObject! 76 | UIView.animate(withDuration: 0.3, animations: { 77 | action = UIAnimationActionPreset.make(layer: layer, event: "bounds.size") 78 | }, completion: nil) 79 | XCTAssertTrue(action is CAAction) 80 | } 81 | 82 | 83 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation2() { 84 | let layer = CALayer() 85 | var action: AnyObject! 86 | UIView.animate(withDuration: 0.3, animations: { 87 | action = UIAnimationActionPreset.make(layer: layer, event: "bounds.size") 88 | }) 89 | XCTAssertTrue(action is CAAction) 90 | } 91 | 92 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfSpringAnimation() { 93 | let layer = CALayer() 94 | var action: AnyObject! 95 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: [], animations: { 96 | action = UIAnimationActionPreset.make(layer: layer, event: "bounds.size") 97 | }, completion: nil) 98 | XCTAssertTrue(action is CAAction) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,vim,swift,objective-c 3 | # Edit at https://www.gitignore.io/?templates=macos,vim,swift,objective-c 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Objective-C ### 34 | # Xcode 35 | # 36 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 37 | 38 | ## Build generated 39 | build/ 40 | DerivedData/ 41 | 42 | ## Various settings 43 | *.pbxuser 44 | !default.pbxuser 45 | *.mode1v3 46 | !default.mode1v3 47 | *.mode2v3 48 | !default.mode2v3 49 | *.perspectivev3 50 | !default.perspectivev3 51 | xcuserdata/ 52 | 53 | ## Other 54 | *.moved-aside 55 | *.xccheckout 56 | *.xcscmblueprint 57 | 58 | ## Obj-C/Swift specific 59 | *.hmap 60 | *.ipa 61 | *.dSYM.zip 62 | *.dSYM 63 | 64 | # CocoaPods 65 | # We recommend against adding the Pods directory to your .gitignore. However 66 | # you should judge for yourself, the pros and cons are mentioned at: 67 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 68 | # Pods/ 69 | # Add this line if you want to avoid checking in source code from the Xcode workspace 70 | # *.xcworkspace 71 | 72 | # Carthage 73 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 74 | # Carthage/Checkouts 75 | 76 | Carthage/Build 77 | 78 | # fastlane 79 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 80 | # screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots/**/*.png 87 | fastlane/test_output 88 | 89 | # Code Injection 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | 95 | ### Objective-C Patch ### 96 | 97 | ### Swift ### 98 | # Xcode 99 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 100 | 101 | 102 | 103 | 104 | 105 | ## Playgrounds 106 | timeline.xctimeline 107 | playground.xcworkspace 108 | 109 | # Swift Package Manager 110 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 111 | # Packages/ 112 | # Package.pins 113 | # Package.resolved 114 | .build/ 115 | 116 | # CocoaPods 117 | # We recommend against adding the Pods directory to your .gitignore. However 118 | # you should judge for yourself, the pros and cons are mentioned at: 119 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 120 | # Pods/ 121 | # Add this line if you want to avoid checking in source code from the Xcode workspace 122 | # *.xcworkspace 123 | 124 | # Carthage 125 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 126 | # Carthage/Checkouts 127 | 128 | 129 | # fastlane 130 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 131 | # screenshots whenever they are needed. 132 | # For more information about the recommended setup visit: 133 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 134 | 135 | 136 | # Code Injection 137 | # After new code Injection tools there's a generated folder /iOSInjectionProject 138 | # https://github.com/johnno1962/injectionforxcode 139 | 140 | 141 | ### Vim ### 142 | # Swap 143 | [._]*.s[a-v][a-z] 144 | [._]*.sw[a-p] 145 | [._]s[a-rt-v][a-z] 146 | [._]ss[a-gi-z] 147 | [._]sw[a-p] 148 | 149 | # Session 150 | Session.vim 151 | 152 | # Temporary 153 | .netrwhist 154 | *~ 155 | # Auto-generated tag files 156 | tags 157 | # Persistent undo 158 | [._]*.un~ 159 | 160 | # End of https://www.gitignore.io/api/macos,vim,swift,objective-c 161 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/CustomPropertiesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPropertiesViewController.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | import UIAnimationToolbox 10 | 11 | class CustomPropertiesViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | } 15 | 16 | @IBOutlet weak var clockView: ClockView! 17 | } 18 | 19 | 20 | extension CustomPropertiesViewController: UIPickerViewDelegate { 21 | func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 22 | UIView.animate(withDuration: 5.0, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: [], animations: { 23 | switch component { 24 | case 0: self.clockView.hour = CGFloat(row) 25 | case 1: self.clockView.minute = CGFloat(row) 26 | default: break 27 | } 28 | }) 29 | } 30 | } 31 | 32 | 33 | extension CustomPropertiesViewController: UIPickerViewDataSource { 34 | func numberOfComponents(in pickerView: UIPickerView) -> Int { 35 | return 2 36 | } 37 | 38 | func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 39 | switch component { 40 | case 0: return 24 41 | case 1: return 60 42 | default: return 0 43 | } 44 | } 45 | 46 | func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { 47 | switch component { 48 | case 0: return "\(row)" 49 | case 1: return "\(row)" 50 | default: return nil 51 | } 52 | } 53 | } 54 | 55 | 56 | // MARK: - ClockView 57 | class ClockView: UIView { 58 | var hour: CGFloat { 59 | get { return _layer.hour } 60 | set { _layer.hour = newValue } 61 | } 62 | 63 | var minute: CGFloat { 64 | get { return _layer.minute } 65 | set { _layer.minute = newValue } 66 | } 67 | 68 | override class var layerClass: AnyClass { 69 | return ClockLayer.self 70 | } 71 | 72 | var _layer: ClockLayer { return layer as! ClockLayer } 73 | 74 | init(hour: CGFloat, minute: CGFloat, second: Float) { 75 | super.init(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 76 | self.hour = hour 77 | self.minute = minute 78 | } 79 | 80 | required init?(coder aDecoder: NSCoder) { 81 | super.init(coder: aDecoder) 82 | hour = 0 83 | minute = 0 84 | } 85 | 86 | override func action(for layer: CALayer, forKey event: String) -> CAAction? { 87 | switch event { 88 | case "hour", "minute": 89 | return UIAnimationActionInferred(layer: layer, event: event) 90 | default: 91 | return super.action(for: layer, forKey: event) 92 | } 93 | } 94 | 95 | override func draw(_ rect: CGRect) { 96 | let ctx = UIGraphicsGetCurrentContext()! 97 | UIGraphicsPushContext(ctx) 98 | 99 | UIColor.black.setFill() 100 | UIColor.clear.setStroke() 101 | 102 | let presentationLayer = _layer.presentation() ?? _layer 103 | 104 | let translate = CGAffineTransform(translationX: 150, y: 150) 105 | 106 | // draw hour 107 | let hourPath = UIBezierPath(rect: CGRect(x: -1, y: -9, width: 4, height: 70)) 108 | let hourRotation = CGAffineTransform(rotationAngle: presentationLayer.hour / 12.0 * 2.0 * .pi - .pi) 109 | let hourTransform = hourRotation.concatenating(translate) 110 | hourPath.apply(hourTransform) 111 | hourPath.fill() 112 | 113 | // draw minute 114 | let minutePath = UIBezierPath(rect: CGRect(x: 0, y: -4, width: 2, height: 90)) 115 | let minuteRotation = CGAffineTransform(rotationAngle: presentationLayer.minute / 60.0 * 2.0 * .pi - .pi) 116 | let minuteTransform = minuteRotation.concatenating(translate) 117 | minutePath.apply(minuteTransform) 118 | minutePath.fill() 119 | 120 | UIGraphicsPopContext() 121 | } 122 | } 123 | 124 | // MARK: - ClockLayer 125 | class ClockLayer: CALayer { 126 | @NSManaged 127 | var hour: CGFloat 128 | 129 | @NSManaged 130 | var minute: CGFloat 131 | 132 | override class func needsDisplay(forKey key: String) -> Bool { 133 | switch key { 134 | case "hour", "minute": return true 135 | default: return super.needsDisplay(forKey: key) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /UIAnimationToolbox/QuartzCore/ToolboxAdditions/CALayer+ToolboxAdditions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+ToolboxAdditions.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 1/2/16. 6 | // 7 | // 8 | 9 | import QuartzCore 10 | import ObjectiveC 11 | 12 | extension CALayer { 13 | public func objectiveCEncoding(forKey key: String) -> String? { 14 | return type(of: self).objectiveCEncoding(forKey: key) 15 | } 16 | 17 | public static func objectiveCEncoding(forKey key: String) -> String? { 18 | guard let property = class_getProperty(self, key) else { 19 | return nil 20 | } 21 | 22 | guard let type = property_copyAttributeValue(property, "T") else { 23 | return nil 24 | } 25 | 26 | guard let nsString = NSString(utf8String: type) else { 27 | return nil 28 | } 29 | 30 | type.deallocate() 31 | 32 | return nsString as String 33 | } 34 | } 35 | 36 | extension CALayer { 37 | @objc(UIATCAPropertyAnimationAdditivePolicy) 38 | public enum AdditivePolicy: Int, CustomStringConvertible, 39 | CustomDebugStringConvertible 40 | { 41 | @objc(UIATCAPropertyAnimationAdditivePolicyNone) 42 | case none 43 | @objc(UIATCAPropertyAnimationAdditivePolicyCGRect) 44 | case cgRect 45 | @objc(UIATCAPropertyAnimationAdditivePolicyCGSize) 46 | case cgSize 47 | @objc(UIATCAPropertyAnimationAdditivePolicyCGPoint) 48 | case cgPoint 49 | @objc(UIATCAPropertyAnimationAdditivePolicyCGVector) 50 | case cgVector 51 | @objc(UIATCAPropertyAnimationAdditivePolicyCATransform3D) 52 | case caTransform3D 53 | @objc(UIATCAPropertyAnimationAdditivePolicyFloat) 54 | case float 55 | @objc(UIATCAPropertyAnimationAdditivePolicyDouble) 56 | case double 57 | 58 | public var description: String { 59 | switch self { 60 | case .none: return "None" 61 | case .cgRect: return "CGRect" 62 | case .cgSize: return "CGSize" 63 | case .cgPoint: return "CGPoint" 64 | case .cgVector: return "CGVector" 65 | case .caTransform3D: return "CATransform3D" 66 | case .float: return "Float" 67 | case .double: return "Double" 68 | } 69 | } 70 | 71 | public var debugDescription: String { 72 | return "<\(type(of: self)); \(description)>" 73 | } 74 | } 75 | 76 | @objc(uiat_additivePolicyForKey:) 77 | open class func additivePolicy(forKey event: String) 78 | -> AdditivePolicy 79 | { 80 | switch event { 81 | case "opacity", "shadowOpacity": 82 | // `opacity` cannot be additive, perhaps because it's clamped. 83 | // `shadowOpacity` is also clamped. 84 | return .none 85 | default: 86 | guard let encoding = objectiveCEncoding(forKey: event) else { 87 | return .none 88 | } 89 | 90 | return additivePolicy(forEncoding: encoding) 91 | } 92 | } 93 | 94 | @objc(uiat_additivePolicyForEncoding:) 95 | open class func additivePolicy(forEncoding encoding: String) 96 | -> AdditivePolicy 97 | { 98 | // Don't be scared. Just a manually built automaton. 99 | 100 | if encoding.hasPrefix("{") { 101 | 102 | let dequeued1 = encoding.dropFirst() 103 | if dequeued1.hasPrefix("C") { 104 | let dequeued2 = dequeued1.dropFirst() 105 | if dequeued2.hasPrefix("G") { 106 | let dequeued3 = dequeued2.dropFirst() 107 | if dequeued3.hasPrefix("Rect=") { 108 | return .cgRect 109 | } 110 | if dequeued3.hasPrefix("Size=") { 111 | return .cgSize 112 | } 113 | if dequeued3.hasPrefix("Point=") { 114 | return .cgPoint 115 | } 116 | if dequeued3.hasPrefix("Vector=") { 117 | return .cgVector 118 | } 119 | } 120 | if dequeued2.hasPrefix("ATransform3D=") { 121 | return .caTransform3D 122 | } 123 | } 124 | } 125 | 126 | if encoding == "f" { return .float } 127 | 128 | if encoding == "d" { return .double } 129 | 130 | return .none 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /UIAnimationToolboxTests/UIAnimationActionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAnimationActionTests.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/16. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable 11 | import UIAnimationToolbox 12 | 13 | class UIAnimationActionTests: XCTestCase { 14 | // MARK: - Init with Action and Style 15 | func testInitWithActionStyle() { 16 | let internalAction = NSNull() 17 | let style = UIAnimationActionStyle.inferred 18 | let action = UIAnimationAction(action: internalAction, style: .inferred) 19 | XCTAssert(action._action === internalAction) 20 | XCTAssert(action._style == style) 21 | } 22 | 23 | // MARK: - Init with Layer and Event 24 | func testInitWithLayerEvent_returnsNil_whenCalledOutsideAnimationBlock() { 25 | let layer = CALayer() 26 | let action = UIAnimationAction(layer: layer, event: "bounds.size", style: .preset) 27 | XCTAssertNil(action) 28 | } 29 | 30 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation5() { 31 | let layer = CALayer() 32 | var action: CAAction! 33 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: { 34 | action = UIAnimationAction(layer: layer, event: "bounds.size", style: .preset) 35 | }, completion: nil) 36 | XCTAssertNotNil(action) 37 | } 38 | 39 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation3() { 40 | let layer = CALayer() 41 | var action: CAAction! 42 | UIView.animate(withDuration: 0.3, animations: { 43 | action = UIAnimationAction(layer: layer, event: "bounds.size", style: .preset) 44 | }, completion: nil) 45 | XCTAssertNotNil(action) 46 | } 47 | 48 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfBlockAnimation2() { 49 | let layer = CALayer() 50 | var action: CAAction! 51 | UIView.animate(withDuration: 0.3, animations: { 52 | action = UIAnimationAction(layer: layer, event: "bounds.size", style: .preset) 53 | }) 54 | XCTAssertNotNil(action) 55 | } 56 | 57 | func testInitWithLayerEvent_returnsNonNil_whenCalledInsideBlockOfSpringAnimation() { 58 | let layer = CALayer() 59 | var action: CAAction! 60 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: [], animations: { 61 | action = UIAnimationAction(layer: layer, event: "bounds.size", style: .preset) 62 | }, completion: nil) 63 | XCTAssertNotNil(action) 64 | } 65 | 66 | // MARK: - Make with Layer Event 67 | func testMakeLayerEvent_returnsInstanceOfNSNull_whenCalledOutsideAnimationBlock() { 68 | let layer = CALayer() 69 | let action = UIAnimationAction.make(layer: layer, event: "bounds.size", style: .preset) 70 | XCTAssertTrue(action is NSNull) 71 | } 72 | 73 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation5() { 74 | let layer = CALayer() 75 | var action: AnyObject! 76 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: { 77 | action = UIAnimationAction.make(layer: layer, event: "bounds.size", style: .preset) 78 | }) 79 | XCTAssertTrue(action is CAAction) 80 | } 81 | 82 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation3() { 83 | let layer = CALayer() 84 | var action: AnyObject! 85 | UIView.animate(withDuration: 0.3, animations: { 86 | action = UIAnimationAction.make(layer: layer, event: "bounds.size", style: .preset) 87 | }, completion: nil) 88 | XCTAssertTrue(action is CAAction) 89 | } 90 | 91 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfBlockAnimation2() { 92 | let layer = CALayer() 93 | var action: AnyObject! 94 | UIView.animate(withDuration: 0.3, animations: { 95 | action = UIAnimationAction.make(layer: layer, event: "bounds.size", style: .preset) 96 | }) 97 | XCTAssertTrue(action is CAAction) 98 | } 99 | 100 | func testMakeLayerEvent_returnsInstanceOfCAAction_whenCalledInsideBlockOfSpringAnimation() { 101 | let layer = CALayer() 102 | var action: AnyObject! 103 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: [], animations: { 104 | action = UIAnimationAction.make(layer: layer, event: "bounds.size", style: .preset) 105 | }, completion: nil) 106 | XCTAssertTrue(action is CAAction) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /UIAnimationToolbox/QuartzCore/ToolboxAdditions/CAMediaTimingFunction+ToolboxAdditions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CAMediaTimingFunction+ToolboxAdditions.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 11/20/15. 6 | // 7 | // 8 | 9 | import QuartzCore 10 | 11 | public func == (lhs: CAMediaTimingFunction, rhs: CAMediaTimingFunction) -> Bool { 12 | return lhs.controlPoints == rhs.controlPoints 13 | } 14 | 15 | // MARK: Getting Control Points 16 | extension CAMediaTimingFunction { 17 | public subscript(index: UInt) -> CGPoint { 18 | switch index { 19 | case 0...3: 20 | let controlPoint = UnsafeMutablePointer.allocate(capacity: 2) 21 | getControlPoint(at: Int(index), values: controlPoint) 22 | let x: Float = controlPoint[0] 23 | let y: Float = controlPoint[1] 24 | controlPoint.deallocate() 25 | return CGPoint(x: CGFloat(x), y: CGFloat(y)) 26 | default: 27 | fatalError("Index\(index) is beyond the boundary, shall be 0...3") 28 | } 29 | } 30 | 31 | public var controlPoints: [CGPoint] { 32 | return (0...3).map({self[$0]}) 33 | } 34 | } 35 | 36 | // MARK: Evaluate Y 37 | extension CAMediaTimingFunction { 38 | private typealias _CGFloat4 = (CGFloat, CGFloat, CGFloat, CGFloat) 39 | 40 | @objc(uiat_evaluateYForX:) 41 | public func evaluateY(forX x: CGFloat) -> CGFloat { 42 | precondition( 43 | MemoryLayout<_CGFloat4>.size == 4 * MemoryLayout.size 44 | ) 45 | 46 | let cp1 = self[1] 47 | let cp2 = self[2] 48 | 49 | let c1x = cp1.x 50 | let c1y = cp1.y 51 | let c2x = cp2.x 52 | let c2y = cp2.y 53 | 54 | var coefficientsX: _CGFloat4 = ( 55 | _c0x, // t^0 56 | -3.0*_c0x + 3.0*c1x, // t^1 57 | 3.0*_c0x - 6.0*c1x + 3.0*c2x, // t^2 58 | -_c0x + 3.0*c1x - 3.0*c2x + _c3x // t^3 59 | ) 60 | 61 | var coefficientsY: _CGFloat4 = ( 62 | _c0y, // t^0 63 | -3.0*_c0y + 3.0*c1y, // t^1 64 | 3.0*_c0y - 6.0*c1y + 3.0*c2y, // t^2 65 | -_c0y + 3.0*c1y - 3.0*c2y + _c3y // t^3 66 | ) 67 | 68 | let coefficientsXPtr = withUnsafePointer(to: &coefficientsX, {$0}) 69 | .withMemoryRebound(to: CGFloat.self, capacity: 4, {$0}) 70 | let coefficientsYPtr = withUnsafePointer(to: &coefficientsY, {$0}) 71 | .withMemoryRebound(to: CGFloat.self, capacity: 4, {$0}) 72 | 73 | let t = _calculateParameter(forX: x, coefficients: coefficientsXPtr) 74 | let y = _evaluate(at: t, coefficients: coefficientsYPtr) 75 | 76 | return y 77 | } 78 | 79 | @inline(__always) 80 | private func _evaluate( 81 | at t: CGFloat , coefficients: UnsafePointer 82 | ) -> CGFloat 83 | { 84 | return coefficients[0] 85 | + (t * coefficients[1]) 86 | + (t * t * coefficients[2]) 87 | + (t * t * t * coefficients[3]) 88 | } 89 | 90 | @inline(__always) 91 | private func _evaluateDerivation( 92 | at t: CGFloat, coefficients: UnsafePointer 93 | ) -> CGFloat 94 | { 95 | return coefficients[1] 96 | + (2 * t * coefficients[2]) 97 | + (3 * t * t * coefficients[3]) 98 | } 99 | 100 | @inline(__always) 101 | private func _calculateParameterViaNewtonRaphson( 102 | forX x: CGFloat, coefficients: UnsafePointer 103 | ) -> CGFloat 104 | { 105 | // see http://en.wikipedia.org/wiki/Newton's_method 106 | 107 | // start with X being the correct value 108 | var t: CGFloat = x 109 | 110 | // iterate several times 111 | for _ in 0..<10 { 112 | 113 | let x2 = _evaluate(at: t, coefficients: coefficients) - x 114 | let d = _evaluateDerivation(at: t, coefficients: coefficients) 115 | 116 | let dt = x2/d 117 | 118 | t = t - dt 119 | } 120 | 121 | return t 122 | } 123 | 124 | @inline(__always) 125 | private func _calculateParameter( 126 | forX x: CGFloat, coefficients: UnsafePointer 127 | ) -> CGFloat 128 | { 129 | // for the time being, we'll guess Newton-Raphson always 130 | // returns the correct value. 131 | 132 | // if we find it doesn't find the solution often enough, 133 | // we can add additional calculation methods. 134 | 135 | let t = _calculateParameterViaNewtonRaphson( 136 | forX: x, coefficients: coefficients 137 | ) 138 | 139 | return t 140 | } 141 | } 142 | 143 | private let _c0x: CGFloat = 0.0 144 | private let _c0y: CGFloat = 0.0 145 | private let _c3x: CGFloat = 1.0 146 | private let _c3y: CGFloat = 1.0 147 | -------------------------------------------------------------------------------- /UIAnimationToolbox.xcodeproj/xcshareddata/xcschemes/UIAnimationToolbox.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 86 | 87 | 93 | 94 | 95 | 96 | 102 | 103 | 109 | 110 | 111 | 112 | 114 | 115 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/AnimationsTimingAnimationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationsTimingAnimationView.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | import UIAnimationToolbox 10 | 11 | protocol AnimationsTimingAnimationViewDataSource: NSObjectProtocol { 12 | func animationViewConfiguration(_ sender: AnimationsTimingAnimationView) -> AnimationsTimingConfiguration 13 | func animationViewAnimationDuration(_ sender: AnimationsTimingAnimationView) -> TimeInterval 14 | func animationViewBeginTime(_ sender: AnimationsTimingAnimationView) -> TimeInterval 15 | func animationViewDuration(_ sender: AnimationsTimingAnimationView) -> TimeInterval 16 | func animationViewSpeed(_ sender: AnimationsTimingAnimationView) -> CGFloat 17 | func animationViewTimeOffset(_ sender: AnimationsTimingAnimationView) -> TimeInterval 18 | func animationViewRepeating(_ sender: AnimationsTimingAnimationView) -> CGFloat 19 | func animationViewAutoreverses(_ sender: AnimationsTimingAnimationView) -> Bool 20 | func animationViewFillMode(_ sender: AnimationsTimingAnimationView) -> UIAnimationTimingFillMode 21 | } 22 | 23 | protocol AnimationsTimingAnimationViewDelegate: NSObjectProtocol { 24 | func animationsTimingAnimationViewDidEndAnimation(_ sender: AnimationsTimingAnimationView) 25 | } 26 | 27 | class AnimationsTimingAnimationView: UIView { 28 | weak var dataSource: AnimationsTimingAnimationViewDataSource! 29 | weak var delegate: AnimationsTimingAnimationViewDelegate! 30 | 31 | @IBOutlet weak var normalTimingIndicator: UIView! 32 | 33 | @IBOutlet weak var normalTimingTralingConstraint: NSLayoutConstraint! 34 | 35 | @IBOutlet weak var shiftedTimingIndicator: ShiftedTimingIndicator! 36 | 37 | @IBOutlet weak var shiftedTimingLeadingConstraint: NSLayoutConstraint! 38 | 39 | @IBOutlet weak var shiftedTimingTralingConstraint: NSLayoutConstraint! 40 | 41 | func play() { 42 | _animate( 43 | { 44 | self.normalTimingTralingConstraint.priority = .defaultHigh 45 | self.setNeedsUpdateConstraints() 46 | self.layoutIfNeeded() 47 | }, 48 | { 49 | self.shiftedTimingTralingConstraint.priority = .defaultHigh 50 | self.shiftedTimingLeadingConstraint.priority = .defaultLow 51 | self.setNeedsUpdateConstraints() 52 | self.layoutIfNeeded() 53 | }, 54 | { _ in 55 | UIView.animate(withDuration: 1.0, animations: { 56 | self.normalTimingTralingConstraint.priority = .defaultLow 57 | self.shiftedTimingTralingConstraint.priority = .defaultLow 58 | self.shiftedTimingLeadingConstraint.priority = .defaultHigh 59 | self.setNeedsUpdateConstraints() 60 | self.layoutIfNeeded() 61 | }, completion: { _ in 62 | self.delegate?.animationsTimingAnimationViewDidEndAnimation(self) 63 | }) 64 | } 65 | ) 66 | } 67 | 68 | func _animate( 69 | _ animations: @escaping () -> Void, 70 | _ timingShiftedAnimations: @escaping () -> Void, 71 | _ completion: @escaping (Bool) -> Void 72 | ) 73 | { 74 | guard let dataSource = dataSource else { return } 75 | 76 | UIView.animate(withDuration: dataSource.animationViewAnimationDuration(self), animations: { 77 | animations() 78 | switch dataSource.animationViewConfiguration(self) { 79 | case .beginTime: 80 | UIView.shiftAnimationsTiming( 81 | beginTime: dataSource.animationViewBeginTime(self), 82 | animations: timingShiftedAnimations 83 | ) 84 | case .duration: 85 | UIView.shiftAnimationsTiming( 86 | duration: dataSource.animationViewDuration(self), 87 | animations: timingShiftedAnimations 88 | ) 89 | case .speed: 90 | UIView.shiftAnimationsTiming( 91 | speed: dataSource.animationViewSpeed(self), 92 | animations: timingShiftedAnimations 93 | ) 94 | case .timeOffset: 95 | UIView.shiftAnimationsTiming( 96 | timeOffset: dataSource.animationViewTimeOffset(self), 97 | animations: timingShiftedAnimations 98 | ) 99 | case .repeatingByCount: 100 | UIView.shiftAnimationsTiming( 101 | repeating: .byCount(dataSource.animationViewRepeating(self)), 102 | animations: timingShiftedAnimations 103 | ) 104 | case .repeatingByDuration: 105 | UIView.shiftAnimationsTiming( 106 | repeating: .byDuration(TimeInterval(dataSource.animationViewRepeating(self))), 107 | animations: timingShiftedAnimations 108 | ) 109 | case .autoreverses: 110 | UIView.shiftAnimationsTiming( 111 | autoreverses: dataSource.animationViewAutoreverses(self), 112 | animations: timingShiftedAnimations 113 | ) 114 | case .fillMode: 115 | UIView.shiftAnimationsTiming( 116 | fillMode: dataSource.animationViewFillMode(self), 117 | animations: timingShiftedAnimations 118 | ) 119 | } 120 | }, completion: completion) 121 | } 122 | } 123 | 124 | class ShiftedTimingIndicator: UIView { 125 | override func prepareForInterfaceBuilder() { 126 | layer.cornerRadius = 15 127 | } 128 | 129 | override func awakeFromNib() { 130 | layer.cornerRadius = 15 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/AnimationsTimingControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationsTimingControl.swift 3 | // UIAnimationToolboxCategory-iOS 4 | // 5 | // Created by WeZZard on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol AnimationsTimingControlDelegate: NSObjectProtocol { 11 | 12 | } 13 | 14 | class AnimationsTimingControl: UIView { 15 | weak var delegate: AnimationsTimingControlDelegate? 16 | 17 | static func make( 18 | configuration: AnimationsTimingConfiguration, 19 | controller: AnimationsTimingViewController 20 | ) -> AnimationsTimingControl 21 | { 22 | switch configuration { 23 | case .beginTime: 24 | let view: AnimationsTimingControlStepper = .makeFromNib() 25 | view.stepper.minimumValue = 0 26 | view.stepper.maximumValue = controller.animationDuration 27 | view.stepper.stepValue = 0.1 28 | view.stepper.value = controller.beginTime 29 | view.titleLabel.text = "\(controller.beginTime)" 30 | return view 31 | case .duration: 32 | let view: AnimationsTimingControlStepper = .makeFromNib() 33 | view.stepper.minimumValue = 0 34 | view.stepper.maximumValue = controller.animationDuration 35 | view.stepper.stepValue = 0.1 36 | view.stepper.value = controller.duration 37 | view.titleLabel.text = "\(controller.duration)" 38 | return view 39 | case .speed: 40 | let view: AnimationsTimingControlStepper = .makeFromNib() 41 | view.stepper.minimumValue = 0 42 | view.stepper.maximumValue = 4 43 | view.stepper.stepValue = 0.5 44 | view.stepper.value = Double(controller.speed) 45 | view.titleLabel.text = "\(controller.speed)" 46 | return view 47 | case .timeOffset: 48 | let view: AnimationsTimingControlStepper = .makeFromNib() 49 | view.stepper.minimumValue = 0 50 | view.stepper.maximumValue = controller.animationDuration 51 | view.stepper.stepValue = 0.1 52 | view.stepper.value = controller.timeOffset 53 | view.titleLabel.text = configuration.rawValue 54 | view.titleLabel.text = "\(controller.timeOffset)" 55 | return view 56 | case .repeatingByCount: 57 | let view: AnimationsTimingControlStepper = .makeFromNib() 58 | view.stepper.minimumValue = 0 59 | view.stepper.maximumValue = 4 60 | view.stepper.stepValue = 0.5 61 | view.stepper.value = Double(controller.repeating) 62 | view.titleLabel.text = configuration.rawValue 63 | view.titleLabel.text = "\(controller.repeating)" 64 | return view 65 | case .repeatingByDuration: 66 | let view: AnimationsTimingControlStepper = .makeFromNib() 67 | view.stepper.minimumValue = 0 68 | view.stepper.maximumValue = 4 69 | view.stepper.stepValue = 0.5 70 | view.stepper.value = Double(controller.repeating) 71 | view.titleLabel.text = "\(controller.repeating)" 72 | return view 73 | case .autoreverses: 74 | let view: AnimationsTimingControlSwitch = .makeFromNib() 75 | view.switch.isOn = controller.autoreverses 76 | view.titleLabel.text = controller.autoreverses ? "Enabled" : "Disabled" 77 | return view 78 | case .fillMode: 79 | let view: AnimationsTimingControlSegments = .makeFromNib() 80 | view.segmentedControl.removeAllSegments() 81 | view.segmentedControl.insertSegment(withTitle: "Removed", at: 0, animated: false) 82 | view.segmentedControl.insertSegment(withTitle: "Backwards", at: 1, animated: false) 83 | view.segmentedControl.insertSegment(withTitle: "Forwards", at: 2, animated: false) 84 | view.segmentedControl.insertSegment(withTitle: "Both", at: 3, animated: false) 85 | view.segmentedControl.selectedSegmentIndex = controller.indexForFillMode(controller.fillMode) 86 | return view 87 | } 88 | } 89 | } 90 | 91 | protocol AnimationsTimingControlSegmentsDelegate: 92 | AnimationsTimingControlDelegate 93 | { 94 | func animationsTimingControlSegments(_ sender: AnimationsTimingControlSegments, didSelectAt index: Int) 95 | } 96 | 97 | class AnimationsTimingControlSegments: AnimationsTimingControl { 98 | @IBOutlet weak var segmentedControl: UISegmentedControl! 99 | 100 | @IBAction func segmentedControlValueChanged(_ sender: UISegmentedControl) { 101 | guard let delegate = delegate as? AnimationsTimingControlSegmentsDelegate else { return } 102 | delegate.animationsTimingControlSegments(self, didSelectAt: sender.selectedSegmentIndex) 103 | } 104 | } 105 | 106 | protocol AnimationsTimingControlSwitchDelegate: 107 | AnimationsTimingControlDelegate 108 | { 109 | func animationsTimingControlSwitch(_ sender: AnimationsTimingControlSwitch, didToggle value: Bool) 110 | } 111 | 112 | class AnimationsTimingControlSwitch: AnimationsTimingControl { 113 | @IBOutlet weak var titleLabel: UILabel! 114 | @IBOutlet weak var `switch`: UISwitch! 115 | 116 | @IBAction func switchValueChanged(_ sender: UISwitch) { 117 | guard let delegate = delegate as? AnimationsTimingControlSwitchDelegate else { return } 118 | delegate.animationsTimingControlSwitch(self, didToggle: sender.isOn) 119 | titleLabel.text = sender.isOn ? "Enabled" : "Disabled" 120 | } 121 | } 122 | 123 | protocol AnimationsTimingControlStepperDelegate: 124 | AnimationsTimingControlDelegate 125 | { 126 | func animationsTimingControlStepper(_ sender: AnimationsTimingControlStepper, didChangeValue value: Double) 127 | } 128 | 129 | class AnimationsTimingControlStepper: AnimationsTimingControl { 130 | @IBOutlet weak var titleLabel: UILabel! 131 | @IBOutlet weak var stepper: UIStepper! 132 | 133 | @IBAction func stepperValueChanged(_ sender: UIStepper) { 134 | guard let delegate = delegate as? AnimationsTimingControlStepperDelegate else { return } 135 | delegate.animationsTimingControlStepper(self, didChangeValue: sender.value) 136 | titleLabel.text = String(format: "%.1f", sender.value) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /使用說明.md: -------------------------------------------------------------------------------- 1 | # UIAnimationToolbox 2 | 3 | [![Build Status](https://travis-ci.com/WeZZard/UIAnimationToolbox.svg?branch=master)](https://travis-ci.com/WeZZard/UIAnimationToolbox) 4 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | 6 | 在 UIKit 層級使用 Core Animation 高級特性。 7 | 8 | ## 亮點 9 | 10 | - 一行代碼變換動畫時機。 11 | 12 | ```swift 13 | UIView.animate(withDuration: 0.3) { 14 | /* 沒有變換時機的動畫 */ 15 | UIView.shiftAnimationsTiming(speed: 0.5) { 16 | /* 變換了時機的動畫 */ 17 | } 18 | } 19 | ``` 20 | 21 | 你也可以變換 `CAMediaTiming` 上的其他 properties。 22 | 23 | - 輕鬆實現自定義可動畫化 view properties。 24 | 25 | - 一行 API 對 UIKit 動畫進行插值。 26 | 27 | ```swift 28 | UIView.animate(withDuration: 0.3) { 29 | /* 動畫設置 */ 30 | UIView.addAnimationInterpolator { (progress) in 31 | /* 可以根據插值來幹活了 */ 32 | } 33 | } 34 | ``` 35 | 36 | - 以「牛頓——拉弗森」方法對 `CAMediaTimingFunction` 的 `y` 進行求值。 37 | 38 | - 在 Core Animation 層輕鬆開啓疊加動畫 (additive animation)。 39 | 40 | ## 用法 41 | 42 | 備註:你可以通過閱讀本項目中的目錄 (category) 應用來對框架獲得一個深刻的認識。 43 | 44 | ### 變換動畫時機 45 | 46 | 本框架介入了 UIKit block 動畫和 spring 動畫的全部裝置過程,所以其可以隨時變換動畫時機。 47 | 48 | ```swift 49 | UIView.animate(withDuration: 0.3) { 50 | /* 沒有變換時機的動畫 */ 51 | UIView.shiftAnimationsTiming(speed: 0.5) { 52 | /* 變換了時機的動畫 */ 53 | } 54 | } 55 | ``` 56 | 57 | 動畫時機 58 | 59 | 你可以在 David Rönnqvist 撰寫的 [Controllign Animation Timing](http://ronnqvi.st/controlling-animation-timing) 60 | 上查看 `CAMediaTiming` 的詳細用法, 或者你可以看看他寫的這個美妙的 61 | [cheat-sheet](http://ronnqvi.st/images/CAMediaTiming%20cheat%20sheet.pdf)。 62 | 63 | ### 輕鬆實現自定義可動畫化 view properties 64 | 65 | 諸如 [Animating Custom Layer Properties](https://www.objc.io/issues/12-animations/animating-custom-layer-properties/) 中所提及的傳統的自定義 property 動畫解決方案一般鼓勵你手動設置一個 `CAAnimation` 對象然後在 `CALayer.action(forKey:)` 函數返回。這非常的麻煩,並且在如今的 spring 動畫滿街飛的時代也不完美,因爲你怎麼也不可能設置出一個和 UIKit API 所創建的 spring 動畫一模一樣的 `CASpringAnimation`。 66 | 67 | UIAnimationToolbox 接管了 block 動畫和 spring 動畫的裝置過程,而你可以通過 UIAnimationToolbox 來獲得和 UIKit API 所創建的一模一樣的動畫對象。 68 | 69 | 你只需要在你的 view backward layer 提供一個可動畫的 property。 70 | 71 | ```swift 72 | class Layer: CALayer { 73 | @NSManaged 74 | var animatedProperty: CGFloat 75 | 76 | override class func needsDisplay(forKey key: String) -> Bool { 77 | switch key { 78 | case "animatedProperty": return true 79 | default: return super.needsDisplay(forKey: key) 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | 然後將 view backward layer 和 view 之間建立聯繫。 86 | 87 | ```swift 88 | class View: UIView { 89 | var animatedProperty: CGFloat { 90 | get { return _layer.animatedProperty } 91 | set { _layer.animatedProperty = newValue } 92 | } 93 | 94 | override class var layerClass: AnyClass { 95 | return Layer.self 96 | } 97 | 98 | var _layer: Layer { return layer as! Layer } 99 | 100 | init(animatedProperty: CGFloat) { 101 | super.init(frame: .zero) 102 | self.animatedProperty = animatedProperty 103 | } 104 | 105 | required init?(coder aDecoder: NSCoder) { 106 | super.init(coder: aDecoder) 107 | animatedProperty = 0 108 | } 109 | 110 | override func draw(_ rect: CGRect) { 111 | let presentationLayer = _layer.presentation() ?? _layer 112 | 113 | // Do things with `presentationLayer.animatedProperty` 114 | } 115 | } 116 | ``` 117 | 118 | 最後完成魔法。 119 | 120 | ```swift 121 | class View: UIView { 122 | ... 123 | 124 | override func action(for layer: CALayer, forKey event: String) -> CAAction? { 125 | switch event { 126 | case "animatedProperty": 127 | return UIAnimationActionInferred(layer: layer, event: event) 128 | default: 129 | return super.action(for: layer, forKey: event) 130 | } 131 | } 132 | 133 | ... 134 | } 135 | ``` 136 | 137 | `UIAnimationActionInferred` 是用來幫助你在 UIKit block/spring 動畫 API 的 block 內部獲得和 UIKit API 所創建的動畫對象一模一樣的 `CAAnimation` 實例的。當你離開 UIKit block/spring 動畫 API 的 block 時其將毫無作用而屆時一切就像設置一個 view 的 `frame` 或者 `alpha` 一樣。 138 | 139 | 另外,對於類型是 `CGRect`, `CGSize`, `CGPoint`, `CGVector`, `CATransform3D`, `CGFloat`, `Double` 和 `Float` 的 property,他們默認將享受疊加動畫 (additive animations)。 140 | 141 | ### UIKit Animation 插值 142 | 143 | 本框架也可以對 UIKit block 動畫和 spring 動畫進行插值。你可以用這個 API 來對你的自定義的可動物件(比如說音量)來和 UIKit block 動畫和 spring 動畫進行同步。 144 | 145 | ```swift 146 | UIView.animate(withDuration: 2.0) { 147 | // ... 148 | UIView.addAnimationInterpolator { (progress) in 149 | // ... 150 | } 151 | } 152 | ``` 153 | 154 | Block 動畫插值 155 | 156 | ```swift 157 | UIView.animate(withDuration: 2.0, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.2, options: []) { 158 | // ... 159 | UIView.addAnimationInterpolator { (progress) in 160 | // ... 161 | } 162 | } 163 | ``` 164 | 165 | Spring 動畫插值 166 | 167 | 當然,你可以將一個動畫插值函數嵌套在一個時機變換函數內。下面這個動畫插值函數將在其父動畫 block 中所設置的動畫開始後的 1.0s 開始工作。 168 | 169 | ```swift 170 | UIView.animate(withDuration: 2.0) { 171 | // ... 172 | UIView.shiftAnimationsTiming(beginTime: 1.0, fillMode: .forwards) { 173 | // ... 174 | UIView.addAnimationInterpolator { (progress) in 175 | // ... 176 | } 177 | } 178 | } 179 | ``` 180 | 181 | ### 對 CAMediaTimingFunction 的 Y 進行求值 182 | 183 | 對於大多數開發者來說 `CAMediaTimingFunction` 是一個黑箱子,但是其原理很簡單——貝塞爾曲線。我們可以用「牛頓——拉弗森」方法對 `CAMediaTimingFunction` 的 y 值進行求值。這個方法應該在多數大學的數學課上講過。我將這個過程封裝在了 `CAMediaTimingFunction.evaluteY(forX:)` 函數內。 184 | 185 | 對 `CAMediaTimingFunction` 的 Y 進行求值可以讓你在使用 `CADisplayLink` 構建動畫時輕鬆獲得一個很漂亮的表現,或者可以讓你在通過對 `CAMediaTimingFunction` 進行插值來調整設備音量時獲得一個自定義的體驗。 186 | 187 | ### 在 Core Animation 層輕鬆開啓疊加動畫 (Additive Animation) 188 | 189 | 本框架隨行一個泛型類 `AdditiveAnimationAction`。這個類可以將非疊加動畫重寫成疊加動畫。你可以用一個待重寫成疊加動畫的一個動畫對象創建一個 `AdditiveAnimationAction`,然後調用 `AdditiveAnimationAction.run(forKey: "animationKey", object: layer, arguments: nil)` 來將重寫後的動畫裝置在 `layer` 的 `animationKey` 上。 190 | 191 | ## 待辦事項 192 | 193 | - [ ] Keyframe 動畫支持. 194 | 195 | ## 許可證 196 | 197 | MIT 198 | -------------------------------------------------------------------------------- /UIAnimationToolboxTests/UIView_AnimationsTimingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView_AnimationsTimingTests.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 2019/3/13. 6 | // 7 | 8 | import XCTest 9 | import UIAnimationToolbox 10 | 11 | class UIView_AnimationsTimingTests: XCTestCase { 12 | func testShiftAnimationsTiming_works_inAnimateWithDurationAnimations() { 13 | let parent = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 14 | let child = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 15 | parent.addSubview(child) 16 | 17 | UIView.animate(withDuration: 0.3, animations: { 18 | parent.bounds = CGRect(x: 0, y: 0, width: 200, height: 200) 19 | UIView.shiftAnimationsTiming(timeOffset: 0.3, animations: { 20 | child.bounds = CGRect(x: 0, y: 0, width: 50, height: 50) 21 | }) 22 | }) 23 | 24 | let parentAnimationBoundsSize = parent.layer.animation(forKey: "bounds.size")! as! CABasicAnimation 25 | XCTAssertTrue(parentAnimationBoundsSize.isAdditive) 26 | XCTAssertEqual(parentAnimationBoundsSize.fromValue as! NSValue?, NSValue(cgSize: CGSize(width: -100, height: -100))) 27 | XCTAssertEqual(parentAnimationBoundsSize.toValue as! NSValue?, NSValue(cgSize: CGSize(width: 0, height: 0))) 28 | XCTAssertEqual(parentAnimationBoundsSize.timeOffset, 0) 29 | 30 | let childAnimationBoundsSize = child.layer.animation(forKey: "bounds.size")! as! CABasicAnimation 31 | XCTAssertTrue(childAnimationBoundsSize.isAdditive) 32 | XCTAssertEqual(childAnimationBoundsSize.fromValue as! NSValue?, NSValue(cgSize: CGSize(width: 50, height: 50))) 33 | XCTAssertEqual(childAnimationBoundsSize.toValue as! NSValue?, NSValue(cgSize: CGSize(width: 0, height: 0))) 34 | XCTAssertEqual(childAnimationBoundsSize.timeOffset, 0.3) 35 | } 36 | 37 | func testShiftAnimationsTiming_works_inAnimateWithDurationAnimationsCompletion() { 38 | let parent = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 39 | let child = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 40 | parent.addSubview(child) 41 | 42 | UIView.animate(withDuration: 0.3, animations: { 43 | parent.bounds = CGRect(x: 0, y: 0, width: 200, height: 200) 44 | UIView.shiftAnimationsTiming(timeOffset: 0.3, animations: { 45 | child.bounds = CGRect(x: 0, y: 0, width: 50, height: 50) 46 | }) 47 | }, completion: nil) 48 | 49 | let parentAnimationBoundsSize = parent.layer.animation(forKey: "bounds.size")! as! CABasicAnimation 50 | XCTAssertTrue(parentAnimationBoundsSize.isAdditive) 51 | XCTAssertEqual(parentAnimationBoundsSize.fromValue as! NSValue?, NSValue(cgSize: CGSize(width: -100, height: -100))) 52 | XCTAssertEqual(parentAnimationBoundsSize.toValue as! NSValue?, NSValue(cgSize: CGSize(width: 0, height: 0))) 53 | XCTAssertEqual(parentAnimationBoundsSize.timeOffset, 0) 54 | 55 | let childAnimationBoundsSize = child.layer.animation(forKey: "bounds.size")! as! CABasicAnimation 56 | XCTAssertTrue(childAnimationBoundsSize.isAdditive) 57 | XCTAssertEqual(childAnimationBoundsSize.fromValue as! NSValue?, NSValue(cgSize: CGSize(width: 50, height: 50))) 58 | XCTAssertEqual(childAnimationBoundsSize.toValue as! NSValue?, NSValue(cgSize: CGSize(width: 0, height: 0))) 59 | XCTAssertEqual(childAnimationBoundsSize.timeOffset, 0.3) 60 | } 61 | 62 | func testShiftAnimationsTiming_works_inAnimateWithDurationDelayOptionsAnimationsCompletion() { 63 | let parent = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 64 | let child = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 65 | parent.addSubview(child) 66 | 67 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: { 68 | parent.bounds = CGRect(x: 0, y: 0, width: 200, height: 200) 69 | UIView.shiftAnimationsTiming(timeOffset: 0.3, animations: { 70 | child.bounds = CGRect(x: 0, y: 0, width: 50, height: 50) 71 | }) 72 | }) 73 | 74 | let parentAnimationBoundsSize = parent.layer.animation(forKey: "bounds.size")! as! CABasicAnimation 75 | XCTAssertTrue(parentAnimationBoundsSize.isAdditive) 76 | XCTAssertEqual(parentAnimationBoundsSize.fromValue as! NSValue?, NSValue(cgSize: CGSize(width: -100, height: -100))) 77 | XCTAssertEqual(parentAnimationBoundsSize.toValue as! NSValue?, NSValue(cgSize: CGSize(width: 0, height: 0))) 78 | XCTAssertEqual(parentAnimationBoundsSize.timeOffset, 0) 79 | 80 | let childAnimationBoundsSize = child.layer.animation(forKey: "bounds.size")! as! CABasicAnimation 81 | XCTAssertTrue(childAnimationBoundsSize.isAdditive) 82 | XCTAssertEqual(childAnimationBoundsSize.fromValue as! NSValue?, NSValue(cgSize: CGSize(width: 50, height: 50))) 83 | XCTAssertEqual(childAnimationBoundsSize.toValue as! NSValue?, NSValue(cgSize: CGSize(width: 0, height: 0))) 84 | XCTAssertEqual(childAnimationBoundsSize.timeOffset, 0.3) 85 | } 86 | 87 | func testShiftAnimationsTiming_works_inSpringAnimations() { 88 | let parent = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 89 | let child = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 90 | parent.addSubview(child) 91 | 92 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0, options: [], animations: { 93 | parent.bounds = CGRect(x: 0, y: 0, width: 200, height: 200) 94 | UIView.shiftAnimationsTiming(timeOffset: 0.3, animations: { 95 | child.bounds = CGRect(x: 0, y: 0, width: 50, height: 50) 96 | }) 97 | }, completion: nil) 98 | 99 | let parentAnimationBoundsSize = parent.layer.animation(forKey: "bounds.size")! as! CABasicAnimation 100 | XCTAssertTrue(parentAnimationBoundsSize.isAdditive) 101 | XCTAssertEqual(parentAnimationBoundsSize.fromValue as! NSValue?, NSValue(cgSize: CGSize(width: -100, height: -100))) 102 | XCTAssertEqual(parentAnimationBoundsSize.toValue as! NSValue?, NSValue(cgSize: CGSize(width: 0, height: 0))) 103 | XCTAssertEqual(parentAnimationBoundsSize.timeOffset, 0) 104 | 105 | let childAnimationBoundsSize = child.layer.animation(forKey: "bounds.size")! as! CABasicAnimation 106 | XCTAssertTrue(childAnimationBoundsSize.isAdditive) 107 | XCTAssertEqual(childAnimationBoundsSize.fromValue as! NSValue?, NSValue(cgSize: CGSize(width: 50, height: 50))) 108 | XCTAssertEqual(childAnimationBoundsSize.toValue as! NSValue?, NSValue(cgSize: CGSize(width: 0, height: 0))) 109 | XCTAssertEqual(childAnimationBoundsSize.timeOffset, 0.3) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /UIAnimationToolbox/QuartzCore/Actions/CABasicAnimationAdditiveAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CABasicAnimationAdditiveAction.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 15/01/2017. 6 | // 7 | // 8 | 9 | import QuartzCore 10 | 11 | /// Finalize a `CABasicAnimation`'s `toValue`. You may fill its 12 | /// `fromValue` by yourself, typically before returning the action to a 13 | /// layer in the layer's delegate method. 14 | /// 15 | /// This class may enabled the animation's `isAdditive` property when 16 | /// possible. 17 | /// 18 | open class CABasicAnimationAdditiveAction: 19 | NSObject, CAAction 20 | { 21 | open unowned var layer: CALayer 22 | 23 | open var event: String 24 | 25 | open var pendingAnimation: Animation 26 | 27 | open var isAdditivenessRewriteEnabled: Bool 28 | 29 | open private(set) var hasRewrittenAdditiveness: Bool = false 30 | 31 | public init( 32 | layer: CALayer, 33 | event: String, 34 | pendingAnimation: Animation, 35 | isAdditivenessRewriteEnabled: Bool = true 36 | ) 37 | { 38 | assert(pendingAnimation.fromValue != nil, "Expects animation's fromValue not to be nil.") 39 | assert(!(pendingAnimation.fromValue is NSNull), "Expects animation's fromValue is not an instance of NSNull.") 40 | self.layer = layer 41 | self.event = event 42 | self.pendingAnimation = pendingAnimation 43 | self.isAdditivenessRewriteEnabled = isAdditivenessRewriteEnabled 44 | } 45 | 46 | open func run( 47 | forKey event: String, 48 | object anObject: Any, 49 | arguments dict: [AnyHashable : Any]? 50 | ) 51 | { 52 | // Set to value as non-additive 53 | pendingAnimation.toValue = layer.value(forKey: event) 54 | 55 | // Rewrite `isAdditive` 56 | if isAdditivenessRewriteEnabled { 57 | assert((anObject as AnyObject) === layer) 58 | _rewriteAdditiveness() 59 | } 60 | 61 | if let aLayer = anObject as? CALayer { 62 | // Detect event collision when `anObject` is of type of `CALayer`. 63 | var noCollisionKey = event 64 | 65 | var probingTimes = 0 66 | 67 | while aLayer.animation(forKey: noCollisionKey) != nil { 68 | switch probingTimes { 69 | case 0: noCollisionKey = event 70 | default: noCollisionKey = "\(event)_\(probingTimes)" 71 | } 72 | probingTimes = probingTimes + 1 73 | } 74 | 75 | pendingAnimation.run(forKey: noCollisionKey, object: aLayer, arguments: dict) 76 | } else { 77 | pendingAnimation.run(forKey: event, object: anObject, arguments: dict) 78 | } 79 | } 80 | 81 | private func _rewriteAdditiveness() { 82 | guard !hasRewrittenAdditiveness else { return } 83 | 84 | precondition(pendingAnimation.fromValue != nil && !(pendingAnimation.fromValue is NSNull)) 85 | 86 | let presentationLayer = layer.presentation() ?? layer 87 | 88 | let additivePolicy = type(of: presentationLayer).additivePolicy(forKey: event) 89 | 90 | switch additivePolicy { 91 | case .cgRect: 92 | let fromValue = pendingAnimation.fromValue as! CGRect 93 | let toValue = layer.value(forKey: event) as! CGRect 94 | let delta = CGRect( 95 | x: fromValue.minX - toValue.minX, 96 | y: fromValue.minY - toValue.minY, 97 | width: fromValue.width - toValue.width, 98 | height: fromValue.height - toValue.height 99 | ) 100 | pendingAnimation.isAdditive = true 101 | pendingAnimation.fromValue = delta 102 | pendingAnimation.toValue = CGRect.zero 103 | case .cgPoint: 104 | let fromValue = pendingAnimation.fromValue as! CGPoint 105 | let toValue = layer.value(forKey: event) as! CGPoint 106 | let delta = CGPoint( 107 | x: fromValue.x - toValue.x, 108 | y: fromValue.y - toValue.y 109 | ) 110 | pendingAnimation.isAdditive = true 111 | pendingAnimation.fromValue = delta 112 | pendingAnimation.toValue = CGPoint.zero 113 | case .cgSize: 114 | let fromValue = pendingAnimation.fromValue as! CGSize 115 | let toValue = layer.value(forKey: event) as! CGSize 116 | let delta = CGSize( 117 | width: fromValue.width - toValue.width, 118 | height: fromValue.height - toValue.height 119 | ) 120 | pendingAnimation.isAdditive = true 121 | pendingAnimation.fromValue = delta 122 | pendingAnimation.toValue = CGSize.zero 123 | case .cgVector: 124 | let fromValue = pendingAnimation.fromValue as! CGVector 125 | let toValue = layer.value(forKey: event) as! CGVector 126 | let delta = CGVector( 127 | dx: fromValue.dx - toValue.dx, 128 | dy: fromValue.dy - toValue.dy 129 | ) 130 | pendingAnimation.isAdditive = true 131 | pendingAnimation.fromValue = delta 132 | pendingAnimation.toValue = CGVector.zero 133 | case .caTransform3D: 134 | let fromTransform = pendingAnimation.fromValue as! CATransform3D 135 | let toTransform = layer.value(forKey: event) as! CATransform3D 136 | if CATransform3DIsAffine(toTransform) && CATransform3DIsAffine(fromTransform) { 137 | pendingAnimation.isAdditive = true 138 | let deltaTransform = CATransform3DConcat(CATransform3DInvert(toTransform), fromTransform) 139 | pendingAnimation.fromValue = deltaTransform 140 | pendingAnimation.toValue = CATransform3DIdentity 141 | } 142 | case .double: 143 | let fromValue = pendingAnimation.fromValue as! Double 144 | let toValue = layer.value(forKey: event) as! Double 145 | let delta = fromValue - toValue 146 | pendingAnimation.isAdditive = true 147 | pendingAnimation.fromValue = delta 148 | pendingAnimation.toValue = Double(0) 149 | case .float: 150 | let fromValue = pendingAnimation.fromValue as! Float 151 | let toValue = layer.value(forKey: event) as! Float 152 | let delta = fromValue - toValue 153 | pendingAnimation.isAdditive = true 154 | pendingAnimation.fromValue = delta 155 | pendingAnimation.toValue = Float(0) 156 | case .none: 157 | break 158 | } 159 | hasRewrittenAdditiveness = true 160 | } 161 | } 162 | 163 | @available(*, renamed: "CABasicAnimationAdditiveAction") 164 | public typealias AdditiveAnimationAction = CABasicAnimationAdditiveAction 165 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/UIView/UIView+Animate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Animate.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 9/14/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: UIView.animate(withDuration:delay:options:animations:completion:) 12 | 13 | internal typealias _UIViewAnimateWithDuration5 = @convention(c) ( 14 | UIView.Type, 15 | Selector, 16 | TimeInterval, 17 | TimeInterval, 18 | UIView.AnimationOptions, 19 | @convention(block) () -> Void, 20 | (@convention(block) (Bool) -> Void)? 21 | ) -> Void 22 | 23 | private var _originalAnimateWithDuration5: _UIViewAnimateWithDuration5! 24 | private let _animateWithDuration5: _UIViewAnimateWithDuration5 = { 25 | (aClass, aSelector, duration, delay, options, animations, 26 | completionOrNil) -> Void in 27 | 28 | let configureContext = _UIBlockAnimationFactory5( 29 | duration: duration, 30 | delay: delay, 31 | options: options, 32 | selector: aSelector, 33 | originalImpl: _originalAnimateWithDuration5 34 | ) 35 | 36 | UIView._animationContexts.append(configureContext) 37 | 38 | configureContext.prepare() 39 | 40 | _originalAnimateWithDuration5( 41 | aClass, aSelector, duration, delay, options, animations, 42 | completionOrNil 43 | ) 44 | 45 | guard let last = UIView._animationContexts.removeLast() 46 | as? _UIBlockAnimationFactory5, 47 | last === configureContext 48 | else 49 | { 50 | fatalError("You cannot call UIResponder decendants from non-main thread.") 51 | } 52 | } 53 | 54 | internal func _swizzleAnimateWithDuration5() { 55 | let sel = #selector(UIView.animate(withDuration:delay:options:animations:completion:)) 56 | let method = class_getClassMethod(UIView.self, sel)! 57 | let impl = method_getImplementation(method) 58 | _originalAnimateWithDuration5 = unsafeBitCast(impl, to: _UIViewAnimateWithDuration5.self) 59 | method_setImplementation(method, unsafeBitCast(_animateWithDuration5, to: IMP.self)) 60 | } 61 | 62 | // MARK: UIView.animate(withDuration:animations:completion:) 63 | 64 | internal typealias _UIViewAnimateWithDuration3 = @convention(c) ( 65 | UIView.Type, 66 | Selector, 67 | TimeInterval, 68 | @convention(block) () -> Void, 69 | (@convention(block) (Bool) -> Void)? 70 | ) -> Void 71 | 72 | private var _originalAnimateWithDuration3: _UIViewAnimateWithDuration3! 73 | private let _animateWithDuration3: _UIViewAnimateWithDuration3 = { 74 | (aClass, aSelector, duration, animations, completionOrNil) -> Void in 75 | 76 | let configureContext = _UIBlockAnimationFactory3( 77 | duration: duration, 78 | selector: aSelector, 79 | originalImpl: _originalAnimateWithDuration3 80 | ) 81 | 82 | UIView._animationContexts.append(configureContext) 83 | 84 | configureContext.prepare() 85 | 86 | _originalAnimateWithDuration3( 87 | aClass, aSelector, duration, animations, completionOrNil 88 | ) 89 | 90 | guard let last = UIView._animationContexts.removeLast() 91 | as? _UIBlockAnimationFactory3, 92 | last === configureContext 93 | else 94 | { 95 | fatalError("You cannot call UIResponder decendants from non-main thread.") 96 | } 97 | } 98 | 99 | internal func _swizzleAnimateWithDuration3() { 100 | let sel = #selector(UIView.animate(withDuration:animations:completion:)) 101 | let method = class_getClassMethod(UIView.self, sel)! 102 | let impl = method_getImplementation(method) 103 | _originalAnimateWithDuration3 = unsafeBitCast(impl, to: _UIViewAnimateWithDuration3.self) 104 | method_setImplementation(method, unsafeBitCast(_animateWithDuration3, to: IMP.self)) 105 | } 106 | 107 | // MARK: UIView.animate(withDuration:animations:) 108 | 109 | internal typealias _UIViewAnimateWithDuration2 = @convention(c) ( 110 | UIView.Type, 111 | Selector, 112 | TimeInterval, 113 | @convention(block) () -> Void 114 | ) -> Void 115 | 116 | private var _originalAnimateWithDuration2: _UIViewAnimateWithDuration2! 117 | private let _animateWithDuration2: _UIViewAnimateWithDuration2 = { 118 | (aClass, aSelector, duration, animations) -> Void in 119 | 120 | let configureContext = _UIBlockAnimationFactory2( 121 | duration: duration, 122 | selector: aSelector, 123 | originalImpl: _originalAnimateWithDuration2 124 | ) 125 | 126 | UIView._animationContexts.append(configureContext) 127 | 128 | configureContext.prepare() 129 | 130 | _originalAnimateWithDuration2( 131 | aClass, aSelector, duration, animations 132 | ) 133 | 134 | guard let last = UIView._animationContexts.removeLast() 135 | as? _UIBlockAnimationFactory2, 136 | last === configureContext 137 | else 138 | { 139 | fatalError("You cannot call UIResponder decendants from non-main thread.") 140 | } 141 | } 142 | 143 | internal func _swizzleAnimateWithDuration2() { 144 | let sel = #selector(UIView.animate(withDuration:animations:)) 145 | let method = class_getClassMethod(UIView.self, sel)! 146 | let impl = method_getImplementation(method) 147 | _originalAnimateWithDuration2 = unsafeBitCast(impl, to: _UIViewAnimateWithDuration2.self) 148 | method_setImplementation(method, unsafeBitCast(_animateWithDuration2, to: IMP.self)) 149 | } 150 | 151 | // MARK: UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:) 152 | 153 | @available(iOS 9.0, *) 154 | internal typealias _UIViewSpringAnimation = @convention(c) ( 155 | UIView.Type, 156 | Selector, 157 | TimeInterval, 158 | TimeInterval, 159 | CGFloat, 160 | CGFloat, 161 | UIView.AnimationOptions, 162 | @convention(block) () -> Void, 163 | (@convention(block) (Bool) -> Void)? 164 | ) -> Void 165 | 166 | @available(iOS 9.0, *) 167 | private var _originalSpringAnimation: _UIViewSpringAnimation! 168 | 169 | @available(iOS 9.0, *) 170 | private let _springAnimation: _UIViewSpringAnimation = { 171 | (aClass, aSelector, duration, delay, damping, initialVelocity, 172 | options, animations, completionOrNil) 173 | -> Void in 174 | 175 | let configureContext = _UISpringAnimationFactory( 176 | duration: duration, 177 | delay: delay, 178 | damping: damping, 179 | initialVelocity: initialVelocity, 180 | options: options, 181 | selector: aSelector, 182 | originalImpl: _originalSpringAnimation 183 | ) 184 | 185 | UIView._animationContexts.append(configureContext) 186 | 187 | configureContext.prepare() 188 | 189 | _originalSpringAnimation( 190 | aClass, aSelector, duration, delay, damping, 191 | initialVelocity, options, animations, completionOrNil 192 | ) 193 | 194 | guard let last = UIView._animationContexts.removeLast() 195 | as? _UISpringAnimationFactory, 196 | last === configureContext 197 | else 198 | { 199 | fatalError("You cannot call UIResponder decendants from non-main thread.") 200 | } 201 | } 202 | 203 | @available(iOS 9.0, *) 204 | internal func _swizzleSpringAnimationAPI() { 205 | let sel = #selector(UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)) 206 | let method = class_getClassMethod(UIView.self, sel)! 207 | let impl = method_getImplementation(method) 208 | _originalSpringAnimation = unsafeBitCast(impl, to: _UIViewSpringAnimation.self) 209 | method_setImplementation(method, unsafeBitCast(_springAnimation, to: IMP.self)) 210 | } 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UIAnimationToolbox 2 | 3 | [![Build Status](https://travis-ci.com/WeZZard/UIAnimationToolbox.svg?branch=master)](https://travis-ci.com/WeZZard/UIAnimationToolbox) 4 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | 6 | [中文](./使用說明.md) 7 | 8 | Make use of advanced Core Animation features without leaving UIKit. 9 | 10 | ## Highlights 11 | 12 | - Shift animations timing with a nest-able one-line API. 13 | 14 | ```swift 15 | UIView.animate(withDuration: 0.3) { 16 | /* Animations without timing shifting */ 17 | UIView.shiftAnimationsTiming(speed: 0.5) { 18 | /* Timing shifted animations */ 19 | } 20 | } 21 | ``` 22 | 23 | You can also shift other properties on `CAMediaTiming`. 24 | 25 | - Custom view property animations with less effort. 26 | 27 | - Interpolate UIKit animations with a single-line API. 28 | 29 | ```swift 30 | UIView.animate(withDuration: 0.3) { 31 | /* Install animations */ 32 | UIView.addAnimationInterpolator { (progress) in 33 | /* Do your works with animation's progress */ 34 | } 35 | } 36 | ``` 37 | 38 | - Evaluate `CAMediaTimingFunction`'s `y` value with Newton-Raphson method. 39 | 40 | - Enables additive animation in Core Animation level with less effort. 41 | 42 | ## Usages 43 | 44 | Notes: You can gain a deep understanding of the framework by reading the 45 | category app in this project. 46 | 47 | ### Shift Animation's Timing 48 | 49 | The framework hooked up into the whole process of the installations of 50 | UIKit block animations and spring animations, thus it can shifting 51 | animations timing on-the-fly. 52 | 53 | ```swift 54 | UIView.animate(withDuration: 0.3) { 55 | /* Animations without timing shifting */ 56 | UIView.shiftAnimationsTiming(speed: 0.5) { 57 | /* Timing shifted animations */ 58 | } 59 | } 60 | ``` 61 | 62 | Animations Timing 63 | 64 | See detailed usages of `CAMediaTiming` on 65 | [Controllign Animation Timing](http://ronnqvi.st/controlling-animation-timing) 66 | by David Rönnqvist, or you can use the beautiful 67 | [cheat-sheet](http://ronnqvi.st/images/CAMediaTiming%20cheat%20sheet.pdf) 68 | done by him. 69 | 70 | ### Custom View Property Animations with Less Effort 71 | 72 | Traditional custom property animations solutions like those mentioned in 73 | [Animating Custom Layer Properties](https://www.objc.io/issues/12-animations/animating-custom-layer-properties/) 74 | encourages you to assemble `CAAnimation` objects and then return them in 75 | `CALayer.action(forKey:)` which is tedious and imperfect in current 76 | spring-animation days, because you almost cannot assemble a 77 | `CASpringAnimation` object perfectly like those created by UIKit API. 78 | 79 | UIAnimationToolbox takes over the control of the installations of block 80 | animations and spring animations and gives you an opportunity to get 81 | animation objects which is totally the same to those created by UIKit API. 82 | 83 | You only have to offer the animated property in your backward layer of the 84 | view. 85 | 86 | ```swift 87 | class Layer: CALayer { 88 | @NSManaged 89 | var animatedProperty: CGFloat 90 | 91 | override class func needsDisplay(forKey key: String) -> Bool { 92 | switch key { 93 | case "animatedProperty": return true 94 | default: return super.needsDisplay(forKey: key) 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | Then build the relationship between the backward layer and the view 101 | 102 | ```swift 103 | class View: UIView { 104 | var animatedProperty: CGFloat { 105 | get { return _layer.animatedProperty } 106 | set { _layer.animatedProperty = newValue } 107 | } 108 | 109 | override class var layerClass: AnyClass { 110 | return Layer.self 111 | } 112 | 113 | var _layer: Layer { return layer as! Layer } 114 | 115 | init(animatedProperty: CGFloat) { 116 | super.init(frame: .zero) 117 | self.animatedProperty = animatedProperty 118 | } 119 | 120 | required init?(coder aDecoder: NSCoder) { 121 | super.init(coder: aDecoder) 122 | animatedProperty = 0 123 | } 124 | 125 | override func draw(_ rect: CGRect) { 126 | let presentationLayer = _layer.presentation() ?? _layer 127 | 128 | // Do things with `presentationLayer.animatedProperty` 129 | } 130 | } 131 | ``` 132 | 133 | Finally do the magic. 134 | 135 | ```swift 136 | class View: UIView { 137 | ... 138 | 139 | override func action(for layer: CALayer, forKey event: String) -> CAAction? { 140 | switch event { 141 | case "animatedProperty": 142 | return UIAnimationActionInferred(layer: layer, event: event) 143 | default: 144 | return super.action(for: layer, forKey: event) 145 | } 146 | } 147 | 148 | ... 149 | } 150 | ``` 151 | 152 | `UIAnimationActionInferred` can help you infer a `CAAnimation` object when 153 | you set the view's `animatedProperty` inside a UIKit block animation block 154 | or spring animation block. When you are out of those blocks, it does 155 | nothing and all things goes like setting a view's `frame` or `alpha`. 156 | 157 | Plus, for properties of type `CGRect`, `CGSize`, `CGPoint`, `CGVector`, 158 | `CATransform3D`, `CGFloat`, `Double` and `Float`, they enjoys additive 159 | animations by default. 160 | 161 | ### Interpolate UIKit Animations 162 | 163 | The framework also can interpolate UIKit block animations and spring 164 | animations. You can use this API to synchronize UIKit block animations and 165 | spring animations with your custom animated stuffs (such as voice volumne). 166 | 167 | ```swift 168 | UIView.animate(withDuration: 2.0) { 169 | // ... 170 | UIView.addAnimationInterpolator { (progress) in 171 | // ... 172 | } 173 | } 174 | ``` 175 | 176 | Interpolate Block Animations 177 | 178 | ```swift 179 | UIView.animate(withDuration: 2.0, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.2, options: []) { 180 | // ... 181 | UIView.addAnimationInterpolator { (progress) in 182 | // ... 183 | } 184 | } 185 | ``` 186 | 187 | Interpolate Spring Animations 188 | 189 | Of course, you can also nest an animation interpolator in a timing 190 | shifting closure. The following animation interpolator gets started to 191 | work in 1.0s later than the animations installed by the parent animation 192 | block began. 193 | 194 | ```swift 195 | UIView.animate(withDuration: 2.0) { 196 | // ... 197 | UIView.shiftAnimationsTiming(beginTime: 1.0, fillMode: .forwards) { 198 | // ... 199 | UIView.addAnimationInterpolator { (progress) in 200 | // ... 201 | } 202 | } 203 | } 204 | ``` 205 | 206 | ### Evaluate CAMediaTimingFunction's Y Value 207 | 208 | `CAMediaTimingFunction` is a black-box to much developers, but the theory 209 | behind this class is quite simple - bezier path. We can evaluate 210 | `CAMediaTimingFunction`'s y value by using Newton-Raphson method which may 211 | be taught on math courses of most colleges. I encapsulated the whole 212 | process within the function `CAMediaTimingFunction.evaluteY(forX:)`. 213 | 214 | Evaluating `CAMediaTimingFunction`'s y value enables you to easily gain a 215 | beautiful performance when building animations with `CADisplayLink` or 216 | customized experience when tuning voice volumn by interpolating 217 | `CAMediaTimingFunction`. 218 | 219 | ### Enables Additive Animation in Core Animation Level with Less Effort 220 | 221 | The framework ships with a generic class `AdditiveAnimationAction` 222 | which rewrite non-additive animations into additive animations. You can 223 | create `AdditiveAnimationAction` with an animation pending to be rewritten 224 | and then call `AdditiveAnimationAction.run(forKey: "animationKey", object: layer, arguments: nil)` 225 | to add the rewritten animation to a `layer` for `"animationKey"`. 226 | 227 | ## Todos 228 | 229 | - [ ] Keyframe animations support. 230 | 231 | ## License 232 | 233 | MIT 234 | -------------------------------------------------------------------------------- /UIAnimationToolboxCategory-iOS/AnimationsTimingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationsTimingViewController.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created on 2019/3/14. 6 | // 7 | 8 | import UIKit 9 | import UIAnimationToolbox 10 | 11 | enum AnimationsTimingConfiguration: String { 12 | case beginTime = "BeginTime" 13 | case duration = "Duration" 14 | case speed = "Speed" 15 | case timeOffset = "TimeOffset" 16 | case repeatingByCount = "Repeating by Count" 17 | case repeatingByDuration = "Repeating by Duration" 18 | case autoreverses = "Autoreverses" 19 | case fillMode = "FillMode" 20 | 21 | var pseudoCode: String { 22 | switch self { 23 | case .beginTime: 24 | return """ 25 | UIView.animate(withDuration: animationDuration) { 26 | //... 27 | UIView.shiftAnimationsTiming(beginTime: yourBeginTime) { 28 | //... 29 | } 30 | } 31 | """ 32 | case .duration: 33 | return """ 34 | UIView.animate(withDuration: animationDuration) { 35 | //... 36 | UIView.shiftAnimationsTiming(duration: yourDuration) { 37 | //... 38 | } 39 | } 40 | """ 41 | case .speed: 42 | return """ 43 | UIView.animate(withDuration: animationDuration) { 44 | //... 45 | UIView.shiftAnimationsTiming(speed: yourSpeed) { 46 | //... 47 | } 48 | } 49 | """ 50 | case .timeOffset: 51 | return """ 52 | UIView.animate(withDuration: animationDuration) { 53 | //... 54 | UIView.shiftAnimationsTiming(timeOffset: yourTimeOffset) { 55 | //... 56 | } 57 | } 58 | """ 59 | case .repeatingByCount: 60 | return """ 61 | UIView.animate(withDuration: animationDuration) { 62 | //... 63 | UIView.shiftAnimationsTiming(repeating: byCount(yourCount)) { 64 | //... 65 | } 66 | } 67 | """ 68 | case .repeatingByDuration: 69 | return """ 70 | UIView.animate(withDuration: animationDuration) { 71 | //... 72 | UIView.shiftAnimationsTiming(repeating: byDuration(yourDuration)) { 73 | //... 74 | } 75 | } 76 | """ 77 | case .autoreverses: 78 | return """ 79 | UIView.animate(withDuration: animationDuration) { 80 | //... 81 | UIView.shiftAnimationsTiming(autoreverses: true) { 82 | //... 83 | } 84 | } 85 | """ 86 | case .fillMode: 87 | return """ 88 | UIView.animate(withDuration: animationDuration) { 89 | //... 90 | UIView.shiftAnimationsTiming(fillMode: yourFillMode) { 91 | //... 92 | } 93 | } 94 | """ 95 | } 96 | } 97 | } 98 | 99 | class AnimationsTimingViewController: UITableViewController { 100 | var configuration: AnimationsTimingConfiguration! 101 | 102 | override func viewDidLoad() { 103 | super.viewDidLoad() 104 | timmingContentLabel.text = configuration.rawValue 105 | animationView.dataSource = self 106 | animationView.delegate = self 107 | let view = AnimationsTimingControl.make( 108 | configuration: configuration!, 109 | controller: self 110 | ) 111 | view.delegate = self 112 | view.translatesAutoresizingMaskIntoConstraints = false 113 | 114 | settingsContolCellContentView.addSubview(view) 115 | 116 | let constraints = [ 117 | NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: settingsContolCellContentView, attribute: .leading, multiplier: 1.0, constant: 0), 118 | NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: settingsContolCellContentView, attribute: .trailing, multiplier: 1.0, constant: 0), 119 | NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: settingsContolCellContentView, attribute: .top, multiplier: 1.0, constant: 0), 120 | NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: settingsContolCellContentView, attribute: .bottom, multiplier: 1.0, constant: 0), 121 | ] 122 | 123 | settingsContolCellContentView.addConstraints(constraints) 124 | 125 | codeTextView.textContainer.lineBreakMode = .byCharWrapping 126 | codeTextView.text = configuration.pseudoCode 127 | } 128 | 129 | @IBOutlet weak var timmingContentLabel: UILabel! 130 | 131 | @IBOutlet weak var settingsContolCellContentView: UIView! 132 | 133 | @IBOutlet weak var animationView: AnimationsTimingAnimationView! 134 | 135 | @IBAction func playAnimationButtonDidTap(_ sender: UIBarButtonItem) { 136 | sender.isEnabled = false 137 | animationView.play() 138 | } 139 | 140 | @IBOutlet weak var playAnimationButton: UIBarButtonItem! 141 | 142 | @IBOutlet weak var codeTextView: UITextView! 143 | 144 | var animationDuration: TimeInterval = 1.0 145 | var beginTime: TimeInterval = 0.5 146 | var duration: TimeInterval = 0.5 147 | var speed: CGFloat = 0.5 148 | var timeOffset: TimeInterval = 0.2 149 | var repeating: CGFloat = 2 150 | var autoreverses: Bool = true 151 | var fillMode: UIAnimationTimingFillMode = .backwards 152 | 153 | func indexForFillMode(_ fillMode: UIAnimationTimingFillMode) -> Int { 154 | switch fillMode { 155 | case .removed: return 0 156 | case .backwards: return 1 157 | case .forwards: return 2 158 | case .both: return 3 159 | } 160 | } 161 | 162 | func fillModeForIndex(_ index: Int) -> UIAnimationTimingFillMode { 163 | switch index { 164 | case 0: return .removed 165 | case 1: return .backwards 166 | case 2: return .forwards 167 | case 3: return .both 168 | default: fatalError() 169 | } 170 | } 171 | } 172 | 173 | extension AnimationsTimingViewController: AnimationsTimingAnimationViewDelegate { 174 | func animationsTimingAnimationViewDidEndAnimation(_ sender: AnimationsTimingAnimationView) { 175 | playAnimationButton.isEnabled = true 176 | } 177 | } 178 | 179 | extension AnimationsTimingViewController: AnimationsTimingAnimationViewDataSource { 180 | func animationViewConfiguration(_ sender: AnimationsTimingAnimationView) -> AnimationsTimingConfiguration { 181 | return configuration 182 | } 183 | 184 | func animationViewAnimationDuration(_ sender: AnimationsTimingAnimationView) -> TimeInterval { 185 | return animationDuration 186 | } 187 | 188 | func animationViewBeginTime(_ sender: AnimationsTimingAnimationView) -> TimeInterval { 189 | return beginTime 190 | } 191 | 192 | func animationViewDuration(_ sender: AnimationsTimingAnimationView) -> TimeInterval { 193 | return duration 194 | } 195 | 196 | func animationViewSpeed(_ sender: AnimationsTimingAnimationView) -> CGFloat { 197 | return speed 198 | } 199 | 200 | func animationViewTimeOffset(_ sender: AnimationsTimingAnimationView) -> TimeInterval { 201 | return timeOffset 202 | } 203 | 204 | func animationViewRepeating(_ sender: AnimationsTimingAnimationView) -> CGFloat { 205 | return repeating 206 | } 207 | 208 | func animationViewAutoreverses(_ sender: AnimationsTimingAnimationView) -> Bool { 209 | return autoreverses 210 | } 211 | 212 | func animationViewFillMode(_ sender: AnimationsTimingAnimationView) -> UIAnimationTimingFillMode { 213 | return fillMode 214 | } 215 | } 216 | 217 | extension AnimationsTimingViewController: AnimationsTimingControlDelegate { } 218 | 219 | extension AnimationsTimingViewController: AnimationsTimingControlSegmentsDelegate { 220 | func animationsTimingControlSegments(_ sender: AnimationsTimingControlSegments, didSelectAt index: Int) { 221 | switch configuration! { 222 | case .fillMode: fillMode = fillModeForIndex(index) 223 | default: break 224 | } 225 | } 226 | } 227 | 228 | extension AnimationsTimingViewController: AnimationsTimingControlSwitchDelegate { 229 | func animationsTimingControlSwitch(_ sender: AnimationsTimingControlSwitch, didToggle value: Bool) { 230 | switch configuration! { 231 | case .autoreverses: 232 | autoreverses = value 233 | default: break 234 | } 235 | } 236 | } 237 | 238 | extension AnimationsTimingViewController: AnimationsTimingControlStepperDelegate { 239 | func animationsTimingControlStepper(_ sender: AnimationsTimingControlStepper, didChangeValue value: Double) { 240 | switch configuration! { 241 | case .beginTime: beginTime = value 242 | case .duration: duration = value 243 | case .speed: speed = CGFloat(value) 244 | case .timeOffset: timeOffset = value 245 | case .repeatingByCount: repeating = CGFloat(value) 246 | case .repeatingByDuration: repeating = CGFloat(value) 247 | default: break 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /UIAnimationToolbox/QuartzCore/EmulatingQuartzCoreHierarchy/_CAAnimationPrototypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _CAAnimationPrototypes.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 1/29/16. 6 | // 7 | // 8 | 9 | import QuartzCore 10 | 11 | internal class _CABasicAnimationPrototype: _CABasicAnimationProtocol { 12 | internal typealias Animation = CABasicAnimation 13 | 14 | internal var beginTime: CFTimeInterval 15 | internal var timeOffset: TimeInterval 16 | internal var repeatCount: Float 17 | internal var repeatDuration: CFTimeInterval 18 | internal var duration: CFTimeInterval 19 | internal var speed: Float 20 | internal var autoreverses: Bool 21 | internal var fillMode: CAMediaTimingFillMode 22 | 23 | internal var isRemovedOnCompletion: Bool 24 | internal var timingFunction: CAMediaTimingFunction? 25 | internal var delegate: CAAnimationDelegate? 26 | 27 | internal var cumulative: Bool 28 | internal var additive: Bool 29 | 30 | internal required init(animation: Animation) { 31 | beginTime = animation.beginTime 32 | timeOffset = animation.timeOffset 33 | repeatCount = animation.repeatCount 34 | repeatDuration = animation.repeatDuration 35 | duration = animation.duration 36 | speed = animation.speed 37 | autoreverses = animation.autoreverses 38 | fillMode = animation.fillMode 39 | isRemovedOnCompletion = animation.isRemovedOnCompletion 40 | timingFunction = animation.timingFunction 41 | delegate = animation.delegate 42 | cumulative = animation.isCumulative 43 | additive = animation.isAdditive 44 | } 45 | 46 | internal func apply(on animation: Animation) { 47 | animation.beginTime = beginTime 48 | animation.timeOffset = timeOffset 49 | animation.repeatCount = repeatCount 50 | animation.repeatDuration = repeatDuration 51 | animation.duration = duration 52 | animation.speed = speed 53 | animation.autoreverses = autoreverses 54 | animation.fillMode = fillMode 55 | animation.isRemovedOnCompletion = isRemovedOnCompletion 56 | animation.timingFunction = timingFunction 57 | animation.delegate = delegate 58 | animation.isCumulative = cumulative 59 | animation.isAdditive = additive 60 | } 61 | } 62 | 63 | 64 | @available(macOS 10.11, iOS 9.0, *) 65 | internal class _CASpringAnimationPrototype: _CASpringAnimationProtocol { 66 | internal typealias Animation = CASpringAnimation 67 | 68 | internal var beginTime: CFTimeInterval 69 | internal var timeOffset: TimeInterval 70 | internal var repeatCount: Float 71 | internal var repeatDuration: CFTimeInterval 72 | internal var duration: CFTimeInterval 73 | internal var speed: Float 74 | internal var autoreverses: Bool 75 | internal var fillMode: CAMediaTimingFillMode 76 | 77 | internal var isRemovedOnCompletion: Bool 78 | internal var timingFunction: CAMediaTimingFunction? 79 | internal var delegate: CAAnimationDelegate? 80 | 81 | internal var cumulative: Bool 82 | internal var additive: Bool 83 | 84 | internal var mass: CGFloat 85 | internal var stiffness: CGFloat 86 | internal var damping: CGFloat 87 | internal var initialVelocity: CGFloat 88 | 89 | internal required init(animation: Animation) { 90 | beginTime = animation.beginTime 91 | timeOffset = animation.timeOffset 92 | repeatCount = animation.repeatCount 93 | repeatDuration = animation.repeatDuration 94 | duration = animation.duration 95 | speed = animation.speed 96 | autoreverses = animation.autoreverses 97 | fillMode = animation.fillMode 98 | isRemovedOnCompletion = animation.isRemovedOnCompletion 99 | timingFunction = animation.timingFunction 100 | delegate = animation.delegate 101 | cumulative = animation.isCumulative 102 | additive = animation.isAdditive 103 | mass = animation.mass 104 | stiffness = animation.stiffness 105 | damping = animation.damping 106 | initialVelocity = animation.initialVelocity 107 | } 108 | 109 | internal func apply(on animation: Animation) { 110 | animation.beginTime = beginTime 111 | animation.timeOffset = timeOffset 112 | animation.repeatCount = repeatCount 113 | animation.repeatDuration = repeatDuration 114 | animation.duration = duration 115 | animation.speed = speed 116 | animation.autoreverses = autoreverses 117 | animation.fillMode = fillMode 118 | animation.isRemovedOnCompletion = isRemovedOnCompletion 119 | animation.timingFunction = timingFunction 120 | animation.delegate = delegate 121 | animation.isCumulative = cumulative 122 | animation.isAdditive = additive 123 | animation.mass = mass 124 | animation.stiffness = stiffness 125 | animation.damping = damping 126 | animation.initialVelocity = initialVelocity 127 | } 128 | } 129 | 130 | 131 | internal class _CAKeyframeAnimationPrototype: 132 | _CAKeyframeAnimationProtocol 133 | { 134 | internal typealias Animation = CAKeyframeAnimation 135 | 136 | internal var beginTime: CFTimeInterval 137 | internal var timeOffset: TimeInterval 138 | internal var repeatCount: Float 139 | internal var repeatDuration: CFTimeInterval 140 | internal var duration: CFTimeInterval 141 | internal var speed: Float 142 | internal var autoreverses: Bool 143 | internal var fillMode: CAMediaTimingFillMode 144 | 145 | internal var isRemovedOnCompletion: Bool 146 | internal var timingFunction: CAMediaTimingFunction? 147 | internal var delegate: CAAnimationDelegate? 148 | 149 | internal var cumulative: Bool 150 | internal var additive: Bool 151 | 152 | internal var keyTimes: [NSNumber]? 153 | internal var timingFunctions: [CAMediaTimingFunction]? 154 | internal var calculationMode: CAAnimationCalculationMode 155 | internal var rotationMode: CAAnimationRotationMode? 156 | internal var tensionValues: [NSNumber]? 157 | internal var continuityValues: [NSNumber]? 158 | internal var biasValues: [NSNumber]? 159 | 160 | internal required init(animation: Animation) { 161 | beginTime = animation.beginTime 162 | timeOffset = animation.timeOffset 163 | repeatCount = animation.repeatCount 164 | repeatDuration = animation.repeatDuration 165 | duration = animation.duration 166 | speed = animation.speed 167 | autoreverses = animation.autoreverses 168 | fillMode = animation.fillMode 169 | isRemovedOnCompletion = animation.isRemovedOnCompletion 170 | timingFunction = animation.timingFunction 171 | delegate = animation.delegate 172 | cumulative = animation.isCumulative 173 | additive = animation.isAdditive 174 | keyTimes = animation.keyTimes 175 | timingFunctions = animation.timingFunctions 176 | calculationMode = animation.calculationMode 177 | rotationMode = animation.rotationMode 178 | tensionValues = animation.tensionValues 179 | continuityValues = animation.continuityValues 180 | biasValues = animation.biasValues 181 | } 182 | 183 | internal func apply(on animation: Animation) { 184 | animation.beginTime = beginTime 185 | animation.timeOffset = timeOffset 186 | animation.repeatCount = repeatCount 187 | animation.repeatDuration = repeatDuration 188 | animation.duration = duration 189 | animation.speed = speed 190 | animation.autoreverses = autoreverses 191 | animation.fillMode = fillMode 192 | animation.isRemovedOnCompletion = isRemovedOnCompletion 193 | animation.timingFunction = timingFunction 194 | animation.delegate = delegate 195 | animation.isCumulative = cumulative 196 | animation.isAdditive = additive 197 | animation.keyTimes = keyTimes 198 | animation.timingFunctions = timingFunctions 199 | animation.calculationMode = calculationMode 200 | animation.rotationMode = rotationMode 201 | animation.tensionValues = tensionValues 202 | animation.continuityValues = continuityValues 203 | animation.biasValues = biasValues 204 | } 205 | } 206 | 207 | 208 | internal class _CATransitionPrototype: _CATransitionProtocol { 209 | internal typealias Animation = CATransition 210 | 211 | internal var beginTime: CFTimeInterval 212 | internal var timeOffset: TimeInterval 213 | internal var repeatCount: Float 214 | internal var repeatDuration: CFTimeInterval 215 | internal var duration: CFTimeInterval 216 | internal var speed: Float 217 | internal var autoreverses: Bool 218 | internal var fillMode: CAMediaTimingFillMode 219 | 220 | internal var isRemovedOnCompletion: Bool 221 | internal var timingFunction: CAMediaTimingFunction? 222 | internal var delegate: CAAnimationDelegate? 223 | 224 | internal required init(animation: Animation) { 225 | beginTime = animation.beginTime 226 | timeOffset = animation.timeOffset 227 | repeatCount = animation.repeatCount 228 | repeatDuration = animation.repeatDuration 229 | duration = animation.duration 230 | speed = animation.speed 231 | autoreverses = animation.autoreverses 232 | fillMode = animation.fillMode 233 | isRemovedOnCompletion = animation.isRemovedOnCompletion 234 | timingFunction = animation.timingFunction 235 | delegate = animation.delegate 236 | } 237 | 238 | internal func apply(on animation: Animation) { 239 | animation.beginTime = beginTime 240 | animation.timeOffset = timeOffset 241 | animation.repeatCount = repeatCount 242 | animation.repeatDuration = repeatDuration 243 | animation.duration = duration 244 | animation.speed = speed 245 | animation.autoreverses = autoreverses 246 | animation.fillMode = fillMode 247 | animation.isRemovedOnCompletion = isRemovedOnCompletion 248 | animation.timingFunction = timingFunction 249 | animation.delegate = delegate 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /UIAnimationToolbox/UIKit/AnimationsTiming/UIAnimationTiming.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAnimationTiming.swift 3 | // UIAnimationToolbox 4 | // 5 | // Created by WeZZard on 1/29/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | public struct UIAnimationTiming { 12 | public let beginTime: TimeInterval? 13 | public let duration: TimeInterval? 14 | public let speed: CGFloat? 15 | public let timeOffset: TimeInterval? 16 | public let repeating: UIAnimationTimingRepeating? 17 | public let autoreverses: Bool? 18 | public let fillMode: UIAnimationTimingFillMode? 19 | 20 | public init( 21 | beginTime: TimeInterval?=nil, 22 | duration: TimeInterval?=nil, 23 | speed: CGFloat?=nil, 24 | timeOffset: TimeInterval?=nil, 25 | repeating: UIAnimationTimingRepeating?=nil, 26 | autoreverses: Bool?=nil, 27 | fillMode: UIAnimationTimingFillMode?=nil 28 | ) 29 | { 30 | self.beginTime = beginTime 31 | self.duration = duration 32 | self.speed = speed 33 | self.timeOffset = timeOffset 34 | self.repeating = repeating 35 | self.autoreverses = autoreverses 36 | self.fillMode = fillMode 37 | } 38 | 39 | internal func _shiftAction(_ action: CAAction) { 40 | if let mediaTiming = action as? CAMediaTiming { 41 | _shiftMediaTiming(mediaTiming) 42 | } else if let mediaTiming = (action as? NSObject)? 43 | .value(forKey: "pendingAnimation") as? CAMediaTiming 44 | { 45 | _shiftMediaTiming(mediaTiming) 46 | } else if action is NSNull { 47 | // Do nothing 48 | } else { 49 | debugPrint("Unable to recognize action \(action) while shifting its timing.") 50 | } 51 | } 52 | 53 | internal func _shiftMediaTiming(_ mediaTiming: CAMediaTiming) { 54 | if let beginTime = beginTime { 55 | mediaTiming.beginTime = beginTime 56 | } 57 | if let duration = duration { 58 | mediaTiming.duration = duration 59 | } 60 | if let speed = speed { 61 | mediaTiming.speed = Float(speed) 62 | } 63 | if let timeOffset = timeOffset { 64 | mediaTiming.timeOffset = timeOffset 65 | } 66 | if let repeating = repeating { 67 | switch repeating { 68 | case let .byCount(count): 69 | mediaTiming.repeatCount = Float(count) 70 | case let .byDuration(duration): 71 | mediaTiming.repeatDuration = duration 72 | } 73 | } 74 | if let autoreverses = autoreverses { 75 | mediaTiming.autoreverses = autoreverses 76 | } 77 | if let fillMode = fillMode { 78 | mediaTiming.fillMode = fillMode._caFillMode 79 | } 80 | } 81 | } 82 | 83 | 84 | extension UIAnimationTiming { 85 | /// Makes a delay effect. 86 | /// 87 | /// Effects of applying `.delaying(for: 1)` on an animation of 88 | /// duration of 1 second: 89 | /// 90 | /// ``` 91 | /// | 0s 1s 2s 92 | /// ---------+-------------- 93 | /// | 94 | /// Original | 1---->2 95 | /// | 96 | /// Shifted | 1---->2 97 | /// | 98 | /// ``` 99 | /// 100 | public static func makeDelay(delay: TimeInterval) -> UIAnimationTiming { 101 | return UIAnimationTiming(beginTime: delay, fillMode: .forwards) 102 | } 103 | 104 | /// Makes a hold effect. 105 | /// 106 | /// Effects of applying `.hold(until: 1)` on an animation of duration 107 | /// of 1 second: 108 | /// 109 | /// ``` 110 | /// | 0s 1s 2s 111 | /// ---------+-------------- 112 | /// | 113 | /// Original | 1---->2 114 | /// | 115 | /// Shifted | 1---->1---->2 116 | /// | 117 | /// ``` 118 | /// 119 | public static func makeHold(duration: TimeInterval) -> UIAnimationTiming { 120 | return UIAnimationTiming(beginTime: duration, fillMode: .backwards) 121 | } 122 | 123 | /// Makes a shift effect. 124 | /// 125 | /// Effects of applying `.shifting(1)` on an animation of duration of 126 | /// 2 second: 127 | /// 128 | /// ``` 129 | /// | 0s 1s 2s 130 | /// ---------+-------------- 131 | /// | 132 | /// Original | 1---->2---->3 133 | /// | 134 | /// Shifted | 2---->3---->1 135 | /// | 136 | /// ``` 137 | /// 138 | public static func makeShift(timeOffset: TimeInterval) -> UIAnimationTiming { 139 | return UIAnimationTiming(timeOffset: timeOffset) 140 | } 141 | 142 | /// Make a speed effect. Negative value is allowed. 143 | /// 144 | /// Effects of applying `.speeding(at: 2)` on an animation of 145 | /// duration of 2 second: 146 | /// 147 | /// ``` 148 | /// | 0s 1s 2s 149 | /// ---------+-------------- 150 | /// | 151 | /// Original | 1---->2---->3 152 | /// | 153 | /// Shifted | 1->2->3 154 | /// | 155 | /// ``` 156 | /// 157 | /// - Notes: This function accepts negative `speed` value which causes 158 | /// the animation played reversely. 159 | /// 160 | public static func makeSpeed(speed: CGFloat) -> UIAnimationTiming { 161 | return UIAnimationTiming(speed: speed) 162 | } 163 | 164 | /// Makes a speed effect. 165 | /// 166 | /// Effects of applying `.autoreversing(at: true)` on an animation of 167 | /// duration of 1 second: 168 | /// 169 | /// ``` 170 | /// | 0s 1s 2s 171 | /// ---------+-------------- 172 | /// | 173 | /// Original | 1---->2 174 | /// | 175 | /// Shifted | 1---->2---->1 176 | /// | 177 | /// ``` 178 | /// 179 | public static func makeAutoreversing(autoreverses: Bool) -> UIAnimationTiming { 180 | return UIAnimationTiming(autoreverses: autoreverses) 181 | } 182 | 183 | /// Makes a repeat-by-count effect. Fractional value is allowed. 184 | /// 185 | /// Effects of applying `.repeating(byCount: 1)` on an animation of 186 | /// duration of 1 second: 187 | /// 188 | /// ``` 189 | /// | 0s 1s 2s 190 | /// ---------+---------------- 191 | /// | 192 | /// Original | 1---->2 193 | /// | 194 | /// Shifted | 1---->2,1---->2 195 | /// | 196 | /// ``` 197 | /// 198 | public static func makeRepetition(count: CGFloat) -> UIAnimationTiming { 199 | return UIAnimationTiming(repeating: .byCount(count)) 200 | } 201 | 202 | /// Makes a repeat-by-duration effect. 203 | /// 204 | /// Effects of applying `.repeating(byDuration: 1)` on an animation of 205 | /// duration of 2 second: 206 | /// 207 | /// ``` 208 | /// | 0s 1s 2s 209 | /// ---------+--------------- 210 | /// | 211 | /// Original | 1---->2---->3 212 | /// | 213 | /// Shifted | 1---->2 214 | /// | 215 | /// ``` 216 | /// 217 | public static func makeRepetition(duration: TimeInterval) -> UIAnimationTiming { 218 | return UIAnimationTiming(repeating: .byDuration(duration)) 219 | } 220 | } 221 | 222 | 223 | extension UIAnimationTiming { 224 | public func delaying(for time: TimeInterval) -> UIAnimationTiming { 225 | return UIAnimationTiming( 226 | beginTime: time, 227 | duration: duration, 228 | speed: speed, 229 | timeOffset: timeOffset, 230 | repeating: repeating, 231 | autoreverses: autoreverses, 232 | fillMode: .forwards 233 | ) 234 | } 235 | 236 | public func holding(until time: TimeInterval) -> UIAnimationTiming { 237 | return UIAnimationTiming( 238 | beginTime: time, 239 | duration: duration, 240 | speed: speed, 241 | timeOffset: timeOffset, 242 | repeating: repeating, 243 | autoreverses: autoreverses, 244 | fillMode: .backwards 245 | ) 246 | } 247 | 248 | public func speeding(_ at: CGFloat) -> UIAnimationTiming { 249 | return UIAnimationTiming( 250 | beginTime: beginTime, 251 | duration: duration, 252 | speed: speed, 253 | timeOffset: timeOffset, 254 | repeating: repeating, 255 | autoreverses: autoreverses, 256 | fillMode: fillMode 257 | ) 258 | } 259 | 260 | public func shifting( timeOffset: TimeInterval) -> UIAnimationTiming { 261 | return UIAnimationTiming( 262 | beginTime: beginTime, 263 | duration: duration, 264 | speed: speed, 265 | timeOffset: timeOffset, 266 | repeating: repeating, 267 | autoreverses: autoreverses, 268 | fillMode: fillMode 269 | ) 270 | } 271 | 272 | public func repeating(byCount count: CGFloat) -> UIAnimationTiming { 273 | return UIAnimationTiming( 274 | beginTime: beginTime, 275 | duration: duration, 276 | speed: speed, 277 | timeOffset: timeOffset, 278 | repeating: .byCount(count), 279 | autoreverses: autoreverses, 280 | fillMode: fillMode 281 | ) 282 | } 283 | 284 | public func repeating(byDuration duration: TimeInterval) -> UIAnimationTiming { 285 | return UIAnimationTiming( 286 | beginTime: beginTime, 287 | duration: duration, 288 | speed: speed, 289 | timeOffset: timeOffset, 290 | repeating: .byDuration(duration), 291 | autoreverses: autoreverses, 292 | fillMode: fillMode 293 | ) 294 | } 295 | 296 | public func autoreversing(_ autoreverses: Bool) -> UIAnimationTiming { 297 | return UIAnimationTiming( 298 | beginTime: beginTime, 299 | duration: duration, 300 | speed: speed, 301 | timeOffset: timeOffset, 302 | repeating: repeating, 303 | autoreverses: autoreverses, 304 | fillMode: fillMode 305 | ) 306 | } 307 | } 308 | 309 | 310 | extension UIAnimationTiming { 311 | public func settingBeginTime(_ beginTime: TimeInterval) -> UIAnimationTiming { 312 | return UIAnimationTiming( 313 | beginTime: beginTime, 314 | duration: duration, 315 | speed: speed, 316 | timeOffset: timeOffset, 317 | repeating: repeating, 318 | autoreverses: autoreverses, 319 | fillMode: fillMode 320 | ) 321 | } 322 | 323 | public func settingDuration(_ duration: TimeInterval) -> UIAnimationTiming { 324 | return UIAnimationTiming( 325 | beginTime: beginTime, 326 | duration: duration, 327 | speed: speed, 328 | timeOffset: timeOffset, 329 | repeating: repeating, 330 | autoreverses: autoreverses, 331 | fillMode: fillMode 332 | ) 333 | } 334 | 335 | public func settingSpeed(_ speed: CGFloat) -> UIAnimationTiming { 336 | return UIAnimationTiming( 337 | beginTime: beginTime, 338 | duration: duration, 339 | speed: speed, 340 | timeOffset: timeOffset, 341 | repeating: repeating, 342 | autoreverses: autoreverses, 343 | fillMode: fillMode 344 | ) 345 | } 346 | 347 | public func settingTimeOffset(_ timeOffset: TimeInterval) -> UIAnimationTiming { 348 | return UIAnimationTiming( 349 | beginTime: beginTime, 350 | duration: duration, 351 | speed: speed, 352 | timeOffset: timeOffset, 353 | repeating: repeating, 354 | autoreverses: autoreverses, 355 | fillMode: fillMode 356 | ) 357 | } 358 | 359 | public func settingRepeating(_ repeating: UIAnimationTimingRepeating) -> UIAnimationTiming { 360 | return UIAnimationTiming( 361 | beginTime: beginTime, 362 | duration: duration, 363 | speed: speed, 364 | timeOffset: timeOffset, 365 | repeating: repeating, 366 | autoreverses: autoreverses, 367 | fillMode: fillMode 368 | ) 369 | } 370 | 371 | public func settingAutoreverses(_ autoreverses: Bool) -> UIAnimationTiming { 372 | return UIAnimationTiming( 373 | beginTime: beginTime, 374 | duration: duration, 375 | speed: speed, 376 | timeOffset: timeOffset, 377 | repeating: repeating, 378 | autoreverses: autoreverses, 379 | fillMode: fillMode 380 | ) 381 | } 382 | 383 | public func settingFillMode(_ fillMode: UIAnimationTimingFillMode) -> UIAnimationTiming { 384 | return UIAnimationTiming( 385 | beginTime: beginTime, 386 | duration: duration, 387 | speed: speed, 388 | timeOffset: timeOffset, 389 | repeating: repeating, 390 | autoreverses: autoreverses, 391 | fillMode: fillMode 392 | ) 393 | } 394 | } 395 | 396 | 397 | public enum UIAnimationTimingRepeating { 398 | case byCount(CGFloat) 399 | case byDuration(TimeInterval) 400 | } 401 | 402 | 403 | public enum UIAnimationTimingFillMode: Int { 404 | case removed 405 | case backwards 406 | case forwards 407 | case both 408 | 409 | internal var _caFillMode: CAMediaTimingFillMode { 410 | switch self { 411 | case .removed: return .removed 412 | case .backwards: return .backwards 413 | case .forwards: return .forwards 414 | case .both: return .both 415 | } 416 | } 417 | } 418 | --------------------------------------------------------------------------------