├── .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 | [](https://travis-ci.com/WeZZard/UIAnimationToolbox)
4 | [](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 |
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 |
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 | [](https://travis-ci.com/WeZZard/UIAnimationToolbox)
4 | [](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------