From e54ce3a118c1e877c5ca78a7d2fed9625d0ffc67 Mon Sep 17 00:00:00 2001 From: featherless Date: Fri, 1 Dec 2017 12:35:43 -0500 Subject: [PATCH 01/11] Add support for additively animating transform. (#85) Does not include support for initial velocity. Includes a simple example. --- examples/TapToBounceExample.swift | 62 +++++++++++++++++++ .../project.pbxproj | 4 ++ examples/apps/Catalog/TableOfContents.swift | 15 ++--- src/MDMAnimatableKeyPaths.h | 11 ++++ src/MDMAnimatableKeyPaths.m | 1 + src/private/CABasicAnimation+MotionAnimator.m | 24 ++++++- src/private/MDMBlockAnimations.m | 1 + src/private/MDMUIKitValueCoercion.m | 17 +++++ .../unit/MotionAnimatorBehavioralTests.swift | 1 + tests/unit/MotionAnimatorTests.swift | 3 + tests/unit/QuartzCoreBehavioralTests.swift | 1 + tests/unit/UIKitBehavioralTests.swift | 2 + 12 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 examples/TapToBounceExample.swift diff --git a/examples/TapToBounceExample.swift b/examples/TapToBounceExample.swift new file mode 100644 index 0000000..c4db306 --- /dev/null +++ b/examples/TapToBounceExample.swift @@ -0,0 +1,62 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit +import MotionAnimator + +class TapToBounceExampleViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + + let circle = UIButton() + circle.bounds = CGRect(x: 0, y: 0, width: 128, height: 128) + circle.center = view.center + circle.layer.cornerRadius = circle.bounds.width / 2 + circle.backgroundColor = UIColor(red: (CGFloat)(0xEF) / 255.0, + green: (CGFloat)(0x88) / 255.0, + blue: (CGFloat)(0xAA) / 255.0, + alpha: 1) + view.addSubview(circle) + + circle.addTarget(self, action: #selector(didFocus), + for: [.touchDown, .touchDragEnter]) + circle.addTarget(self, action: #selector(didUnfocus), + for: [.touchUpInside, .touchUpOutside, .touchDragExit]) + } + + let timing = MotionTiming(delay: 0, + duration: 0.5, + curve: MotionCurveMakeSpring(mass: 1, tension: 100, friction: 10), + repetition: .init()) + + func didFocus(_ sender: UIButton) { + let animator = MotionAnimator() + animator.animate(with: timing) { + sender.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) + } + } + + func didUnfocus(_ sender: UIButton) { + let animator = MotionAnimator() + animator.animate(with: timing) { + sender.transform = .identity + } + } +} + diff --git a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj index c0dca84..f32c841 100644 --- a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 2AA864EDA683CEF5FAA721BE /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DBE814C7B88BAD6337052DB /* Pods_UnitTests.framework */; }; + 660248A41FD1B923004C0147 /* TapToBounceExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660248A31FD1B923004C0147 /* TapToBounceExample.swift */; }; 660636021FACC24300C3DFB8 /* TimeScaleFactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */; }; 6625876C1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */; }; 664F59941FCCE27E002EC56D /* UIKitBehavioralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */; }; @@ -55,6 +56,7 @@ 2DBE814C7B88BAD6337052DB /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50D808A6F9E944D54276D32F /* Pods_MotionAnimatorCatalog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MotionAnimatorCatalog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52820916F8FAA40E942A7333 /* Pods-UnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.release.xcconfig"; sourceTree = ""; }; + 660248A31FD1B923004C0147 /* TapToBounceExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToBounceExample.swift; sourceTree = ""; }; 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeScaleFactorTests.swift; sourceTree = ""; }; 6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialVelocityTests.swift; sourceTree = ""; }; 664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBehavioralTests.swift; sourceTree = ""; }; @@ -171,6 +173,7 @@ 66DD4BF41EEF0ECB00207119 /* CalendarCardExpansionExample.m */, 66DD4BF61EEF1C4B00207119 /* CalendarChipMotionSpec.h */, 66DD4BF71EEF1C4B00207119 /* CalendarChipMotionSpec.m */, + 660248A31FD1B923004C0147 /* TapToBounceExample.swift */, ); name = examples; path = ../..; @@ -508,6 +511,7 @@ 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */, 66DD4BF81EEF1C4B00207119 /* CalendarChipMotionSpec.m in Sources */, 66DD4BF51EEF0ECB00207119 /* CalendarCardExpansionExample.m in Sources */, + 660248A41FD1B923004C0147 /* TapToBounceExample.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/examples/apps/Catalog/TableOfContents.swift b/examples/apps/Catalog/TableOfContents.swift index b44f6e3..9b99ab5 100644 --- a/examples/apps/Catalog/TableOfContents.swift +++ b/examples/apps/Catalog/TableOfContents.swift @@ -16,12 +16,9 @@ // MARK: Catalog by convention -// Example entry in the table of contents: -// Extend a UIViewController instance and implement catalogBreadcrumbs(), returning the list of -// breadcrumbs required to navigate to an instance of this view controller. -// -//extension ExampleViewController { -// class func catalogBreadcrumbs() -> [String] { -// return ["Example"] -// } -//} +extension TapToBounceExampleViewController { + class func catalogBreadcrumbs() -> [String] { + return ["Tap to bounce"] + } +} + diff --git a/src/MDMAnimatableKeyPaths.h b/src/MDMAnimatableKeyPaths.h index 7062d27..2a9bbc3 100644 --- a/src/MDMAnimatableKeyPaths.h +++ b/src/MDMAnimatableKeyPaths.h @@ -180,6 +180,17 @@ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathStrokeStart NS_SWIFT_NAME(strok */ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathStrokeEnd NS_SWIFT_NAME(strokeEnd); +/** + Transform. + + Equivalent UIView property: transform (2d only) + Equivalent CALayer property: transform (3d) + Expected value type: CGAffineTransform, CATransform or NSValue with either transform type. + CGAffineTransform value types will be converted to CATransform. + Additive animation supported: Yes. + */ +FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathTransform NS_SWIFT_NAME(transform); + /** Width. diff --git a/src/MDMAnimatableKeyPaths.m b/src/MDMAnimatableKeyPaths.m index 8e6cd14..1fc39a8 100644 --- a/src/MDMAnimatableKeyPaths.m +++ b/src/MDMAnimatableKeyPaths.m @@ -29,6 +29,7 @@ MDMAnimatableKeyPath MDMKeyPathShadowRadius = @"shadowRadius"; MDMAnimatableKeyPath MDMKeyPathStrokeStart = @"strokeStart"; MDMAnimatableKeyPath MDMKeyPathStrokeEnd = @"strokeEnd"; +MDMAnimatableKeyPath MDMKeyPathTransform = @"transform"; MDMAnimatableKeyPath MDMKeyPathWidth = @"bounds.size.width"; MDMAnimatableKeyPath MDMKeyPathX = @"position.x"; MDMAnimatableKeyPath MDMKeyPathY = @"position.y"; diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index ea3a4fd..e3cf2e7 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -44,6 +44,15 @@ static BOOL IsCGSizeType(id someValue) { return NO; } +static BOOL IsCATransform3DType(id someValue) { + if ([someValue isKindOfClass:[NSValue class]]) { + NSValue *asValue = (NSValue *)someValue; + const char *objCType = @encode(CATransform3D); + return strncmp(asValue.objCType, objCType, strlen(objCType)) == 0; + } + return NO; +} + static BOOL IsAnimationKeyPathAlwaysNonAdditive(NSString *keyPath) { static NSSet *nonAdditiveKeyPaths = nil; static dispatch_once_t onceToken; @@ -100,7 +109,10 @@ BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue) { if (IsAnimationKeyPathAlwaysNonAdditive(keyPath)) { return NO; } - return IsNumberValue(toValue) || IsCGSizeType(toValue) || IsCGPointType(toValue); + return (IsNumberValue(toValue) + || IsCGSizeType(toValue) + || IsCGPointType(toValue) + || IsCATransform3DType(toValue)); } void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) { @@ -263,6 +275,16 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) springAnimation.initialVelocity = absoluteInitialVelocity / displacement; } } + + } else if (IsCATransform3DType(animation.toValue)) { + CATransform3D from = [animation.fromValue CATransform3DValue]; + CATransform3D to = [animation.toValue CATransform3DValue]; + + if (animation.additive) { + CATransform3D divisor = CATransform3DInvert(to); + animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DConcat(from, divisor)]; + animation.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; + } } // Update the animation's duration to match the proposed settling duration. diff --git a/src/private/MDMBlockAnimations.m b/src/private/MDMBlockAnimations.m index 90c7929..bfa0ad8 100644 --- a/src/private/MDMBlockAnimations.m +++ b/src/private/MDMBlockAnimations.m @@ -40,6 +40,7 @@ MDMKeyPathShadowRadius, MDMKeyPathStrokeStart, MDMKeyPathStrokeEnd, + MDMKeyPathTransform, MDMKeyPathWidth, MDMKeyPathX, MDMKeyPathY]]; diff --git a/src/private/MDMUIKitValueCoercion.m b/src/private/MDMUIKitValueCoercion.m index 6021dc2..2ac9f40 100644 --- a/src/private/MDMUIKitValueCoercion.m +++ b/src/private/MDMUIKitValueCoercion.m @@ -18,6 +18,15 @@ #import +static BOOL IsCGAffineTransformType(id someValue) { + if ([someValue isKindOfClass:[NSValue class]]) { + NSValue *asValue = (NSValue *)someValue; + const char *objCType = @encode(CGAffineTransform); + return strncmp(asValue.objCType, objCType, strlen(objCType)) == 0; + } + return NO; +} + NSArray* MDMCoerceUIKitValuesToCoreAnimationValues(NSArray *values) { if ([[values firstObject] isKindOfClass:[UIColor class]]) { NSMutableArray *convertedArray = [NSMutableArray arrayWithCapacity:values.count]; @@ -32,6 +41,14 @@ [convertedArray addObject:(id)bezierPath.CGPath]; } values = convertedArray; + + } else if (IsCGAffineTransformType([values firstObject])) { + NSMutableArray *convertedArray = [NSMutableArray arrayWithCapacity:values.count]; + for (NSValue *value in values) { + CATransform3D asTransform3D = CATransform3DMakeAffineTransform(value.CGAffineTransformValue); + [convertedArray addObject:[NSValue valueWithCATransform3D:asTransform3D]]; + } + values = convertedArray; } return values; } diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index f53b36c..5789f0c 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -59,6 +59,7 @@ class AnimatorBehavioralTests: XCTestCase { .shadowRadius: 5, .strokeStart: 0.2, .strokeEnd: 0.5, + .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), .width: 25, .x: 12, .y: 23, diff --git a/tests/unit/MotionAnimatorTests.swift b/tests/unit/MotionAnimatorTests.swift index 6a6322e..c6649d6 100644 --- a/tests/unit/MotionAnimatorTests.swift +++ b/tests/unit/MotionAnimatorTests.swift @@ -50,6 +50,9 @@ class MotionAnimatorTests: XCTestCase { animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .shadowRadius) animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .strokeStart) animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .strokeEnd) + animator.animate(with: timing, to: layer, + withValues: [CGAffineTransform(rotationAngle: 12), + CGAffineTransform(rotationAngle: 50)], keyPath: .transform) animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .width) animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .x) animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .y) diff --git a/tests/unit/QuartzCoreBehavioralTests.swift b/tests/unit/QuartzCoreBehavioralTests.swift index db09318..032aff5 100644 --- a/tests/unit/QuartzCoreBehavioralTests.swift +++ b/tests/unit/QuartzCoreBehavioralTests.swift @@ -86,6 +86,7 @@ class QuartzCoreBehavioralTests: XCTestCase { .shadowRadius: 5, .strokeStart: 0.2, .strokeEnd: 0.5, + .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), .width: 25, .x: 12, .y: 23, diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index 2bba001..c651de0 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -77,6 +77,7 @@ class UIKitBehavioralTests: XCTestCase { .position: CGPoint(x: 50, y: 20), .rotation: 42, .scale: 2.5, + .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), .width: 25, .x: 12, .y: 23, @@ -163,6 +164,7 @@ class UIKitBehavioralTests: XCTestCase { .shadowRadius: 5, .strokeStart: 0.2, .strokeEnd: 0.5, + .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), .width: 25, .x: 12, .y: 23, From be7f9081c0678484e034cc976aafcdab748b58bf Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 4 Dec 2017 12:40:20 -0500 Subject: [PATCH 02/11] Update with ObjC implementation. --- Podfile | 1 + Podfile.lock | 9 +- examples/CalendarCardExpansionExample.m | 34 ++--- examples/CalendarChipMotionSpec.h | 24 ++-- examples/CalendarChipMotionSpec.m | 126 +++++++++++------- examples/TapToBounceExample.swift | 13 +- src/MDMMotionAnimator.h | 33 ++--- src/MDMMotionAnimator.m | 28 ++-- src/private/CABasicAnimation+MotionAnimator.h | 6 +- src/private/CABasicAnimation+MotionAnimator.m | 114 +++++++--------- .../CAMediaTimingFunction+MotionAnimator.h | 2 +- tests/unit/AdditiveAnimatorTests.swift | 15 +-- tests/unit/AnimationRemovalTests.swift | 17 +-- tests/unit/BeginFromCurrentStateTests.swift | 25 ++-- .../HeadlessLayerImplicitAnimationTests.swift | 22 ++- tests/unit/ImplicitAnimationTests.swift | 120 +++++++++-------- tests/unit/InitialVelocityTests.swift | 13 +- tests/unit/InstantAnimationTests.swift | 37 +++-- .../unit/MotionAnimatorBehavioralTests.swift | 17 +-- tests/unit/MotionAnimatorTests.m | 60 ++++----- tests/unit/MotionAnimatorTests.swift | 57 ++++---- tests/unit/NonAdditiveAnimatorTests.swift | 17 +-- tests/unit/TimeScaleFactorTests.swift | 23 ++-- 23 files changed, 410 insertions(+), 403 deletions(-) diff --git a/Podfile b/Podfile index 7936bb7..661aa41 100644 --- a/Podfile +++ b/Podfile @@ -5,6 +5,7 @@ target "MotionAnimatorCatalog" do pod 'CatalogByConvention' pod 'MotionAnimator', :path => './' project 'examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj' + pod 'MotionInterchange', :path => '../motion-interchange-objc/' end target "UnitTests" do diff --git a/Podfile.lock b/Podfile.lock index 2bbf1ff..21fa606 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,21 +2,24 @@ PODS: - CatalogByConvention (2.2.0) - MotionAnimator (2.6.0): - MotionInterchange (~> 1.3) - - MotionInterchange (1.3.0) + - MotionInterchange (1.4.0) DEPENDENCIES: - CatalogByConvention - MotionAnimator (from `./`) + - MotionInterchange (from `../motion-interchange-objc/`) EXTERNAL SOURCES: MotionAnimator: :path: ./ + MotionInterchange: + :path: ../motion-interchange-objc/ SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f MotionAnimator: a4b0ba87a674bb3e89e25f0530b7e80a204ac1c1 - MotionInterchange: 988fc0011e4b806cc33f2fb4f9566f5eeb4159e8 + MotionInterchange: 35e0fd286ceab53dd4ee03494b3fcafa6a70637a -PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3 +PODFILE CHECKSUM: f354f45cd3f9eb0e6ac9a2bfd9429945eae8c0ad COCOAPODS: 1.3.1 diff --git a/examples/CalendarCardExpansionExample.m b/examples/CalendarCardExpansionExample.m index f424c33..d4826fb 100644 --- a/examples/CalendarCardExpansionExample.m +++ b/examples/CalendarCardExpansionExample.m @@ -20,12 +20,12 @@ #import "MotionAnimator.h" -// This example demonstrates how to use a motion timing specification to build a complex +// This example demonstrates how to use a motion traits specification to build a complex // bi-directional animation using the MDMMotionAnimator object. MDMMotionAnimator is designed for // building fine-tuned explicit animations. Unlike UIView's implicit animation API, which can be // used to cause cascading animations on a variety of properties, MDMMotionAnimator will always add // exactly one animation per key path to the layer. This means you don't get as much for "free", but -// you do gain more control over the timing and motion of the animation. +// you do gain more control over the traits and motion of the animation. @implementation CalendarCardExpansionExampleViewController { // In a real-world scenario we'd likely create a separate view to manage all of these subviews so @@ -40,15 +40,15 @@ @implementation CalendarCardExpansionExampleViewController { - (void)didTap { _expanded = !_expanded; - CalendarChipTiming timing = (_expanded - ? CalendarChipMotionSpec.expansion - : CalendarChipMotionSpec.collapse); + id traits = (_expanded + ? CalendarChipMotionSpec.expansion + : CalendarChipMotionSpec.collapse); MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; animator.shouldReverseValues = !_expanded; animator.beginFromCurrentState = YES; - [animator animateWithTiming:timing.navigationBarY animations:^{ + [animator animateWithTraits:traits.navigationBarY animations:^{ [self.navigationController setNavigationBarHidden:_expanded animated:YES]; }]; @@ -56,33 +56,33 @@ - (void)didTap { CGRect headerFrame = [self frameForHeader]; // Animate the chip itself. - [animator animateWithTiming:timing.chipHeight + [animator animateWithTraits:traits.chipHeight toLayer:_chipView.layer withValues:@[ @(chipFrame.size.height), @(headerFrame.size.height) ] keyPath:MDMKeyPathHeight]; - [animator animateWithTiming:timing.chipWidth + [animator animateWithTraits:traits.chipWidth toLayer:_chipView.layer withValues:@[ @(chipFrame.size.width), @(headerFrame.size.width) ] keyPath:MDMKeyPathWidth]; - [animator animateWithTiming:timing.chipWidth + [animator animateWithTraits:traits.chipWidth toLayer:_chipView.layer withValues:@[ @(CGRectGetMidX(chipFrame)), @(CGRectGetMidX(headerFrame)) ] keyPath:MDMKeyPathX]; - [animator animateWithTiming:timing.chipY + [animator animateWithTraits:traits.chipY toLayer:_chipView.layer withValues:@[ @(CGRectGetMidY(chipFrame)), @(CGRectGetMidY(headerFrame)) ] keyPath:MDMKeyPathY]; - [animator animateWithTiming:timing.chipHeight + [animator animateWithTraits:traits.chipHeight toLayer:_chipView.layer withValues:@[ @([self chipCornerRadius]), @0 ] keyPath:MDMKeyPathCornerRadius]; // Cross-fade the chip's contents. - [animator animateWithTiming:timing.chipContentOpacity + [animator animateWithTraits:traits.chipContentOpacity toLayer:_collapsedContent.layer withValues:@[ @1, @0 ] keyPath:MDMKeyPathOpacity]; - [animator animateWithTiming:timing.headerContentOpacity + [animator animateWithTraits:traits.headerContentOpacity toLayer:_expandedContent.layer withValues:@[ @0, @1 ] keyPath:MDMKeyPathOpacity]; @@ -90,7 +90,7 @@ - (void)didTap { // Keeps the expandec content aligned to the bottom of the card by taking into consideration the // extra height. CGFloat excessTopMargin = chipFrame.size.height - headerFrame.size.height; - [animator animateWithTiming:timing.chipHeight + [animator animateWithTraits:traits.chipHeight toLayer:_expandedContent.layer withValues:@[ @(CGRectGetMidY([self expandedContentFrame]) + excessTopMargin), @(CGRectGetMidY([self expandedContentFrame])) ] @@ -99,7 +99,7 @@ - (void)didTap { // Keeps the collapsed content aligned to its position on screen by taking into consideration the // excess left margin. CGFloat excessLeftMargin = chipFrame.origin.x - headerFrame.origin.x; - [animator animateWithTiming:timing.chipWidth + [animator animateWithTraits:traits.chipWidth toLayer:_collapsedContent.layer withValues:@[ @(CGRectGetMidX([self collapsedContentFrame])), @(CGRectGetMidX([self collapsedContentFrame]) + excessLeftMargin) ] @@ -108,11 +108,11 @@ - (void)didTap { // Keeps the shape anchored to the bottom right of the chip. CGRect shapeFrameInChip = [self shapeFrameInRect:chipFrame]; CGRect shapeFrameInHeader = [self shapeFrameInRect:headerFrame]; - [animator animateWithTiming:timing.chipWidth + [animator animateWithTraits:traits.chipWidth toLayer:_shapeView.layer withValues:@[ @(CGRectGetMidX(shapeFrameInChip)), @(CGRectGetMidX(shapeFrameInHeader)) ] keyPath:MDMKeyPathX]; - [animator animateWithTiming:timing.chipHeight + [animator animateWithTraits:traits.chipHeight toLayer:_shapeView.layer withValues:@[ @(CGRectGetMidY(shapeFrameInChip)), @(CGRectGetMidY(shapeFrameInHeader)) ] keyPath:MDMKeyPathY]; diff --git a/examples/CalendarChipMotionSpec.h b/examples/CalendarChipMotionSpec.h index 019160b..d7989f2 100644 --- a/examples/CalendarChipMotionSpec.h +++ b/examples/CalendarChipMotionSpec.h @@ -17,24 +17,26 @@ #import #import -typedef struct CalendarChipTiming { - MDMMotionTiming chipWidth; - MDMMotionTiming chipHeight; - MDMMotionTiming chipY; +@protocol CalendarChipTiming - MDMMotionTiming chipContentOpacity; - MDMMotionTiming headerContentOpacity; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipWidth; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipHeight; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipY; - MDMMotionTiming navigationBarY; -} CalendarChipTiming; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipContentOpacity; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *headerContentOpacity; + +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *navigationBarY; + +@end @interface CalendarChipMotionSpec: NSObject -@property(nonatomic, class, readonly) CalendarChipTiming expansion; -@property(nonatomic, class, readonly) CalendarChipTiming collapse; +@property(nonatomic, class, strong, nonnull, readonly) id expansion; +@property(nonatomic, class, strong, nonnull, readonly) id collapse; // This object is not meant to be instantiated. -- (instancetype)init NS_UNAVAILABLE; +- (nonnull instancetype)init NS_UNAVAILABLE; @end diff --git a/examples/CalendarChipMotionSpec.m b/examples/CalendarChipMotionSpec.m index b0ef2b8..51bffc3 100644 --- a/examples/CalendarChipMotionSpec.m +++ b/examples/CalendarChipMotionSpec.m @@ -16,58 +16,84 @@ #import "CalendarChipMotionSpec.h" +static id StandardTimingCurve(void) { + return [CAMediaTimingFunction functionWithControlPoints:0.4f :0.0f :0.2f :1.0f]; +} + +static id LinearTimingCurve(void) { + return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; +} + +@interface CalendarChipExpansionTiming: NSObject +@end + +@implementation CalendarChipExpansionTiming + +- (MDMAnimationTraits *)chipWidth { + return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.285 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipHeight { + return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipY { + return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipContentOpacity { + return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.075 timingCurve:LinearTimingCurve()]; +} + +- (MDMAnimationTraits *)headerContentOpacity { + return [[MDMAnimationTraits alloc] initWithDelay:0.075 duration:0.150 timingCurve:LinearTimingCurve()]; +} + +- (MDMAnimationTraits *)navigationBarY { + return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; +} + +@end + +@interface CalendarChipCollapseTiming: NSObject +@end + +@implementation CalendarChipCollapseTiming + +- (MDMAnimationTraits *)chipWidth { + return [[MDMAnimationTraits alloc] initWithDelay:0.045 duration:0.330 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipHeight { + return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.330 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipY { + return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.330 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipContentOpacity { + return [[MDMAnimationTraits alloc] initWithDelay:0.150 duration:0.150 timingCurve:LinearTimingCurve()]; +} + +- (MDMAnimationTraits *)headerContentOpacity { + return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.075 timingCurve:LinearTimingCurve()]; +} + +- (MDMAnimationTraits *)navigationBarY { + return [[MDMAnimationTraits alloc] initWithDelay:0.045 duration:0.150 timingCurve:StandardTimingCurve()]; +} + +@end + @implementation CalendarChipMotionSpec -+ (MDMMotionCurve)eightyForty { - return MDMMotionCurveMakeBezier(0.4f, 0.0f, 0.2f, 1.0f); -} - -+ (CalendarChipTiming)expansion { - MDMMotionCurve eightyForty = [self eightyForty]; - return (CalendarChipTiming){ - .chipWidth = { - .delay = 0.000, .duration = 0.285, .curve = eightyForty, - }, - .chipHeight = { - .delay = 0.015, .duration = 0.360, .curve = eightyForty, - }, - .chipY = { - .delay = 0.015, .duration = 0.360, .curve = eightyForty, - }, - .chipContentOpacity = { - .delay = 0.000, .duration = 0.075, .curve = MDMLinearMotionCurve, - }, - .headerContentOpacity = { - .delay = 0.075, .duration = 0.150, .curve = MDMLinearMotionCurve, - }, - .navigationBarY = { - .delay = 0.015, .duration = 0.360, .curve = eightyForty, - }, - }; -} - -+ (CalendarChipTiming)collapse { - MDMMotionCurve eightyForty = [self eightyForty]; - return (CalendarChipTiming){ - .chipWidth = { - .delay = 0.045, .duration = 0.330, .curve = eightyForty, - }, - .chipHeight = { - .delay = 0.000, .duration = 0.330, .curve = eightyForty, - }, - .chipY = { - .delay = 0.015, .duration = 0.330, .curve = eightyForty, - }, - .chipContentOpacity = { - .delay = 0.150, .duration = 0.150, .curve = MDMLinearMotionCurve, - }, - .headerContentOpacity = { - .delay = 0.000, .duration = 0.075, .curve = MDMLinearMotionCurve, - }, - .navigationBarY = { - .delay = 0.045, .duration = 0.150, .curve = eightyForty, - } - }; ++ (id)expansion { + return [[CalendarChipExpansionTiming alloc] init]; +} + ++ (id)collapse { + return [[CalendarChipCollapseTiming alloc] init]; } @end diff --git a/examples/TapToBounceExample.swift b/examples/TapToBounceExample.swift index c4db306..0974145 100644 --- a/examples/TapToBounceExample.swift +++ b/examples/TapToBounceExample.swift @@ -40,21 +40,22 @@ class TapToBounceExampleViewController: UIViewController { for: [.touchUpInside, .touchUpOutside, .touchDragExit]) } - let timing = MotionTiming(delay: 0, - duration: 0.5, - curve: MotionCurveMakeSpring(mass: 1, tension: 100, friction: 10), - repetition: .init()) + let traits = MDMAnimationTraits(delay: 0, + duration: 0.5, + timingCurve: MDMSpringTimingCurve(mass: 1, + tension: 100, + friction: 10)) func didFocus(_ sender: UIButton) { let animator = MotionAnimator() - animator.animate(with: timing) { + animator.animate(with: traits) { sender.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) } } func didUnfocus(_ sender: UIButton) { let animator = MotionAnimator() - animator.animate(with: timing) { + animator.animate(with: traits) { sender.transform = .identity } } diff --git a/src/MDMMotionAnimator.h b/src/MDMMotionAnimator.h index 1ac537d..151ce7b 100644 --- a/src/MDMMotionAnimator.h +++ b/src/MDMMotionAnimator.h @@ -27,7 +27,7 @@ #import "MDMCoreAnimationTraceable.h" /** - An animator adds Core Animation animations to a layer based on a provided motion timing. + An animator adds Core Animation animations to a layer based on a provided motion traits. */ NS_SWIFT_NAME(MotionAnimator) @interface MDMMotionAnimator : NSObject @@ -44,7 +44,7 @@ NS_SWIFT_NAME(MotionAnimator) /** If enabled, explicitly-provided values will be reversed before animating. - This property does not affect the animateWithTiming:animations: family of methods. + This property does not affect the animateWithTraits:animations: family of methods. Disabled by default. */ @@ -70,32 +70,32 @@ NS_SWIFT_NAME(MotionAnimator) @property(nonatomic, assign) BOOL additive; /** - Adds a single animation to the layer with the given timing structure. + Adds a single animation to the layer with the given traits structure. If `additive` is disabled, the animation will be added to the layer with the keyPath as its key. In this case, multiple invocations of this function on the same key path will remove the animations added from prior invocations. - @param timing The timing to be used for the animation. + @param traits The traits to be used for the animation. @param layer The layer to be animated. @param values The values to be used in the animation. Must contain exactly two values. Supported UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include UIColor and UIBezierPath. @param keyPath The key path of the property to be animated. */ -- (void)animateWithTiming:(MDMMotionTiming)timing +- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits toLayer:(nonnull CALayer *)layer withValues:(nonnull NSArray *)values keyPath:(nonnull MDMAnimatableKeyPath)keyPath; /** - Adds a single animation to the layer with the given timing structure. + Adds a single animation to the layer with the given traits structure. If `additive` is disabled, the animation will be added to the layer with the keyPath as its key. In this case, multiple invocations of this function on the same key path will remove the animations added from prior invocations. - @param timing The timing to be used for the animation. + @param traits The traits to be used for the animation. @param layer The layer to be animated. @param values The values to be used in the animation. Must contain exactly two values. Supported UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include @@ -105,35 +105,36 @@ NS_SWIFT_NAME(MotionAnimator) animation hierarchy. If the duration of the animation is 0, this block is executed immediately. The block is escaping and will be released once the animations have completed. */ -- (void)animateWithTiming:(MDMMotionTiming)timing +- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits toLayer:(nonnull CALayer *)layer withValues:(nonnull NSArray *)values keyPath:(nonnull MDMAnimatableKeyPath)keyPath completion:(nullable void(^)(void))completion; /** - Performs `animations` using the timing provided. + Performs `animations` using the traits provided. - @param timing The timing to be used for the animation. + @param traits The traits to be used for the animation. @param animations The block to be executed. Any animatable properties changed within this block - will result in animations being added to the view's layer with the provided timing. The block is + will result in animations being added to the view's layer with the provided traits. The block is non-escaping. */ -- (void)animateWithTiming:(MDMMotionTiming)timing animations:(nonnull void(^)(void))animations; +- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits + animations:(nonnull void(^)(void))animations; /** - Performs `animations` using the timing provided and executes the completion handler once all added + Performs `animations` using the traits provided and executes the completion handler once all added animations have completed. - @param timing The timing to be used for the animation. + @param traits The traits to be used for the animation. @param animations The block to be executed. Any animatable properties changed within this block - will result in animations being added to the view's layer with the provided timing. The block is + will result in animations being added to the view's layer with the provided traits. The block is non-escaping. @param completion A block object to be executed once the animation sequence ends or it has been removed from the animation hierarchy. If the duration of the animation is 0, this block is executed immediately. The block is escaping and will be released once the animation sequence has completed. */ -- (void)animateWithTiming:(MDMMotionTiming)timing +- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits animations:(nonnull void (^)(void))animations completion:(nullable void(^)(void))completion; diff --git a/src/MDMMotionAnimator.m b/src/MDMMotionAnimator.m index f4a4c0f..a2709d7 100644 --- a/src/MDMMotionAnimator.m +++ b/src/MDMMotionAnimator.m @@ -40,14 +40,14 @@ - (instancetype)init { return self; } -- (void)animateWithTiming:(MDMMotionTiming)timing +- (void)animateWithTraits:(MDMAnimationTraits *)traits toLayer:(CALayer *)layer withValues:(NSArray *)values keyPath:(MDMAnimatableKeyPath)keyPath { - [self animateWithTiming:timing toLayer:layer withValues:values keyPath:keyPath completion:nil]; + [self animateWithTraits:traits toLayer:layer withValues:values keyPath:keyPath completion:nil]; } -- (void)animateWithTiming:(MDMMotionTiming)timing +- (void)animateWithTraits:(MDMAnimationTraits *)traits toLayer:(CALayer *)layer withValues:(NSArray *)values keyPath:(MDMAnimatableKeyPath)keyPath @@ -80,7 +80,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing return; } - CABasicAnimation *animation = MDMAnimationFromTiming(timing, timeScaleFactor); + CABasicAnimation *animation = MDMAnimationFromTiming(traits, timeScaleFactor); if (animation == nil) { exitEarly(); @@ -92,7 +92,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing [self addAnimation:animation toLayer:layer withKeyPath:keyPath - timing:timing + traits:traits timeScaleFactor:timeScaleFactor destination:[values lastObject] initialValue:^(BOOL wantsPresentationValue) { @@ -115,11 +115,11 @@ - (void)animateWithTiming:(MDMMotionTiming)timing } } -- (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations { - [self animateWithTiming:timing animations:animations completion:nil]; +- (void)animateWithTraits:(MDMAnimationTraits *)traits animations:(void (^)(void))animations { + [self animateWithTraits:traits animations:animations completion:nil]; } -- (void)animateWithTiming:(MDMMotionTiming)timing +- (void)animateWithTraits:(MDMAnimationTraits *)traits animations:(void (^)(void))animations completion:(void(^)(void))completion { NSArray *actions = MDMAnimateImplicitly(animations); @@ -142,7 +142,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing } // We'll reuse this animation template for each action. - CABasicAnimation *animationTemplate = MDMAnimationFromTiming(timing, timeScaleFactor); + CABasicAnimation *animationTemplate = MDMAnimationFromTiming(traits, timeScaleFactor); if (animationTemplate == nil) { exitEarly(); return; @@ -157,7 +157,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing [self addAnimation:animation toLayer:action.layer withKeyPath:action.keyPath - timing:timing + traits:traits timeScaleFactor:timeScaleFactor destination:[action.layer valueForKeyPath:action.keyPath] initialValue:^(BOOL wantsPresentationValue) { @@ -214,7 +214,7 @@ - (CGFloat)computedTimeScaleFactor { - (void)addAnimation:(CABasicAnimation *)animation toLayer:(CALayer *)layer withKeyPath:(NSString *)keyPath - timing:(MDMMotionTiming)timing + traits:(MDMAnimationTraits *)traits timeScaleFactor:(CGFloat)timeScaleFactor destination:(id)destination initialValue:(id(^)(BOOL wantsPresentationValue))initialValueBlock @@ -237,11 +237,11 @@ - (void)addAnimation:(CABasicAnimation *)animation NSString *key = animation.additive ? nil : keyPath; - MDMConfigureAnimation(animation, timing); + MDMConfigureAnimation(animation, traits); - if (timing.delay != 0) { + if (traits.delay != 0) { animation.beginTime = ([layer convertTime:CACurrentMediaTime() fromLayer:nil] - + timing.delay * timeScaleFactor); + + traits.delay * timeScaleFactor); animation.fillMode = kCAFillModeBackwards; } diff --git a/src/private/CABasicAnimation+MotionAnimator.h b/src/private/CABasicAnimation+MotionAnimator.h index b3e0ce6..4e4fd9a 100644 --- a/src/private/CABasicAnimation+MotionAnimator.h +++ b/src/private/CABasicAnimation+MotionAnimator.h @@ -20,9 +20,9 @@ #import #import -// Returns a basic animation configured with the provided timing and scale factor. +// Returns a basic animation configured with the provided traits and scale factor. FOUNDATION_EXPORT -CABasicAnimation *MDMAnimationFromTiming(MDMMotionTiming timing, CGFloat timeScaleFactor); +CABasicAnimation *MDMAnimationFromTiming(MDMAnimationTraits * traits, CGFloat timeScaleFactor); // Returns a Boolean indicating whether or not an animation with the given key path and toValue // can be animated additively. @@ -33,4 +33,4 @@ FOUNDATION_EXPORT BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue); // // Not all animation value types support being additive. If an animation's value type was not // supported, the animation's values will not be modified. -FOUNDATION_EXPORT void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing); +FOUNDATION_EXPORT void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * traits); diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index e3cf2e7..6d78830 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -65,44 +65,39 @@ static BOOL IsAnimationKeyPathAlwaysNonAdditive(NSString *keyPath) { #pragma mark - Public -CABasicAnimation *MDMAnimationFromTiming(MDMMotionTiming timing, CGFloat timeScaleFactor) { - CABasicAnimation *animation; - switch (timing.curve.type) { - case MDMMotionCurveTypeInstant: - animation = nil; - break; +CABasicAnimation *MDMAnimationFromTiming(MDMAnimationTraits * traits, CGFloat timeScaleFactor) { + if (traits.timingCurve == nil) { + return nil; + } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - case MDMMotionCurveTypeDefault: -#pragma clang diagnostic pop - case MDMMotionCurveTypeBezier: - animation = [CABasicAnimation animation]; - animation.timingFunction = MDMTimingFunctionWithControlPoints(timing.curve.data); - animation.duration = timing.duration * timeScaleFactor; + if ([traits.timingCurve isKindOfClass:[CAMediaTimingFunction class]]) { + CFTimeInterval duration = traits.duration * timeScaleFactor; + if (duration == 0) { + return nil; + } + CABasicAnimation *animation = [CABasicAnimation animation]; + animation.timingFunction = (CAMediaTimingFunction *)traits.timingCurve; + animation.duration = duration; + return animation; + } - if (animation.duration == 0) { - return nil; - } - break; + if ([traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]]) { + MDMSpringTimingCurve *springTiming = (MDMSpringTimingCurve *)traits.timingCurve; - case MDMMotionCurveTypeSpring: { #pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. #pragma clang diagnostic ignored "-Wpartial-availability" - CASpringAnimation *spring = [CASpringAnimation animation]; + CASpringAnimation *animation = [CASpringAnimation animation]; #pragma clang diagnostic pop - spring.mass = timing.curve.data[MDMSpringMotionCurveDataIndexMass]; - spring.stiffness = timing.curve.data[MDMSpringMotionCurveDataIndexTension]; - spring.damping = timing.curve.data[MDMSpringMotionCurveDataIndexFriction]; - spring.duration = timing.duration; - - animation = spring; - break; - } + animation.mass = springTiming.mass; + animation.stiffness = springTiming.tension; + animation.damping = springTiming.friction; + animation.duration = traits.duration; + return animation; } - return animation; + + return nil; } BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue) { @@ -115,8 +110,18 @@ BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue) { || IsCATransform3DType(toValue)); } -void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) { - if (!animation.additive && timing.curve.type != MDMMotionCurveTypeSpring) { +void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * traits) { +#pragma clang diagnostic push + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. +#pragma clang diagnostic ignored "-Wpartial-availability" + BOOL isSpringAnimation = ([animation isKindOfClass:[CASpringAnimation class]] + && [traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]]); + MDMSpringTimingCurve *springTimingCurve = (MDMSpringTimingCurve *)traits.timingCurve; + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop + + if (!animation.additive && !isSpringAnimation) { return; // Nothing to do here. } @@ -158,17 +163,10 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) animation.toValue = @0; } -#pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. -#pragma clang diagnostic ignored "-Wpartial-availability" - if ([animation isKindOfClass:[CASpringAnimation class]]) { - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop - - CGFloat absoluteInitialVelocity = timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; + if (isSpringAnimation) { + CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; - // Our timing's initialVelocity is in points per second, but Core Animation expects initial + // Our traits's initialVelocity is in points per second, but Core Animation expects initial // velocity to be in terms of displacement per second. // // From the UIView animateWithDuration header docs: @@ -220,13 +218,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) animation.toValue = [NSValue valueWithCGSize:CGSizeZero]; } -#pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. -#pragma clang diagnostic ignored "-Wpartial-availability" - if ([animation isKindOfClass:[CASpringAnimation class]]) { - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop + if (isSpringAnimation) { // Core Animation's velocity system is single dimensional, so we pick the dominant direction // of movement and normalize accordingly. CGFloat biggestDelta; @@ -236,8 +228,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) biggestDelta = additiveDisplacement.height; } CGFloat displacement = -biggestDelta; - CGFloat absoluteInitialVelocity = - timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; + CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; if (fabs(displacement) > 0.00001) { springAnimation.initialVelocity = absoluteInitialVelocity / displacement; } @@ -253,13 +244,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) animation.toValue = [NSValue valueWithCGPoint:CGPointZero]; } -#pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. -#pragma clang diagnostic ignored "-Wpartial-availability" - if ([animation isKindOfClass:[CASpringAnimation class]]) { - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop + if (isSpringAnimation) { // Core Animation's velocity system is single dimensional, so we pick the dominant direction // of movement and normalize accordingly. CGFloat biggestDelta; @@ -269,8 +254,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) biggestDelta = additiveDisplacement.y; } CGFloat displacement = -biggestDelta; - CGFloat absoluteInitialVelocity = - timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; + CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; if (fabs(displacement) > 0.00001) { springAnimation.initialVelocity = absoluteInitialVelocity / displacement; } @@ -287,13 +271,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) } } - // Update the animation's duration to match the proposed settling duration. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - if ([animation isKindOfClass:[CASpringAnimation class]]) { - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop - + if (isSpringAnimation) { // This API is only available on iOS 9+ if ([springAnimation respondsToSelector:@selector(settlingDuration)]) { animation.duration = springAnimation.settlingDuration; diff --git a/src/private/CAMediaTimingFunction+MotionAnimator.h b/src/private/CAMediaTimingFunction+MotionAnimator.h index 8790721..e1e1803 100644 --- a/src/private/CAMediaTimingFunction+MotionAnimator.h +++ b/src/private/CAMediaTimingFunction+MotionAnimator.h @@ -18,6 +18,6 @@ #import #import -// Returns a timing function with the given control points. +// Returns a traits function with the given control points. FOUNDATION_EXPORT CAMediaTimingFunction* MDMTimingFunctionWithControlPoints(CGFloat controlPoints[4]); diff --git a/tests/unit/AdditiveAnimatorTests.swift b/tests/unit/AdditiveAnimatorTests.swift index f4e60ea..c5855ba 100644 --- a/tests/unit/AdditiveAnimatorTests.swift +++ b/tests/unit/AdditiveAnimatorTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class AdditiveAnimationTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! override func setUp() { @@ -33,10 +33,7 @@ class AdditiveAnimationTests: XCTestCase { animator.additive = true - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 1, p2y: 1), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -49,14 +46,14 @@ class AdditiveAnimationTests: XCTestCase { override func tearDown() { animator = nil - timing = nil + traits = nil view = nil super.tearDown() } func testNumericKeyPathsAnimateAdditively() { - animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: traits, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -75,7 +72,7 @@ class AdditiveAnimationTests: XCTestCase { } func testCGSizeKeyPathsAnimateAdditively() { - animator.animate(with: timing, to: view.layer, + animator.animate(with: traits, to: view.layer, withValues: [CGSize(width: 0, height: 0), CGSize(width: 1, height: 2)], keyPath: .shadowOffset) @@ -96,7 +93,7 @@ class AdditiveAnimationTests: XCTestCase { } func testCGPointKeyPathsAnimateAdditively() { - animator.animate(with: timing, to: view.layer, + animator.animate(with: traits, to: view.layer, withValues: [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 2)], keyPath: .position) diff --git a/tests/unit/AnimationRemovalTests.swift b/tests/unit/AnimationRemovalTests.swift index 197c8d2..70aa766 100644 --- a/tests/unit/AnimationRemovalTests.swift +++ b/tests/unit/AnimationRemovalTests.swift @@ -24,7 +24,7 @@ import MotionAnimator class AnimationRemovalTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! var originalImplementation: IMP? @@ -32,10 +32,7 @@ class AnimationRemovalTests: XCTestCase { super.setUp() animator = MotionAnimator() - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -48,15 +45,15 @@ class AnimationRemovalTests: XCTestCase { override func tearDown() { animator = nil - timing = nil + traits = nil view = nil super.tearDown() } func testAllAdditiveAnimationsGetsRemoved() { - animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) + animator.animate(with: traits, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) XCTAssertEqual(view.layer.animationKeys()!.count, 2) @@ -73,8 +70,8 @@ class AnimationRemovalTests: XCTestCase { didComplete = true } - animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) + animator.animate(with: traits, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) CATransaction.commit() diff --git a/tests/unit/BeginFromCurrentStateTests.swift b/tests/unit/BeginFromCurrentStateTests.swift index 7117089..20655cd 100644 --- a/tests/unit/BeginFromCurrentStateTests.swift +++ b/tests/unit/BeginFromCurrentStateTests.swift @@ -24,7 +24,7 @@ import MotionAnimator class BeginFromCurrentStateTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! var addedAnimations: [CAAnimation]! @@ -35,10 +35,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.beginFromCurrentState = true - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -56,7 +53,7 @@ class BeginFromCurrentStateTests: XCTestCase { override func tearDown() { animator = nil - timing = nil + traits = nil view = nil addedAnimations = nil @@ -68,7 +65,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.additive = false - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -103,7 +100,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.additive = false - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0.5 } @@ -138,7 +135,7 @@ class BeginFromCurrentStateTests: XCTestCase { func testExplicitlyAnimatesFromPresentationValue() { animator.additive = false - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) RunLoop.main.run(until: .init(timeIntervalSinceNow: 0.01)) XCTAssertNotNil(view.layer.presentation(), "No presentation layer found.") @@ -147,7 +144,7 @@ class BeginFromCurrentStateTests: XCTestCase { } let initialValue = presentation.opacity - animator.animate(with: timing, to: view.layer, withValues: [0, 0.2], keyPath: .opacity) + animator.animate(with: traits, to: view.layer, withValues: [0, 0.2], keyPath: .opacity) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -180,7 +177,7 @@ class BeginFromCurrentStateTests: XCTestCase { func testImplicitlyAnimatesFromPresentationValue() { animator.additive = false - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) RunLoop.main.run(until: .init(timeIntervalSinceNow: 0.01)) @@ -190,7 +187,7 @@ class BeginFromCurrentStateTests: XCTestCase { } let initialValue = presentation.opacity - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0.2 } @@ -226,7 +223,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.beginFromCurrentState = true animator.additive = false - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0.5 } @@ -234,7 +231,7 @@ class BeginFromCurrentStateTests: XCTestCase { let initialValue = view.layer.presentation()!.opacity - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 1.0 } diff --git a/tests/unit/HeadlessLayerImplicitAnimationTests.swift b/tests/unit/HeadlessLayerImplicitAnimationTests.swift index 7b0e4f6..bf6862b 100644 --- a/tests/unit/HeadlessLayerImplicitAnimationTests.swift +++ b/tests/unit/HeadlessLayerImplicitAnimationTests.swift @@ -97,7 +97,7 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { // Verifies the somewhat counter-intuitive fact that CATransaction's animation duration always // takes precedence over UIView's animation duration. This means that animating a headless layer - // using UIView animation APIs may not result in the expected timings. + // using UIView animation APIs may not result in the expected traitss. func testCATransactionTimingTakesPrecedenceOverUIViewTimingOutside() { CATransaction.begin() CATransaction.setAnimationDuration(0.2) @@ -146,18 +146,19 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { func testAnimatorTimingTakesPrecedenceOverCATransactionTiming() { let animator = MotionAnimator() animator.additive = false - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) - animator.animate(with: timing) { + let traits = MDMAnimationTraits(duration: 1) + + CATransaction.begin() + CATransaction.setAnimationDuration(0.5) + animator.animate(with: traits) { self.layer.opacity = 0.5 } + CATransaction.commit() let animation = layer.animation(forKey: "opacity") as! CABasicAnimation XCTAssertEqual(animation.keyPath, "opacity") - XCTAssertEqual(animation.duration, timing.duration) + XCTAssertEqual(animation.duration, traits.duration) } // MARK: Deprecated tests. @@ -204,12 +205,9 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { let animator = MotionAnimator() animator.additive = false - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) - animator.animate(with: timing) { + animator.animate(with: traits) { self.layer.opacity = 0.5 } diff --git a/tests/unit/ImplicitAnimationTests.swift b/tests/unit/ImplicitAnimationTests.swift index 80eab59..61cf5ec 100644 --- a/tests/unit/ImplicitAnimationTests.swift +++ b/tests/unit/ImplicitAnimationTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class ImplicitAnimationTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! var addedAnimations: [CAAnimation]! @@ -34,10 +34,7 @@ class ImplicitAnimationTests: XCTestCase { animator = MotionAnimator() animator.additive = false - timing = MotionTiming(delay: 0, - duration: 0.7, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 1, p2y: 1), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -69,7 +66,7 @@ class ImplicitAnimationTests: XCTestCase { } func testNoActionAddsNoAnimations() { - animator.animate(with: timing) { + animator.animate(with: traits) { // No-op } @@ -77,7 +74,7 @@ class ImplicitAnimationTests: XCTestCase { } func testOneActionAddsOneAnimation() { - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 } @@ -86,18 +83,21 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.opacity.rawValue) XCTAssertEqual(animation.fromValue as! CGFloat, 1) XCTAssertEqual(animation.toValue as! CGFloat, 0) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } func testTwoActionsAddsTwoAnimations() { - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 self.view.center = .init(x: 50, y: 50) } @@ -110,14 +110,17 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.opacity.rawValue) XCTAssertEqual(animation.fromValue as! CGFloat, 1) XCTAssertEqual(animation.toValue as! CGFloat, 0) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } do { let animation = addedAnimations[1] as! CABasicAnimation @@ -125,19 +128,22 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.position.rawValue) XCTAssertEqual(animation.fromValue as! CGPoint, .init(x: 0, y: 0)) XCTAssertEqual(animation.toValue as! CGPoint, .init(x: 50, y: 50)) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } } func testFrameActionAddsTwoAnimations() { - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.frame = .init(x: 0, y: 0, width: 100, height: 100) } @@ -150,14 +156,17 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertFalse(animation.isAdditive) XCTAssertEqual(animation.fromValue as! CGPoint, .init(x: 0, y: 0)) XCTAssertEqual(animation.toValue as! CGPoint, .init(x: 50, y: 50)) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } do { let animation = addedAnimations @@ -166,14 +175,17 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertFalse(animation.isAdditive) XCTAssertEqual(animation.fromValue as! CGRect, .init(x: 0, y: 0, width: 0, height: 0)) XCTAssertEqual(animation.toValue as! CGRect, .init(x: 0, y: 0, width: 100, height: 100)) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } } @@ -181,7 +193,7 @@ class ImplicitAnimationTests: XCTestCase { CATransaction.begin() CATransaction.setDisableActions(true) - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 } @@ -211,9 +223,9 @@ class ImplicitAnimationTests: XCTestCase { } func testDurationOfZeroRunsAnimationsBlockButGeneratesNoAnimations() { - timing.duration = 0 + let traits = MDMAnimationTraits(duration: 0) - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 } @@ -224,7 +236,7 @@ class ImplicitAnimationTests: XCTestCase { func testTimeScaleFactorOfZeroRunsAnimationsBlockButGeneratesNoAnimations() { animator.timeScaleFactor = 0 - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 } @@ -233,7 +245,7 @@ class ImplicitAnimationTests: XCTestCase { } func testUnsupportedAnimationKeyIsNotAnimated() { - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.layer.sublayers = [] } diff --git a/tests/unit/InitialVelocityTests.swift b/tests/unit/InitialVelocityTests.swift index 06378f9..d7cc74f 100644 --- a/tests/unit/InitialVelocityTests.swift +++ b/tests/unit/InitialVelocityTests.swift @@ -135,16 +135,15 @@ class InitialVelocityTests: XCTestCase { } private func animate(from: CGFloat, to: CGFloat, withVelocity velocity: CGFloat) { - let timing = MotionTiming(delay: 0, - duration: 0.7, - curve: .init(type: .spring, data: (1, 1, 1, velocity)), - repetition: .init(type: .none, amount: 0, autoreverses: false)) - animator.animate(with: timing, to: CALayer(), withValues: [from, to], + let springCurve = MDMSpringTimingCurve(mass: 1, tension: 1, friction: 1, + initialVelocity: velocity) + let traits = MDMAnimationTraits(delay: 0, duration: 0.7, timingCurve: springCurve) + animator.animate(with: traits, to: CALayer(), withValues: [from, to], keyPath: .opacity) - animator.animate(with: timing, to: CALayer(), withValues: [CGPoint(x: from, y: from), + animator.animate(with: traits, to: CALayer(), withValues: [CGPoint(x: from, y: from), CGPoint(x: to, y: to)], keyPath: .position) - animator.animate(with: timing, to: CALayer(), withValues: [CGSize(width: from, height: from), + animator.animate(with: traits, to: CALayer(), withValues: [CGSize(width: from, height: from), CGSize(width: to, height: to)], keyPath: .init(rawValue: "bounds.size")) } diff --git a/tests/unit/InstantAnimationTests.swift b/tests/unit/InstantAnimationTests.swift index 3a40426..281778b 100644 --- a/tests/unit/InstantAnimationTests.swift +++ b/tests/unit/InstantAnimationTests.swift @@ -24,7 +24,6 @@ import MotionAnimator class InstantAnimationTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! var view: UIView! var addedAnimations: [CAAnimation]! @@ -33,11 +32,6 @@ class InstantAnimationTests: XCTestCase { animator = MotionAnimator() - timing = MotionTiming(delay: 0, - duration: 0, - curve: .init(type: .instant, data: (0, 0, 0, 0)), - repetition: .init(type: .none, amount: 0, autoreverses: false)) - let window = UIWindow() window.makeKeyAndVisible() view = UIView() // Need to animate a view's layer to get implicit animations. @@ -61,15 +55,42 @@ class InstantAnimationTests: XCTestCase { } func testDoesNotGenerateImplicitAnimations() { - animator.animate(with: timing, to: view.layer, withValues: [1, 0.5], keyPath: .opacity) + let traits = MDMAnimationTraits(duration: 0) + + animator.animate(with: traits, to: view.layer, withValues: [1, 0.5], keyPath: .opacity) XCTAssertNil(view.layer.animationKeys()) XCTAssertEqual(addedAnimations.count, 0) } func testDoesNotGenerateImplicitAnimationsInUIViewAnimationBlock() { + let traits = MDMAnimationTraits(duration: 0) + + UIView.animate(withDuration: 0.5) { + self.animator.animate(with: traits, + to: self.view.layer, + withValues: [1, 0.5], + keyPath: .opacity) + } + + XCTAssertNil(view.layer.animationKeys()) + XCTAssertEqual(addedAnimations.count, 0) + } + + func testDoesNotGenerateImplicitAnimationsWithNilCurve() { + let traits = MDMAnimationTraits(delay: 0, duration: 0.5, timingCurve: nil) + + animator.animate(with: traits, to: view.layer, withValues: [1, 0.5], keyPath: .opacity) + + XCTAssertNil(view.layer.animationKeys()) + XCTAssertEqual(addedAnimations.count, 0) + } + + func testDoesNotGenerateImplicitAnimationsInUIViewAnimationBlockWithNilCurve() { + let traits = MDMAnimationTraits(delay: 0, duration: 0.5, timingCurve: nil) + UIView.animate(withDuration: 0.5) { - self.animator.animate(with: self.timing, + self.animator.animate(with: traits, to: self.view.layer, withValues: [1, 0.5], keyPath: .opacity) diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index 5789f0c..fa220f5 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class AnimatorBehavioralTests: XCTestCase { var window: UIWindow! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var originalImplementation: IMP? override func setUp() { @@ -32,14 +32,11 @@ class AnimatorBehavioralTests: XCTestCase { window = UIWindow() window.makeKeyAndVisible() - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) } override func tearDown() { - timing = nil + traits = nil window = nil super.tearDown() @@ -75,7 +72,7 @@ class AnimatorBehavioralTests: XCTestCase { let animator = MotionAnimator() let initialValue = view.layer.value(forKeyPath: keyPath.rawValue) ?? NSNull() - animator.animate(with: timing, + animator.animate(with: traits, to: view.layer, withValues: [initialValue, value], keyPath: keyPath) @@ -99,7 +96,7 @@ class AnimatorBehavioralTests: XCTestCase { CATransaction.flush() let animator = MotionAnimator() - animator.animate(with: timing) { + animator.animate(with: traits) { view.layer.setValue(value, forKeyPath: keyPath.rawValue) } @@ -123,7 +120,7 @@ class AnimatorBehavioralTests: XCTestCase { let animator = MotionAnimator() let initialValue = layer.value(forKeyPath: keyPath.rawValue) ?? NSNull() - animator.animate(with: timing, + animator.animate(with: traits, to: layer, withValues: [initialValue, value], keyPath: keyPath) @@ -147,7 +144,7 @@ class AnimatorBehavioralTests: XCTestCase { CATransaction.flush() let animator = MotionAnimator() - animator.animate(with: timing) { + animator.animate(with: traits) { layer.setValue(value, forKeyPath: keyPath.rawValue) } diff --git a/tests/unit/MotionAnimatorTests.m b/tests/unit/MotionAnimatorTests.m index 10a0eac..9407247 100644 --- a/tests/unit/MotionAnimatorTests.m +++ b/tests/unit/MotionAnimatorTests.m @@ -27,13 +27,11 @@ - (void)testNoDurationSetsValueInstantly { CALayer *layer = [[CALayer alloc] init]; - MDMMotionTiming timing = { - .duration = 0, - }; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; layer.opacity = 0.5; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity"]; + [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity"]; XCTAssertEqual(layer.opacity, 1); } @@ -43,14 +41,12 @@ - (void)testNoDurationCallsCompletionHandler { CALayer *layer = [[CALayer alloc] init]; - MDMMotionTiming timing = { - .duration = 0, - }; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; layer.opacity = 0.5; __block BOOL didInvokeCompletion = false; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity" completion:^{ + [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity" completion:^{ didInvokeCompletion = true; }]; @@ -64,13 +60,11 @@ - (void)testReversingSetsTheFirstValue { CALayer *layer = [[CALayer alloc] init]; - MDMMotionTiming timing = { - .duration = 0, - }; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; layer.opacity = 0.5; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"cornerRadius"]; + [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:@"cornerRadius"]; XCTAssertEqual(layer.cornerRadius, 0); } @@ -85,11 +79,11 @@ - (void)testCubicBezierAnimationFloatValue { // Setting to some bogus value because it will be ignored with the default animator settings. layer.cornerRadius = 0.5; - MDMMotionTiming timing = { - .delay = 0.5, - .duration = 1, - .curve = MDMMotionCurveMakeBezier(0.1, 0.2, 0.3, 0.4), - }; + CAMediaTimingFunction *timingFunction = + [CAMediaTimingFunction functionWithControlPoints:0.1 :0.2 :0.3 :0.4]; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDelay:0.5 + duration:1 + timingCurve:timingFunction]; __block BOOL didAddAnimation = false; [animator addCoreAnimationTracer:^(CALayer *layer, CAAnimation *animation) { @@ -98,7 +92,7 @@ - (void)testCubicBezierAnimationFloatValue { XCTAssertEqual(basicAnimation.keyPath, keyPath); - XCTAssertEqual(basicAnimation.duration, timing.duration); + XCTAssertEqual(basicAnimation.duration, traits.duration); XCTAssertGreaterThan(basicAnimation.beginTime, 0); XCTAssertTrue(basicAnimation.additive); @@ -109,15 +103,15 @@ - (void)testCubicBezierAnimationFloatValue { float point2[2]; [basicAnimation.timingFunction getControlPointAtIndex:1 values:point1]; [basicAnimation.timingFunction getControlPointAtIndex:2 values:point2]; - XCTAssertEqualWithAccuracy(timing.curve.data[0], point1[0], 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[1], point1[1], 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[2], point2[0], 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[3], point2[1], 0.00001); + XCTAssertEqualWithAccuracy(timingFunction.mdm_point1.x, point1[0], 0.00001); + XCTAssertEqualWithAccuracy(timingFunction.mdm_point1.y, point1[1], 0.00001); + XCTAssertEqualWithAccuracy(timingFunction.mdm_point2.x, point2[0], 0.00001); + XCTAssertEqualWithAccuracy(timingFunction.mdm_point2.y, point2[1], 0.00001); didAddAnimation = true; }]; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; + [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; XCTAssertEqual(layer.cornerRadius, 1); XCTAssertTrue(didAddAnimation); @@ -133,11 +127,11 @@ - (void)testSpringAnimationFloatValue { // Setting to some bogus value because it will be ignored with the default animator settings. layer.cornerRadius = 0.5; - MDMMotionTiming timing = { - .delay = 0.5, - .duration = 1, - .curve = MDMMotionCurveMakeSpring(0.1, 0.2, 0.3), - }; + MDMSpringTimingCurve *springCurve = + [[MDMSpringTimingCurve alloc] initWithMass:0.1 tension:0.2 friction:0.3]; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDelay:0.5 + duration:1 + timingCurve:springCurve]; __block BOOL didAddAnimation = false; [animator addCoreAnimationTracer:^(CALayer *layer, CAAnimation *animation) { @@ -149,7 +143,7 @@ - (void)testSpringAnimationFloatValue { if ([springAnimation respondsToSelector:@selector(settlingDuration)]) { XCTAssertEqual(springAnimation.duration, springAnimation.settlingDuration); } else { - XCTAssertEqual(springAnimation.duration, timing.duration); + XCTAssertEqual(springAnimation.duration, traits.duration); } XCTAssertGreaterThan(springAnimation.beginTime, 0); @@ -157,14 +151,14 @@ - (void)testSpringAnimationFloatValue { XCTAssertEqual([springAnimation.fromValue doubleValue], -1); XCTAssertEqual([springAnimation.toValue doubleValue], 0); - XCTAssertEqualWithAccuracy(timing.curve.data[0], springAnimation.mass, 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[1], springAnimation.stiffness, 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[2], springAnimation.damping, 0.00001); + XCTAssertEqualWithAccuracy(springCurve.mass, springAnimation.mass, 0.00001); + XCTAssertEqualWithAccuracy(springCurve.tension, springAnimation.stiffness, 0.00001); + XCTAssertEqualWithAccuracy(springCurve.friction, springAnimation.damping, 0.00001); didAddAnimation = true; }]; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; + [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; XCTAssertEqual(layer.cornerRadius, 1); XCTAssertTrue(didAddAnimation); diff --git a/tests/unit/MotionAnimatorTests.swift b/tests/unit/MotionAnimatorTests.swift index c6649d6..7a221aa 100644 --- a/tests/unit/MotionAnimatorTests.swift +++ b/tests/unit/MotionAnimatorTests.swift @@ -26,38 +26,35 @@ class MotionAnimatorTests: XCTestCase { func testAnimatorAPIsCompile() { let animator = MotionAnimator() - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) let layer = CALayer() - animator.animate(with: timing, to: layer, + animator.animate(with: traits, to: layer, withValues: [UIColor.blue, UIColor.red], keyPath: .backgroundColor) - animator.animate(with: timing, to: layer, + animator.animate(with: traits, to: layer, withValues: [CGRect.zero, CGRect(x: 0, y: 0, width: 100, height: 50)], keyPath: .bounds) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .cornerRadius) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .height) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .opacity) - animator.animate(with: timing, to: layer, + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .cornerRadius) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .height) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .opacity) + animator.animate(with: traits, to: layer, withValues: [CGPoint.zero, CGPoint(x: 1, y: 1)], keyPath: .position) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .scale) - animator.animate(with: timing, to: layer, + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .scale) + animator.animate(with: traits, to: layer, withValues: [CGSize.zero, CGSize(width: 1, height: 1)], keyPath: .shadowOffset) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .shadowOpacity) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .shadowRadius) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .strokeStart) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .strokeEnd) - animator.animate(with: timing, to: layer, + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .shadowOpacity) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .shadowRadius) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .strokeStart) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .strokeEnd) + animator.animate(with: traits, to: layer, withValues: [CGAffineTransform(rotationAngle: 12), CGAffineTransform(rotationAngle: 50)], keyPath: .transform) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .width) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .x) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .y) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .width) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .x) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .y) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .init(rawValue: "bounds.size.width")) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .init(rawValue: "bounds.size.width")) XCTAssertTrue(true) } @@ -65,11 +62,7 @@ class MotionAnimatorTests: XCTestCase { func testAnimatorOnlyUsesSingleNonAdditiveAnimationForKeyPath() { let animator = MotionAnimator() animator.additive = false - - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -79,7 +72,7 @@ class MotionAnimatorTests: XCTestCase { XCTAssertEqual(view.layer.delegate as? UIView, view) UIView.animate(withDuration: 0.5) { - animator.animate(with: timing, to: view.layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, to: view.layer, withValues: [0, 1], keyPath: .rotation) XCTAssertEqual(view.layer.animationKeys()?.count, 1) } @@ -87,11 +80,7 @@ class MotionAnimatorTests: XCTestCase { func testCompletionCallbackIsExecutedWithZeroDuration() { let animator = MotionAnimator() - - let timing = MotionTiming(delay: 0, - duration: 0, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -101,7 +90,7 @@ class MotionAnimatorTests: XCTestCase { XCTAssertEqual(view.layer.delegate as? UIView, view) let didComplete = expectation(description: "Did complete") - animator.animate(with: timing, to: view.layer, withValues: [0, 1], keyPath: .rotation) { + animator.animate(with: traits, to: view.layer, withValues: [0, 1], keyPath: .rotation) { didComplete.fulfill() } diff --git a/tests/unit/NonAdditiveAnimatorTests.swift b/tests/unit/NonAdditiveAnimatorTests.swift index 8fa5fdf..8a17617 100644 --- a/tests/unit/NonAdditiveAnimatorTests.swift +++ b/tests/unit/NonAdditiveAnimatorTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class NonAdditiveAnimationTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! override func setUp() { @@ -33,10 +33,7 @@ class NonAdditiveAnimationTests: XCTestCase { animator.additive = false - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -49,14 +46,14 @@ class NonAdditiveAnimationTests: XCTestCase { override func tearDown() { animator = nil - timing = nil + traits = nil view = nil super.tearDown() } func testNumericKeyPathsDontAnimateAdditively() { - animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: traits, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -75,7 +72,7 @@ class NonAdditiveAnimationTests: XCTestCase { } func testSizeKeyPathsDontAnimateAdditively() { - animator.animate(with: timing, to: view.layer, + animator.animate(with: traits, to: view.layer, withValues: [CGSize(width: 0, height: 0), CGSize(width: 1, height: 2)], keyPath: .shadowOffset) @@ -96,7 +93,7 @@ class NonAdditiveAnimationTests: XCTestCase { } func testPositionKeyPathsDontAnimateAdditively() { - animator.animate(with: timing, to: view.layer, + animator.animate(with: traits, to: view.layer, withValues: [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 2)], keyPath: .position) @@ -117,7 +114,7 @@ class NonAdditiveAnimationTests: XCTestCase { } func testRectKeyPathsDontAnimateAdditively() { - animator.animate(with: timing, to: view.layer, + animator.animate(with: traits, to: view.layer, withValues: [CGRect(x: 0, y: 0, width: 0, height: 0), CGRect(x: 0, y: 0, width: 100, height: 50)], keyPath: .bounds) diff --git a/tests/unit/TimeScaleFactorTests.swift b/tests/unit/TimeScaleFactorTests.swift index 9e89ad4..c5695d0 100644 --- a/tests/unit/TimeScaleFactorTests.swift +++ b/tests/unit/TimeScaleFactorTests.swift @@ -23,10 +23,7 @@ import MotionAnimator class TimeScaleFactorTests: XCTestCase { - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) var layer: CALayer! var addedAnimations: [CAAnimation]! var animator: MotionAnimator! @@ -52,7 +49,7 @@ class TimeScaleFactorTests: XCTestCase { } func testDefaultTimeScaleFactorDoesNotModifyDuration() { - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! @@ -62,23 +59,23 @@ class TimeScaleFactorTests: XCTestCase { func testExplicitTimeScaleFactorChangesDuration() { animator.timeScaleFactor = 0.5 - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, timing.duration * 0.5) + XCTAssertEqual(animation.duration, traits.duration * 0.5) } func testTransactionTimeScaleFactorChangesDuration() { CATransaction.begin() CATransaction.mdm_setTimeScaleFactor(0.5) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, timing.duration * 0.5) + XCTAssertEqual(animation.duration, traits.duration * 0.5) } func testTransactionTimeScaleFactorOverridesAnimatorTimeScaleFactor() { @@ -87,13 +84,13 @@ class TimeScaleFactorTests: XCTestCase { CATransaction.begin() CATransaction.mdm_setTimeScaleFactor(0.5) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, timing.duration * 0.5) + XCTAssertEqual(animation.duration, traits.duration * 0.5) } func testNilTransactionTimeScaleFactorUsesAnimatorTimeScaleFactor() { @@ -103,12 +100,12 @@ class TimeScaleFactorTests: XCTestCase { CATransaction.mdm_setTimeScaleFactor(0.5) CATransaction.mdm_setTimeScaleFactor(nil) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, timing.duration * 2) + XCTAssertEqual(animation.duration, traits.duration * 2) } } From f55625d9f63e857e878eff4e7687ddd40bad0fea Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 4 Dec 2017 12:45:25 -0500 Subject: [PATCH 03/11] Revert "Update with ObjC implementation." This reverts commit be7f9081c0678484e034cc976aafcdab748b58bf. --- Podfile | 1 - Podfile.lock | 9 +- examples/CalendarCardExpansionExample.m | 34 ++--- examples/CalendarChipMotionSpec.h | 24 ++-- examples/CalendarChipMotionSpec.m | 126 +++++++----------- examples/TapToBounceExample.swift | 13 +- src/MDMMotionAnimator.h | 33 +++-- src/MDMMotionAnimator.m | 28 ++-- src/private/CABasicAnimation+MotionAnimator.h | 6 +- src/private/CABasicAnimation+MotionAnimator.m | 114 +++++++++------- .../CAMediaTimingFunction+MotionAnimator.h | 2 +- tests/unit/AdditiveAnimatorTests.swift | 15 ++- tests/unit/AnimationRemovalTests.swift | 17 ++- tests/unit/BeginFromCurrentStateTests.swift | 25 ++-- .../HeadlessLayerImplicitAnimationTests.swift | 22 +-- tests/unit/ImplicitAnimationTests.swift | 120 ++++++++--------- tests/unit/InitialVelocityTests.swift | 13 +- tests/unit/InstantAnimationTests.swift | 37 ++--- .../unit/MotionAnimatorBehavioralTests.swift | 17 ++- tests/unit/MotionAnimatorTests.m | 60 +++++---- tests/unit/MotionAnimatorTests.swift | 57 ++++---- tests/unit/NonAdditiveAnimatorTests.swift | 17 ++- tests/unit/TimeScaleFactorTests.swift | 23 ++-- 23 files changed, 403 insertions(+), 410 deletions(-) diff --git a/Podfile b/Podfile index 661aa41..7936bb7 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,6 @@ target "MotionAnimatorCatalog" do pod 'CatalogByConvention' pod 'MotionAnimator', :path => './' project 'examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj' - pod 'MotionInterchange', :path => '../motion-interchange-objc/' end target "UnitTests" do diff --git a/Podfile.lock b/Podfile.lock index 21fa606..2bbf1ff 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,24 +2,21 @@ PODS: - CatalogByConvention (2.2.0) - MotionAnimator (2.6.0): - MotionInterchange (~> 1.3) - - MotionInterchange (1.4.0) + - MotionInterchange (1.3.0) DEPENDENCIES: - CatalogByConvention - MotionAnimator (from `./`) - - MotionInterchange (from `../motion-interchange-objc/`) EXTERNAL SOURCES: MotionAnimator: :path: ./ - MotionInterchange: - :path: ../motion-interchange-objc/ SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f MotionAnimator: a4b0ba87a674bb3e89e25f0530b7e80a204ac1c1 - MotionInterchange: 35e0fd286ceab53dd4ee03494b3fcafa6a70637a + MotionInterchange: 988fc0011e4b806cc33f2fb4f9566f5eeb4159e8 -PODFILE CHECKSUM: f354f45cd3f9eb0e6ac9a2bfd9429945eae8c0ad +PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3 COCOAPODS: 1.3.1 diff --git a/examples/CalendarCardExpansionExample.m b/examples/CalendarCardExpansionExample.m index d4826fb..f424c33 100644 --- a/examples/CalendarCardExpansionExample.m +++ b/examples/CalendarCardExpansionExample.m @@ -20,12 +20,12 @@ #import "MotionAnimator.h" -// This example demonstrates how to use a motion traits specification to build a complex +// This example demonstrates how to use a motion timing specification to build a complex // bi-directional animation using the MDMMotionAnimator object. MDMMotionAnimator is designed for // building fine-tuned explicit animations. Unlike UIView's implicit animation API, which can be // used to cause cascading animations on a variety of properties, MDMMotionAnimator will always add // exactly one animation per key path to the layer. This means you don't get as much for "free", but -// you do gain more control over the traits and motion of the animation. +// you do gain more control over the timing and motion of the animation. @implementation CalendarCardExpansionExampleViewController { // In a real-world scenario we'd likely create a separate view to manage all of these subviews so @@ -40,15 +40,15 @@ @implementation CalendarCardExpansionExampleViewController { - (void)didTap { _expanded = !_expanded; - id traits = (_expanded - ? CalendarChipMotionSpec.expansion - : CalendarChipMotionSpec.collapse); + CalendarChipTiming timing = (_expanded + ? CalendarChipMotionSpec.expansion + : CalendarChipMotionSpec.collapse); MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; animator.shouldReverseValues = !_expanded; animator.beginFromCurrentState = YES; - [animator animateWithTraits:traits.navigationBarY animations:^{ + [animator animateWithTiming:timing.navigationBarY animations:^{ [self.navigationController setNavigationBarHidden:_expanded animated:YES]; }]; @@ -56,33 +56,33 @@ - (void)didTap { CGRect headerFrame = [self frameForHeader]; // Animate the chip itself. - [animator animateWithTraits:traits.chipHeight + [animator animateWithTiming:timing.chipHeight toLayer:_chipView.layer withValues:@[ @(chipFrame.size.height), @(headerFrame.size.height) ] keyPath:MDMKeyPathHeight]; - [animator animateWithTraits:traits.chipWidth + [animator animateWithTiming:timing.chipWidth toLayer:_chipView.layer withValues:@[ @(chipFrame.size.width), @(headerFrame.size.width) ] keyPath:MDMKeyPathWidth]; - [animator animateWithTraits:traits.chipWidth + [animator animateWithTiming:timing.chipWidth toLayer:_chipView.layer withValues:@[ @(CGRectGetMidX(chipFrame)), @(CGRectGetMidX(headerFrame)) ] keyPath:MDMKeyPathX]; - [animator animateWithTraits:traits.chipY + [animator animateWithTiming:timing.chipY toLayer:_chipView.layer withValues:@[ @(CGRectGetMidY(chipFrame)), @(CGRectGetMidY(headerFrame)) ] keyPath:MDMKeyPathY]; - [animator animateWithTraits:traits.chipHeight + [animator animateWithTiming:timing.chipHeight toLayer:_chipView.layer withValues:@[ @([self chipCornerRadius]), @0 ] keyPath:MDMKeyPathCornerRadius]; // Cross-fade the chip's contents. - [animator animateWithTraits:traits.chipContentOpacity + [animator animateWithTiming:timing.chipContentOpacity toLayer:_collapsedContent.layer withValues:@[ @1, @0 ] keyPath:MDMKeyPathOpacity]; - [animator animateWithTraits:traits.headerContentOpacity + [animator animateWithTiming:timing.headerContentOpacity toLayer:_expandedContent.layer withValues:@[ @0, @1 ] keyPath:MDMKeyPathOpacity]; @@ -90,7 +90,7 @@ - (void)didTap { // Keeps the expandec content aligned to the bottom of the card by taking into consideration the // extra height. CGFloat excessTopMargin = chipFrame.size.height - headerFrame.size.height; - [animator animateWithTraits:traits.chipHeight + [animator animateWithTiming:timing.chipHeight toLayer:_expandedContent.layer withValues:@[ @(CGRectGetMidY([self expandedContentFrame]) + excessTopMargin), @(CGRectGetMidY([self expandedContentFrame])) ] @@ -99,7 +99,7 @@ - (void)didTap { // Keeps the collapsed content aligned to its position on screen by taking into consideration the // excess left margin. CGFloat excessLeftMargin = chipFrame.origin.x - headerFrame.origin.x; - [animator animateWithTraits:traits.chipWidth + [animator animateWithTiming:timing.chipWidth toLayer:_collapsedContent.layer withValues:@[ @(CGRectGetMidX([self collapsedContentFrame])), @(CGRectGetMidX([self collapsedContentFrame]) + excessLeftMargin) ] @@ -108,11 +108,11 @@ - (void)didTap { // Keeps the shape anchored to the bottom right of the chip. CGRect shapeFrameInChip = [self shapeFrameInRect:chipFrame]; CGRect shapeFrameInHeader = [self shapeFrameInRect:headerFrame]; - [animator animateWithTraits:traits.chipWidth + [animator animateWithTiming:timing.chipWidth toLayer:_shapeView.layer withValues:@[ @(CGRectGetMidX(shapeFrameInChip)), @(CGRectGetMidX(shapeFrameInHeader)) ] keyPath:MDMKeyPathX]; - [animator animateWithTraits:traits.chipHeight + [animator animateWithTiming:timing.chipHeight toLayer:_shapeView.layer withValues:@[ @(CGRectGetMidY(shapeFrameInChip)), @(CGRectGetMidY(shapeFrameInHeader)) ] keyPath:MDMKeyPathY]; diff --git a/examples/CalendarChipMotionSpec.h b/examples/CalendarChipMotionSpec.h index d7989f2..019160b 100644 --- a/examples/CalendarChipMotionSpec.h +++ b/examples/CalendarChipMotionSpec.h @@ -17,26 +17,24 @@ #import #import -@protocol CalendarChipTiming +typedef struct CalendarChipTiming { + MDMMotionTiming chipWidth; + MDMMotionTiming chipHeight; + MDMMotionTiming chipY; -@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipWidth; -@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipHeight; -@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipY; + MDMMotionTiming chipContentOpacity; + MDMMotionTiming headerContentOpacity; -@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipContentOpacity; -@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *headerContentOpacity; - -@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *navigationBarY; - -@end + MDMMotionTiming navigationBarY; +} CalendarChipTiming; @interface CalendarChipMotionSpec: NSObject -@property(nonatomic, class, strong, nonnull, readonly) id expansion; -@property(nonatomic, class, strong, nonnull, readonly) id collapse; +@property(nonatomic, class, readonly) CalendarChipTiming expansion; +@property(nonatomic, class, readonly) CalendarChipTiming collapse; // This object is not meant to be instantiated. -- (nonnull instancetype)init NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; @end diff --git a/examples/CalendarChipMotionSpec.m b/examples/CalendarChipMotionSpec.m index 51bffc3..b0ef2b8 100644 --- a/examples/CalendarChipMotionSpec.m +++ b/examples/CalendarChipMotionSpec.m @@ -16,84 +16,58 @@ #import "CalendarChipMotionSpec.h" -static id StandardTimingCurve(void) { - return [CAMediaTimingFunction functionWithControlPoints:0.4f :0.0f :0.2f :1.0f]; -} - -static id LinearTimingCurve(void) { - return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; -} - -@interface CalendarChipExpansionTiming: NSObject -@end - -@implementation CalendarChipExpansionTiming - -- (MDMAnimationTraits *)chipWidth { - return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.285 timingCurve:StandardTimingCurve()]; -} - -- (MDMAnimationTraits *)chipHeight { - return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; -} - -- (MDMAnimationTraits *)chipY { - return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; -} - -- (MDMAnimationTraits *)chipContentOpacity { - return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.075 timingCurve:LinearTimingCurve()]; -} - -- (MDMAnimationTraits *)headerContentOpacity { - return [[MDMAnimationTraits alloc] initWithDelay:0.075 duration:0.150 timingCurve:LinearTimingCurve()]; -} - -- (MDMAnimationTraits *)navigationBarY { - return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; -} - -@end - -@interface CalendarChipCollapseTiming: NSObject -@end - -@implementation CalendarChipCollapseTiming - -- (MDMAnimationTraits *)chipWidth { - return [[MDMAnimationTraits alloc] initWithDelay:0.045 duration:0.330 timingCurve:StandardTimingCurve()]; -} - -- (MDMAnimationTraits *)chipHeight { - return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.330 timingCurve:StandardTimingCurve()]; -} - -- (MDMAnimationTraits *)chipY { - return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.330 timingCurve:StandardTimingCurve()]; -} - -- (MDMAnimationTraits *)chipContentOpacity { - return [[MDMAnimationTraits alloc] initWithDelay:0.150 duration:0.150 timingCurve:LinearTimingCurve()]; -} - -- (MDMAnimationTraits *)headerContentOpacity { - return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.075 timingCurve:LinearTimingCurve()]; -} - -- (MDMAnimationTraits *)navigationBarY { - return [[MDMAnimationTraits alloc] initWithDelay:0.045 duration:0.150 timingCurve:StandardTimingCurve()]; -} - -@end - @implementation CalendarChipMotionSpec -+ (id)expansion { - return [[CalendarChipExpansionTiming alloc] init]; -} - -+ (id)collapse { - return [[CalendarChipCollapseTiming alloc] init]; ++ (MDMMotionCurve)eightyForty { + return MDMMotionCurveMakeBezier(0.4f, 0.0f, 0.2f, 1.0f); +} + ++ (CalendarChipTiming)expansion { + MDMMotionCurve eightyForty = [self eightyForty]; + return (CalendarChipTiming){ + .chipWidth = { + .delay = 0.000, .duration = 0.285, .curve = eightyForty, + }, + .chipHeight = { + .delay = 0.015, .duration = 0.360, .curve = eightyForty, + }, + .chipY = { + .delay = 0.015, .duration = 0.360, .curve = eightyForty, + }, + .chipContentOpacity = { + .delay = 0.000, .duration = 0.075, .curve = MDMLinearMotionCurve, + }, + .headerContentOpacity = { + .delay = 0.075, .duration = 0.150, .curve = MDMLinearMotionCurve, + }, + .navigationBarY = { + .delay = 0.015, .duration = 0.360, .curve = eightyForty, + }, + }; +} + ++ (CalendarChipTiming)collapse { + MDMMotionCurve eightyForty = [self eightyForty]; + return (CalendarChipTiming){ + .chipWidth = { + .delay = 0.045, .duration = 0.330, .curve = eightyForty, + }, + .chipHeight = { + .delay = 0.000, .duration = 0.330, .curve = eightyForty, + }, + .chipY = { + .delay = 0.015, .duration = 0.330, .curve = eightyForty, + }, + .chipContentOpacity = { + .delay = 0.150, .duration = 0.150, .curve = MDMLinearMotionCurve, + }, + .headerContentOpacity = { + .delay = 0.000, .duration = 0.075, .curve = MDMLinearMotionCurve, + }, + .navigationBarY = { + .delay = 0.045, .duration = 0.150, .curve = eightyForty, + } + }; } @end diff --git a/examples/TapToBounceExample.swift b/examples/TapToBounceExample.swift index 0974145..c4db306 100644 --- a/examples/TapToBounceExample.swift +++ b/examples/TapToBounceExample.swift @@ -40,22 +40,21 @@ class TapToBounceExampleViewController: UIViewController { for: [.touchUpInside, .touchUpOutside, .touchDragExit]) } - let traits = MDMAnimationTraits(delay: 0, - duration: 0.5, - timingCurve: MDMSpringTimingCurve(mass: 1, - tension: 100, - friction: 10)) + let timing = MotionTiming(delay: 0, + duration: 0.5, + curve: MotionCurveMakeSpring(mass: 1, tension: 100, friction: 10), + repetition: .init()) func didFocus(_ sender: UIButton) { let animator = MotionAnimator() - animator.animate(with: traits) { + animator.animate(with: timing) { sender.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) } } func didUnfocus(_ sender: UIButton) { let animator = MotionAnimator() - animator.animate(with: traits) { + animator.animate(with: timing) { sender.transform = .identity } } diff --git a/src/MDMMotionAnimator.h b/src/MDMMotionAnimator.h index 151ce7b..1ac537d 100644 --- a/src/MDMMotionAnimator.h +++ b/src/MDMMotionAnimator.h @@ -27,7 +27,7 @@ #import "MDMCoreAnimationTraceable.h" /** - An animator adds Core Animation animations to a layer based on a provided motion traits. + An animator adds Core Animation animations to a layer based on a provided motion timing. */ NS_SWIFT_NAME(MotionAnimator) @interface MDMMotionAnimator : NSObject @@ -44,7 +44,7 @@ NS_SWIFT_NAME(MotionAnimator) /** If enabled, explicitly-provided values will be reversed before animating. - This property does not affect the animateWithTraits:animations: family of methods. + This property does not affect the animateWithTiming:animations: family of methods. Disabled by default. */ @@ -70,32 +70,32 @@ NS_SWIFT_NAME(MotionAnimator) @property(nonatomic, assign) BOOL additive; /** - Adds a single animation to the layer with the given traits structure. + Adds a single animation to the layer with the given timing structure. If `additive` is disabled, the animation will be added to the layer with the keyPath as its key. In this case, multiple invocations of this function on the same key path will remove the animations added from prior invocations. - @param traits The traits to be used for the animation. + @param timing The timing to be used for the animation. @param layer The layer to be animated. @param values The values to be used in the animation. Must contain exactly two values. Supported UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include UIColor and UIBezierPath. @param keyPath The key path of the property to be animated. */ -- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits +- (void)animateWithTiming:(MDMMotionTiming)timing toLayer:(nonnull CALayer *)layer withValues:(nonnull NSArray *)values keyPath:(nonnull MDMAnimatableKeyPath)keyPath; /** - Adds a single animation to the layer with the given traits structure. + Adds a single animation to the layer with the given timing structure. If `additive` is disabled, the animation will be added to the layer with the keyPath as its key. In this case, multiple invocations of this function on the same key path will remove the animations added from prior invocations. - @param traits The traits to be used for the animation. + @param timing The timing to be used for the animation. @param layer The layer to be animated. @param values The values to be used in the animation. Must contain exactly two values. Supported UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include @@ -105,36 +105,35 @@ NS_SWIFT_NAME(MotionAnimator) animation hierarchy. If the duration of the animation is 0, this block is executed immediately. The block is escaping and will be released once the animations have completed. */ -- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits +- (void)animateWithTiming:(MDMMotionTiming)timing toLayer:(nonnull CALayer *)layer withValues:(nonnull NSArray *)values keyPath:(nonnull MDMAnimatableKeyPath)keyPath completion:(nullable void(^)(void))completion; /** - Performs `animations` using the traits provided. + Performs `animations` using the timing provided. - @param traits The traits to be used for the animation. + @param timing The timing to be used for the animation. @param animations The block to be executed. Any animatable properties changed within this block - will result in animations being added to the view's layer with the provided traits. The block is + will result in animations being added to the view's layer with the provided timing. The block is non-escaping. */ -- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits - animations:(nonnull void(^)(void))animations; +- (void)animateWithTiming:(MDMMotionTiming)timing animations:(nonnull void(^)(void))animations; /** - Performs `animations` using the traits provided and executes the completion handler once all added + Performs `animations` using the timing provided and executes the completion handler once all added animations have completed. - @param traits The traits to be used for the animation. + @param timing The timing to be used for the animation. @param animations The block to be executed. Any animatable properties changed within this block - will result in animations being added to the view's layer with the provided traits. The block is + will result in animations being added to the view's layer with the provided timing. The block is non-escaping. @param completion A block object to be executed once the animation sequence ends or it has been removed from the animation hierarchy. If the duration of the animation is 0, this block is executed immediately. The block is escaping and will be released once the animation sequence has completed. */ -- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits +- (void)animateWithTiming:(MDMMotionTiming)timing animations:(nonnull void (^)(void))animations completion:(nullable void(^)(void))completion; diff --git a/src/MDMMotionAnimator.m b/src/MDMMotionAnimator.m index a2709d7..f4a4c0f 100644 --- a/src/MDMMotionAnimator.m +++ b/src/MDMMotionAnimator.m @@ -40,14 +40,14 @@ - (instancetype)init { return self; } -- (void)animateWithTraits:(MDMAnimationTraits *)traits +- (void)animateWithTiming:(MDMMotionTiming)timing toLayer:(CALayer *)layer withValues:(NSArray *)values keyPath:(MDMAnimatableKeyPath)keyPath { - [self animateWithTraits:traits toLayer:layer withValues:values keyPath:keyPath completion:nil]; + [self animateWithTiming:timing toLayer:layer withValues:values keyPath:keyPath completion:nil]; } -- (void)animateWithTraits:(MDMAnimationTraits *)traits +- (void)animateWithTiming:(MDMMotionTiming)timing toLayer:(CALayer *)layer withValues:(NSArray *)values keyPath:(MDMAnimatableKeyPath)keyPath @@ -80,7 +80,7 @@ - (void)animateWithTraits:(MDMAnimationTraits *)traits return; } - CABasicAnimation *animation = MDMAnimationFromTiming(traits, timeScaleFactor); + CABasicAnimation *animation = MDMAnimationFromTiming(timing, timeScaleFactor); if (animation == nil) { exitEarly(); @@ -92,7 +92,7 @@ - (void)animateWithTraits:(MDMAnimationTraits *)traits [self addAnimation:animation toLayer:layer withKeyPath:keyPath - traits:traits + timing:timing timeScaleFactor:timeScaleFactor destination:[values lastObject] initialValue:^(BOOL wantsPresentationValue) { @@ -115,11 +115,11 @@ - (void)animateWithTraits:(MDMAnimationTraits *)traits } } -- (void)animateWithTraits:(MDMAnimationTraits *)traits animations:(void (^)(void))animations { - [self animateWithTraits:traits animations:animations completion:nil]; +- (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations { + [self animateWithTiming:timing animations:animations completion:nil]; } -- (void)animateWithTraits:(MDMAnimationTraits *)traits +- (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations completion:(void(^)(void))completion { NSArray *actions = MDMAnimateImplicitly(animations); @@ -142,7 +142,7 @@ - (void)animateWithTraits:(MDMAnimationTraits *)traits } // We'll reuse this animation template for each action. - CABasicAnimation *animationTemplate = MDMAnimationFromTiming(traits, timeScaleFactor); + CABasicAnimation *animationTemplate = MDMAnimationFromTiming(timing, timeScaleFactor); if (animationTemplate == nil) { exitEarly(); return; @@ -157,7 +157,7 @@ - (void)animateWithTraits:(MDMAnimationTraits *)traits [self addAnimation:animation toLayer:action.layer withKeyPath:action.keyPath - traits:traits + timing:timing timeScaleFactor:timeScaleFactor destination:[action.layer valueForKeyPath:action.keyPath] initialValue:^(BOOL wantsPresentationValue) { @@ -214,7 +214,7 @@ - (CGFloat)computedTimeScaleFactor { - (void)addAnimation:(CABasicAnimation *)animation toLayer:(CALayer *)layer withKeyPath:(NSString *)keyPath - traits:(MDMAnimationTraits *)traits + timing:(MDMMotionTiming)timing timeScaleFactor:(CGFloat)timeScaleFactor destination:(id)destination initialValue:(id(^)(BOOL wantsPresentationValue))initialValueBlock @@ -237,11 +237,11 @@ - (void)addAnimation:(CABasicAnimation *)animation NSString *key = animation.additive ? nil : keyPath; - MDMConfigureAnimation(animation, traits); + MDMConfigureAnimation(animation, timing); - if (traits.delay != 0) { + if (timing.delay != 0) { animation.beginTime = ([layer convertTime:CACurrentMediaTime() fromLayer:nil] - + traits.delay * timeScaleFactor); + + timing.delay * timeScaleFactor); animation.fillMode = kCAFillModeBackwards; } diff --git a/src/private/CABasicAnimation+MotionAnimator.h b/src/private/CABasicAnimation+MotionAnimator.h index 4e4fd9a..b3e0ce6 100644 --- a/src/private/CABasicAnimation+MotionAnimator.h +++ b/src/private/CABasicAnimation+MotionAnimator.h @@ -20,9 +20,9 @@ #import #import -// Returns a basic animation configured with the provided traits and scale factor. +// Returns a basic animation configured with the provided timing and scale factor. FOUNDATION_EXPORT -CABasicAnimation *MDMAnimationFromTiming(MDMAnimationTraits * traits, CGFloat timeScaleFactor); +CABasicAnimation *MDMAnimationFromTiming(MDMMotionTiming timing, CGFloat timeScaleFactor); // Returns a Boolean indicating whether or not an animation with the given key path and toValue // can be animated additively. @@ -33,4 +33,4 @@ FOUNDATION_EXPORT BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue); // // Not all animation value types support being additive. If an animation's value type was not // supported, the animation's values will not be modified. -FOUNDATION_EXPORT void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * traits); +FOUNDATION_EXPORT void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing); diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index 6d78830..e3cf2e7 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -65,39 +65,44 @@ static BOOL IsAnimationKeyPathAlwaysNonAdditive(NSString *keyPath) { #pragma mark - Public -CABasicAnimation *MDMAnimationFromTiming(MDMAnimationTraits * traits, CGFloat timeScaleFactor) { - if (traits.timingCurve == nil) { - return nil; - } +CABasicAnimation *MDMAnimationFromTiming(MDMMotionTiming timing, CGFloat timeScaleFactor) { + CABasicAnimation *animation; + switch (timing.curve.type) { + case MDMMotionCurveTypeInstant: + animation = nil; + break; - if ([traits.timingCurve isKindOfClass:[CAMediaTimingFunction class]]) { - CFTimeInterval duration = traits.duration * timeScaleFactor; - if (duration == 0) { - return nil; - } - CABasicAnimation *animation = [CABasicAnimation animation]; - animation.timingFunction = (CAMediaTimingFunction *)traits.timingCurve; - animation.duration = duration; - return animation; - } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + case MDMMotionCurveTypeDefault: +#pragma clang diagnostic pop + case MDMMotionCurveTypeBezier: + animation = [CABasicAnimation animation]; + animation.timingFunction = MDMTimingFunctionWithControlPoints(timing.curve.data); + animation.duration = timing.duration * timeScaleFactor; - if ([traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]]) { - MDMSpringTimingCurve *springTiming = (MDMSpringTimingCurve *)traits.timingCurve; + if (animation.duration == 0) { + return nil; + } + break; + case MDMMotionCurveTypeSpring: { #pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. #pragma clang diagnostic ignored "-Wpartial-availability" - CASpringAnimation *animation = [CASpringAnimation animation]; + CASpringAnimation *spring = [CASpringAnimation animation]; #pragma clang diagnostic pop - animation.mass = springTiming.mass; - animation.stiffness = springTiming.tension; - animation.damping = springTiming.friction; - animation.duration = traits.duration; - return animation; - } + spring.mass = timing.curve.data[MDMSpringMotionCurveDataIndexMass]; + spring.stiffness = timing.curve.data[MDMSpringMotionCurveDataIndexTension]; + spring.damping = timing.curve.data[MDMSpringMotionCurveDataIndexFriction]; + spring.duration = timing.duration; - return nil; + animation = spring; + break; + } + } + return animation; } BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue) { @@ -110,18 +115,8 @@ BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue) { || IsCATransform3DType(toValue)); } -void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * traits) { -#pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. -#pragma clang diagnostic ignored "-Wpartial-availability" - BOOL isSpringAnimation = ([animation isKindOfClass:[CASpringAnimation class]] - && [traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]]); - MDMSpringTimingCurve *springTimingCurve = (MDMSpringTimingCurve *)traits.timingCurve; - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop - - if (!animation.additive && !isSpringAnimation) { +void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) { + if (!animation.additive && timing.curve.type != MDMMotionCurveTypeSpring) { return; // Nothing to do here. } @@ -163,10 +158,17 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * tra animation.toValue = @0; } - if (isSpringAnimation) { - CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; +#pragma clang diagnostic push + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. +#pragma clang diagnostic ignored "-Wpartial-availability" + if ([animation isKindOfClass:[CASpringAnimation class]]) { + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop + + CGFloat absoluteInitialVelocity = timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; - // Our traits's initialVelocity is in points per second, but Core Animation expects initial + // Our timing's initialVelocity is in points per second, but Core Animation expects initial // velocity to be in terms of displacement per second. // // From the UIView animateWithDuration header docs: @@ -218,7 +220,13 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * tra animation.toValue = [NSValue valueWithCGSize:CGSizeZero]; } - if (isSpringAnimation) { +#pragma clang diagnostic push + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. +#pragma clang diagnostic ignored "-Wpartial-availability" + if ([animation isKindOfClass:[CASpringAnimation class]]) { + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop // Core Animation's velocity system is single dimensional, so we pick the dominant direction // of movement and normalize accordingly. CGFloat biggestDelta; @@ -228,7 +236,8 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * tra biggestDelta = additiveDisplacement.height; } CGFloat displacement = -biggestDelta; - CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; + CGFloat absoluteInitialVelocity = + timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; if (fabs(displacement) > 0.00001) { springAnimation.initialVelocity = absoluteInitialVelocity / displacement; } @@ -244,7 +253,13 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * tra animation.toValue = [NSValue valueWithCGPoint:CGPointZero]; } - if (isSpringAnimation) { +#pragma clang diagnostic push + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. +#pragma clang diagnostic ignored "-Wpartial-availability" + if ([animation isKindOfClass:[CASpringAnimation class]]) { + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop // Core Animation's velocity system is single dimensional, so we pick the dominant direction // of movement and normalize accordingly. CGFloat biggestDelta; @@ -254,7 +269,8 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * tra biggestDelta = additiveDisplacement.y; } CGFloat displacement = -biggestDelta; - CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; + CGFloat absoluteInitialVelocity = + timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; if (fabs(displacement) > 0.00001) { springAnimation.initialVelocity = absoluteInitialVelocity / displacement; } @@ -271,7 +287,13 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * tra } } - if (isSpringAnimation) { + // Update the animation's duration to match the proposed settling duration. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + if ([animation isKindOfClass:[CASpringAnimation class]]) { + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop + // This API is only available on iOS 9+ if ([springAnimation respondsToSelector:@selector(settlingDuration)]) { animation.duration = springAnimation.settlingDuration; diff --git a/src/private/CAMediaTimingFunction+MotionAnimator.h b/src/private/CAMediaTimingFunction+MotionAnimator.h index e1e1803..8790721 100644 --- a/src/private/CAMediaTimingFunction+MotionAnimator.h +++ b/src/private/CAMediaTimingFunction+MotionAnimator.h @@ -18,6 +18,6 @@ #import #import -// Returns a traits function with the given control points. +// Returns a timing function with the given control points. FOUNDATION_EXPORT CAMediaTimingFunction* MDMTimingFunctionWithControlPoints(CGFloat controlPoints[4]); diff --git a/tests/unit/AdditiveAnimatorTests.swift b/tests/unit/AdditiveAnimatorTests.swift index c5855ba..f4e60ea 100644 --- a/tests/unit/AdditiveAnimatorTests.swift +++ b/tests/unit/AdditiveAnimatorTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class AdditiveAnimationTests: XCTestCase { var animator: MotionAnimator! - var traits: MDMAnimationTraits! + var timing: MotionTiming! var view: UIView! override func setUp() { @@ -33,7 +33,10 @@ class AdditiveAnimationTests: XCTestCase { animator.additive = true - traits = MDMAnimationTraits(duration: 1) + timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 1, p2y: 1), + repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() window.makeKeyAndVisible() @@ -46,14 +49,14 @@ class AdditiveAnimationTests: XCTestCase { override func tearDown() { animator = nil - traits = nil + timing = nil view = nil super.tearDown() } func testNumericKeyPathsAnimateAdditively() { - animator.animate(with: traits, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -72,7 +75,7 @@ class AdditiveAnimationTests: XCTestCase { } func testCGSizeKeyPathsAnimateAdditively() { - animator.animate(with: traits, to: view.layer, + animator.animate(with: timing, to: view.layer, withValues: [CGSize(width: 0, height: 0), CGSize(width: 1, height: 2)], keyPath: .shadowOffset) @@ -93,7 +96,7 @@ class AdditiveAnimationTests: XCTestCase { } func testCGPointKeyPathsAnimateAdditively() { - animator.animate(with: traits, to: view.layer, + animator.animate(with: timing, to: view.layer, withValues: [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 2)], keyPath: .position) diff --git a/tests/unit/AnimationRemovalTests.swift b/tests/unit/AnimationRemovalTests.swift index 70aa766..197c8d2 100644 --- a/tests/unit/AnimationRemovalTests.swift +++ b/tests/unit/AnimationRemovalTests.swift @@ -24,7 +24,7 @@ import MotionAnimator class AnimationRemovalTests: XCTestCase { var animator: MotionAnimator! - var traits: MDMAnimationTraits! + var timing: MotionTiming! var view: UIView! var originalImplementation: IMP? @@ -32,7 +32,10 @@ class AnimationRemovalTests: XCTestCase { super.setUp() animator = MotionAnimator() - traits = MDMAnimationTraits(duration: 1) + timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() window.makeKeyAndVisible() @@ -45,15 +48,15 @@ class AnimationRemovalTests: XCTestCase { override func tearDown() { animator = nil - traits = nil + timing = nil view = nil super.tearDown() } func testAllAdditiveAnimationsGetsRemoved() { - animator.animate(with: traits, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) - animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) + animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) XCTAssertEqual(view.layer.animationKeys()!.count, 2) @@ -70,8 +73,8 @@ class AnimationRemovalTests: XCTestCase { didComplete = true } - animator.animate(with: traits, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) - animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) + animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) CATransaction.commit() diff --git a/tests/unit/BeginFromCurrentStateTests.swift b/tests/unit/BeginFromCurrentStateTests.swift index 20655cd..7117089 100644 --- a/tests/unit/BeginFromCurrentStateTests.swift +++ b/tests/unit/BeginFromCurrentStateTests.swift @@ -24,7 +24,7 @@ import MotionAnimator class BeginFromCurrentStateTests: XCTestCase { var animator: MotionAnimator! - var traits: MDMAnimationTraits! + var timing: MotionTiming! var view: UIView! var addedAnimations: [CAAnimation]! @@ -35,7 +35,10 @@ class BeginFromCurrentStateTests: XCTestCase { animator.beginFromCurrentState = true - traits = MDMAnimationTraits(duration: 1) + timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() window.makeKeyAndVisible() @@ -53,7 +56,7 @@ class BeginFromCurrentStateTests: XCTestCase { override func tearDown() { animator = nil - traits = nil + timing = nil view = nil addedAnimations = nil @@ -65,7 +68,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.additive = false - animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -100,7 +103,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.additive = false - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 0.5 } @@ -135,7 +138,7 @@ class BeginFromCurrentStateTests: XCTestCase { func testExplicitlyAnimatesFromPresentationValue() { animator.additive = false - animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) RunLoop.main.run(until: .init(timeIntervalSinceNow: 0.01)) XCTAssertNotNil(view.layer.presentation(), "No presentation layer found.") @@ -144,7 +147,7 @@ class BeginFromCurrentStateTests: XCTestCase { } let initialValue = presentation.opacity - animator.animate(with: traits, to: view.layer, withValues: [0, 0.2], keyPath: .opacity) + animator.animate(with: timing, to: view.layer, withValues: [0, 0.2], keyPath: .opacity) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -177,7 +180,7 @@ class BeginFromCurrentStateTests: XCTestCase { func testImplicitlyAnimatesFromPresentationValue() { animator.additive = false - animator.animate(with: traits, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) RunLoop.main.run(until: .init(timeIntervalSinceNow: 0.01)) @@ -187,7 +190,7 @@ class BeginFromCurrentStateTests: XCTestCase { } let initialValue = presentation.opacity - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 0.2 } @@ -223,7 +226,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.beginFromCurrentState = true animator.additive = false - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 0.5 } @@ -231,7 +234,7 @@ class BeginFromCurrentStateTests: XCTestCase { let initialValue = view.layer.presentation()!.opacity - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 1.0 } diff --git a/tests/unit/HeadlessLayerImplicitAnimationTests.swift b/tests/unit/HeadlessLayerImplicitAnimationTests.swift index bf6862b..7b0e4f6 100644 --- a/tests/unit/HeadlessLayerImplicitAnimationTests.swift +++ b/tests/unit/HeadlessLayerImplicitAnimationTests.swift @@ -97,7 +97,7 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { // Verifies the somewhat counter-intuitive fact that CATransaction's animation duration always // takes precedence over UIView's animation duration. This means that animating a headless layer - // using UIView animation APIs may not result in the expected traitss. + // using UIView animation APIs may not result in the expected timings. func testCATransactionTimingTakesPrecedenceOverUIViewTimingOutside() { CATransaction.begin() CATransaction.setAnimationDuration(0.2) @@ -146,19 +146,18 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { func testAnimatorTimingTakesPrecedenceOverCATransactionTiming() { let animator = MotionAnimator() animator.additive = false + let timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) - let traits = MDMAnimationTraits(duration: 1) - - CATransaction.begin() - CATransaction.setAnimationDuration(0.5) - animator.animate(with: traits) { + animator.animate(with: timing) { self.layer.opacity = 0.5 } - CATransaction.commit() let animation = layer.animation(forKey: "opacity") as! CABasicAnimation XCTAssertEqual(animation.keyPath, "opacity") - XCTAssertEqual(animation.duration, traits.duration) + XCTAssertEqual(animation.duration, timing.duration) } // MARK: Deprecated tests. @@ -205,9 +204,12 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { let animator = MotionAnimator() animator.additive = false - let traits = MDMAnimationTraits(duration: 1) + let timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) - animator.animate(with: traits) { + animator.animate(with: timing) { self.layer.opacity = 0.5 } diff --git a/tests/unit/ImplicitAnimationTests.swift b/tests/unit/ImplicitAnimationTests.swift index 61cf5ec..80eab59 100644 --- a/tests/unit/ImplicitAnimationTests.swift +++ b/tests/unit/ImplicitAnimationTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class ImplicitAnimationTests: XCTestCase { var animator: MotionAnimator! - var traits: MDMAnimationTraits! + var timing: MotionTiming! var view: UIView! var addedAnimations: [CAAnimation]! @@ -34,7 +34,10 @@ class ImplicitAnimationTests: XCTestCase { animator = MotionAnimator() animator.additive = false - traits = MDMAnimationTraits(duration: 1) + timing = MotionTiming(delay: 0, + duration: 0.7, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 1, p2y: 1), + repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() window.makeKeyAndVisible() @@ -66,7 +69,7 @@ class ImplicitAnimationTests: XCTestCase { } func testNoActionAddsNoAnimations() { - animator.animate(with: traits) { + animator.animate(with: timing) { // No-op } @@ -74,7 +77,7 @@ class ImplicitAnimationTests: XCTestCase { } func testOneActionAddsOneAnimation() { - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 0 } @@ -83,21 +86,18 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.opacity.rawValue) XCTAssertEqual(animation.fromValue as! CGFloat, 1) XCTAssertEqual(animation.toValue as! CGFloat, 0) - XCTAssertEqual(animation.duration, traits.duration) - - let timingCurve = traits.timingCurve as! CAMediaTimingFunction - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, - accuracy: 0.001) + XCTAssertEqual(animation.duration, timing.duration) + + let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) + XCTAssertEqual(addedCurve.type, timing.curve.type) + XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) + XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) + XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) + XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) } func testTwoActionsAddsTwoAnimations() { - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 0 self.view.center = .init(x: 50, y: 50) } @@ -110,17 +110,14 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.opacity.rawValue) XCTAssertEqual(animation.fromValue as! CGFloat, 1) XCTAssertEqual(animation.toValue as! CGFloat, 0) - XCTAssertEqual(animation.duration, traits.duration) - - let timingCurve = traits.timingCurve as! CAMediaTimingFunction - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, - accuracy: 0.001) + XCTAssertEqual(animation.duration, timing.duration) + + let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) + XCTAssertEqual(addedCurve.type, timing.curve.type) + XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) + XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) + XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) + XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) } do { let animation = addedAnimations[1] as! CABasicAnimation @@ -128,22 +125,19 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.position.rawValue) XCTAssertEqual(animation.fromValue as! CGPoint, .init(x: 0, y: 0)) XCTAssertEqual(animation.toValue as! CGPoint, .init(x: 50, y: 50)) - XCTAssertEqual(animation.duration, traits.duration) - - let timingCurve = traits.timingCurve as! CAMediaTimingFunction - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, - accuracy: 0.001) + XCTAssertEqual(animation.duration, timing.duration) + + let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) + XCTAssertEqual(addedCurve.type, timing.curve.type) + XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) + XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) + XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) + XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) } } func testFrameActionAddsTwoAnimations() { - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.frame = .init(x: 0, y: 0, width: 100, height: 100) } @@ -156,17 +150,14 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertFalse(animation.isAdditive) XCTAssertEqual(animation.fromValue as! CGPoint, .init(x: 0, y: 0)) XCTAssertEqual(animation.toValue as! CGPoint, .init(x: 50, y: 50)) - XCTAssertEqual(animation.duration, traits.duration) - - let timingCurve = traits.timingCurve as! CAMediaTimingFunction - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, - accuracy: 0.001) + XCTAssertEqual(animation.duration, timing.duration) + + let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) + XCTAssertEqual(addedCurve.type, timing.curve.type) + XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) + XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) + XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) + XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) } do { let animation = addedAnimations @@ -175,17 +166,14 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertFalse(animation.isAdditive) XCTAssertEqual(animation.fromValue as! CGRect, .init(x: 0, y: 0, width: 0, height: 0)) XCTAssertEqual(animation.toValue as! CGRect, .init(x: 0, y: 0, width: 100, height: 100)) - XCTAssertEqual(animation.duration, traits.duration) - - let timingCurve = traits.timingCurve as! CAMediaTimingFunction - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, - accuracy: 0.001) - XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, - accuracy: 0.001) + XCTAssertEqual(animation.duration, timing.duration) + + let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) + XCTAssertEqual(addedCurve.type, timing.curve.type) + XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) + XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) + XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) + XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) } } @@ -193,7 +181,7 @@ class ImplicitAnimationTests: XCTestCase { CATransaction.begin() CATransaction.setDisableActions(true) - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 0 } @@ -223,9 +211,9 @@ class ImplicitAnimationTests: XCTestCase { } func testDurationOfZeroRunsAnimationsBlockButGeneratesNoAnimations() { - let traits = MDMAnimationTraits(duration: 0) + timing.duration = 0 - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 0 } @@ -236,7 +224,7 @@ class ImplicitAnimationTests: XCTestCase { func testTimeScaleFactorOfZeroRunsAnimationsBlockButGeneratesNoAnimations() { animator.timeScaleFactor = 0 - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.alpha = 0 } @@ -245,7 +233,7 @@ class ImplicitAnimationTests: XCTestCase { } func testUnsupportedAnimationKeyIsNotAnimated() { - animator.animate(with: traits) { + animator.animate(with: timing) { self.view.layer.sublayers = [] } diff --git a/tests/unit/InitialVelocityTests.swift b/tests/unit/InitialVelocityTests.swift index d7cc74f..06378f9 100644 --- a/tests/unit/InitialVelocityTests.swift +++ b/tests/unit/InitialVelocityTests.swift @@ -135,15 +135,16 @@ class InitialVelocityTests: XCTestCase { } private func animate(from: CGFloat, to: CGFloat, withVelocity velocity: CGFloat) { - let springCurve = MDMSpringTimingCurve(mass: 1, tension: 1, friction: 1, - initialVelocity: velocity) - let traits = MDMAnimationTraits(delay: 0, duration: 0.7, timingCurve: springCurve) - animator.animate(with: traits, to: CALayer(), withValues: [from, to], + let timing = MotionTiming(delay: 0, + duration: 0.7, + curve: .init(type: .spring, data: (1, 1, 1, velocity)), + repetition: .init(type: .none, amount: 0, autoreverses: false)) + animator.animate(with: timing, to: CALayer(), withValues: [from, to], keyPath: .opacity) - animator.animate(with: traits, to: CALayer(), withValues: [CGPoint(x: from, y: from), + animator.animate(with: timing, to: CALayer(), withValues: [CGPoint(x: from, y: from), CGPoint(x: to, y: to)], keyPath: .position) - animator.animate(with: traits, to: CALayer(), withValues: [CGSize(width: from, height: from), + animator.animate(with: timing, to: CALayer(), withValues: [CGSize(width: from, height: from), CGSize(width: to, height: to)], keyPath: .init(rawValue: "bounds.size")) } diff --git a/tests/unit/InstantAnimationTests.swift b/tests/unit/InstantAnimationTests.swift index 281778b..3a40426 100644 --- a/tests/unit/InstantAnimationTests.swift +++ b/tests/unit/InstantAnimationTests.swift @@ -24,6 +24,7 @@ import MotionAnimator class InstantAnimationTests: XCTestCase { var animator: MotionAnimator! + var timing: MotionTiming! var view: UIView! var addedAnimations: [CAAnimation]! @@ -32,6 +33,11 @@ class InstantAnimationTests: XCTestCase { animator = MotionAnimator() + timing = MotionTiming(delay: 0, + duration: 0, + curve: .init(type: .instant, data: (0, 0, 0, 0)), + repetition: .init(type: .none, amount: 0, autoreverses: false)) + let window = UIWindow() window.makeKeyAndVisible() view = UIView() // Need to animate a view's layer to get implicit animations. @@ -55,42 +61,15 @@ class InstantAnimationTests: XCTestCase { } func testDoesNotGenerateImplicitAnimations() { - let traits = MDMAnimationTraits(duration: 0) - - animator.animate(with: traits, to: view.layer, withValues: [1, 0.5], keyPath: .opacity) + animator.animate(with: timing, to: view.layer, withValues: [1, 0.5], keyPath: .opacity) XCTAssertNil(view.layer.animationKeys()) XCTAssertEqual(addedAnimations.count, 0) } func testDoesNotGenerateImplicitAnimationsInUIViewAnimationBlock() { - let traits = MDMAnimationTraits(duration: 0) - - UIView.animate(withDuration: 0.5) { - self.animator.animate(with: traits, - to: self.view.layer, - withValues: [1, 0.5], - keyPath: .opacity) - } - - XCTAssertNil(view.layer.animationKeys()) - XCTAssertEqual(addedAnimations.count, 0) - } - - func testDoesNotGenerateImplicitAnimationsWithNilCurve() { - let traits = MDMAnimationTraits(delay: 0, duration: 0.5, timingCurve: nil) - - animator.animate(with: traits, to: view.layer, withValues: [1, 0.5], keyPath: .opacity) - - XCTAssertNil(view.layer.animationKeys()) - XCTAssertEqual(addedAnimations.count, 0) - } - - func testDoesNotGenerateImplicitAnimationsInUIViewAnimationBlockWithNilCurve() { - let traits = MDMAnimationTraits(delay: 0, duration: 0.5, timingCurve: nil) - UIView.animate(withDuration: 0.5) { - self.animator.animate(with: traits, + self.animator.animate(with: self.timing, to: self.view.layer, withValues: [1, 0.5], keyPath: .opacity) diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index fa220f5..5789f0c 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class AnimatorBehavioralTests: XCTestCase { var window: UIWindow! - var traits: MDMAnimationTraits! + var timing: MotionTiming! var originalImplementation: IMP? override func setUp() { @@ -32,11 +32,14 @@ class AnimatorBehavioralTests: XCTestCase { window = UIWindow() window.makeKeyAndVisible() - traits = MDMAnimationTraits(duration: 1) + timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) } override func tearDown() { - traits = nil + timing = nil window = nil super.tearDown() @@ -72,7 +75,7 @@ class AnimatorBehavioralTests: XCTestCase { let animator = MotionAnimator() let initialValue = view.layer.value(forKeyPath: keyPath.rawValue) ?? NSNull() - animator.animate(with: traits, + animator.animate(with: timing, to: view.layer, withValues: [initialValue, value], keyPath: keyPath) @@ -96,7 +99,7 @@ class AnimatorBehavioralTests: XCTestCase { CATransaction.flush() let animator = MotionAnimator() - animator.animate(with: traits) { + animator.animate(with: timing) { view.layer.setValue(value, forKeyPath: keyPath.rawValue) } @@ -120,7 +123,7 @@ class AnimatorBehavioralTests: XCTestCase { let animator = MotionAnimator() let initialValue = layer.value(forKeyPath: keyPath.rawValue) ?? NSNull() - animator.animate(with: traits, + animator.animate(with: timing, to: layer, withValues: [initialValue, value], keyPath: keyPath) @@ -144,7 +147,7 @@ class AnimatorBehavioralTests: XCTestCase { CATransaction.flush() let animator = MotionAnimator() - animator.animate(with: traits) { + animator.animate(with: timing) { layer.setValue(value, forKeyPath: keyPath.rawValue) } diff --git a/tests/unit/MotionAnimatorTests.m b/tests/unit/MotionAnimatorTests.m index 9407247..10a0eac 100644 --- a/tests/unit/MotionAnimatorTests.m +++ b/tests/unit/MotionAnimatorTests.m @@ -27,11 +27,13 @@ - (void)testNoDurationSetsValueInstantly { CALayer *layer = [[CALayer alloc] init]; - MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; + MDMMotionTiming timing = { + .duration = 0, + }; layer.opacity = 0.5; - [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity"]; + [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity"]; XCTAssertEqual(layer.opacity, 1); } @@ -41,12 +43,14 @@ - (void)testNoDurationCallsCompletionHandler { CALayer *layer = [[CALayer alloc] init]; - MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; + MDMMotionTiming timing = { + .duration = 0, + }; layer.opacity = 0.5; __block BOOL didInvokeCompletion = false; - [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity" completion:^{ + [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity" completion:^{ didInvokeCompletion = true; }]; @@ -60,11 +64,13 @@ - (void)testReversingSetsTheFirstValue { CALayer *layer = [[CALayer alloc] init]; - MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; + MDMMotionTiming timing = { + .duration = 0, + }; layer.opacity = 0.5; - [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:@"cornerRadius"]; + [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"cornerRadius"]; XCTAssertEqual(layer.cornerRadius, 0); } @@ -79,11 +85,11 @@ - (void)testCubicBezierAnimationFloatValue { // Setting to some bogus value because it will be ignored with the default animator settings. layer.cornerRadius = 0.5; - CAMediaTimingFunction *timingFunction = - [CAMediaTimingFunction functionWithControlPoints:0.1 :0.2 :0.3 :0.4]; - MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDelay:0.5 - duration:1 - timingCurve:timingFunction]; + MDMMotionTiming timing = { + .delay = 0.5, + .duration = 1, + .curve = MDMMotionCurveMakeBezier(0.1, 0.2, 0.3, 0.4), + }; __block BOOL didAddAnimation = false; [animator addCoreAnimationTracer:^(CALayer *layer, CAAnimation *animation) { @@ -92,7 +98,7 @@ - (void)testCubicBezierAnimationFloatValue { XCTAssertEqual(basicAnimation.keyPath, keyPath); - XCTAssertEqual(basicAnimation.duration, traits.duration); + XCTAssertEqual(basicAnimation.duration, timing.duration); XCTAssertGreaterThan(basicAnimation.beginTime, 0); XCTAssertTrue(basicAnimation.additive); @@ -103,15 +109,15 @@ - (void)testCubicBezierAnimationFloatValue { float point2[2]; [basicAnimation.timingFunction getControlPointAtIndex:1 values:point1]; [basicAnimation.timingFunction getControlPointAtIndex:2 values:point2]; - XCTAssertEqualWithAccuracy(timingFunction.mdm_point1.x, point1[0], 0.00001); - XCTAssertEqualWithAccuracy(timingFunction.mdm_point1.y, point1[1], 0.00001); - XCTAssertEqualWithAccuracy(timingFunction.mdm_point2.x, point2[0], 0.00001); - XCTAssertEqualWithAccuracy(timingFunction.mdm_point2.y, point2[1], 0.00001); + XCTAssertEqualWithAccuracy(timing.curve.data[0], point1[0], 0.00001); + XCTAssertEqualWithAccuracy(timing.curve.data[1], point1[1], 0.00001); + XCTAssertEqualWithAccuracy(timing.curve.data[2], point2[0], 0.00001); + XCTAssertEqualWithAccuracy(timing.curve.data[3], point2[1], 0.00001); didAddAnimation = true; }]; - [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; + [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; XCTAssertEqual(layer.cornerRadius, 1); XCTAssertTrue(didAddAnimation); @@ -127,11 +133,11 @@ - (void)testSpringAnimationFloatValue { // Setting to some bogus value because it will be ignored with the default animator settings. layer.cornerRadius = 0.5; - MDMSpringTimingCurve *springCurve = - [[MDMSpringTimingCurve alloc] initWithMass:0.1 tension:0.2 friction:0.3]; - MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDelay:0.5 - duration:1 - timingCurve:springCurve]; + MDMMotionTiming timing = { + .delay = 0.5, + .duration = 1, + .curve = MDMMotionCurveMakeSpring(0.1, 0.2, 0.3), + }; __block BOOL didAddAnimation = false; [animator addCoreAnimationTracer:^(CALayer *layer, CAAnimation *animation) { @@ -143,7 +149,7 @@ - (void)testSpringAnimationFloatValue { if ([springAnimation respondsToSelector:@selector(settlingDuration)]) { XCTAssertEqual(springAnimation.duration, springAnimation.settlingDuration); } else { - XCTAssertEqual(springAnimation.duration, traits.duration); + XCTAssertEqual(springAnimation.duration, timing.duration); } XCTAssertGreaterThan(springAnimation.beginTime, 0); @@ -151,14 +157,14 @@ - (void)testSpringAnimationFloatValue { XCTAssertEqual([springAnimation.fromValue doubleValue], -1); XCTAssertEqual([springAnimation.toValue doubleValue], 0); - XCTAssertEqualWithAccuracy(springCurve.mass, springAnimation.mass, 0.00001); - XCTAssertEqualWithAccuracy(springCurve.tension, springAnimation.stiffness, 0.00001); - XCTAssertEqualWithAccuracy(springCurve.friction, springAnimation.damping, 0.00001); + XCTAssertEqualWithAccuracy(timing.curve.data[0], springAnimation.mass, 0.00001); + XCTAssertEqualWithAccuracy(timing.curve.data[1], springAnimation.stiffness, 0.00001); + XCTAssertEqualWithAccuracy(timing.curve.data[2], springAnimation.damping, 0.00001); didAddAnimation = true; }]; - [animator animateWithTraits:traits toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; + [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; XCTAssertEqual(layer.cornerRadius, 1); XCTAssertTrue(didAddAnimation); diff --git a/tests/unit/MotionAnimatorTests.swift b/tests/unit/MotionAnimatorTests.swift index 7a221aa..c6649d6 100644 --- a/tests/unit/MotionAnimatorTests.swift +++ b/tests/unit/MotionAnimatorTests.swift @@ -26,35 +26,38 @@ class MotionAnimatorTests: XCTestCase { func testAnimatorAPIsCompile() { let animator = MotionAnimator() - let traits = MDMAnimationTraits(duration: 1) + let timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) let layer = CALayer() - animator.animate(with: traits, to: layer, + animator.animate(with: timing, to: layer, withValues: [UIColor.blue, UIColor.red], keyPath: .backgroundColor) - animator.animate(with: traits, to: layer, + animator.animate(with: timing, to: layer, withValues: [CGRect.zero, CGRect(x: 0, y: 0, width: 100, height: 50)], keyPath: .bounds) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .cornerRadius) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .height) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .opacity) - animator.animate(with: traits, to: layer, + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .cornerRadius) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .height) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .opacity) + animator.animate(with: timing, to: layer, withValues: [CGPoint.zero, CGPoint(x: 1, y: 1)], keyPath: .position) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .scale) - animator.animate(with: traits, to: layer, + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .scale) + animator.animate(with: timing, to: layer, withValues: [CGSize.zero, CGSize(width: 1, height: 1)], keyPath: .shadowOffset) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .shadowOpacity) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .shadowRadius) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .strokeStart) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .strokeEnd) - animator.animate(with: traits, to: layer, + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .shadowOpacity) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .shadowRadius) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .strokeStart) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .strokeEnd) + animator.animate(with: timing, to: layer, withValues: [CGAffineTransform(rotationAngle: 12), CGAffineTransform(rotationAngle: 50)], keyPath: .transform) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .width) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .x) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .y) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .width) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .x) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .y) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .init(rawValue: "bounds.size.width")) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .init(rawValue: "bounds.size.width")) XCTAssertTrue(true) } @@ -62,7 +65,11 @@ class MotionAnimatorTests: XCTestCase { func testAnimatorOnlyUsesSingleNonAdditiveAnimationForKeyPath() { let animator = MotionAnimator() animator.additive = false - let traits = MDMAnimationTraits(duration: 1) + + let timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() window.makeKeyAndVisible() @@ -72,7 +79,7 @@ class MotionAnimatorTests: XCTestCase { XCTAssertEqual(view.layer.delegate as? UIView, view) UIView.animate(withDuration: 0.5) { - animator.animate(with: traits, to: view.layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: timing, to: view.layer, withValues: [0, 1], keyPath: .rotation) XCTAssertEqual(view.layer.animationKeys()?.count, 1) } @@ -80,7 +87,11 @@ class MotionAnimatorTests: XCTestCase { func testCompletionCallbackIsExecutedWithZeroDuration() { let animator = MotionAnimator() - let traits = MDMAnimationTraits(duration: 1) + + let timing = MotionTiming(delay: 0, + duration: 0, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() window.makeKeyAndVisible() @@ -90,7 +101,7 @@ class MotionAnimatorTests: XCTestCase { XCTAssertEqual(view.layer.delegate as? UIView, view) let didComplete = expectation(description: "Did complete") - animator.animate(with: traits, to: view.layer, withValues: [0, 1], keyPath: .rotation) { + animator.animate(with: timing, to: view.layer, withValues: [0, 1], keyPath: .rotation) { didComplete.fulfill() } diff --git a/tests/unit/NonAdditiveAnimatorTests.swift b/tests/unit/NonAdditiveAnimatorTests.swift index 8a17617..8fa5fdf 100644 --- a/tests/unit/NonAdditiveAnimatorTests.swift +++ b/tests/unit/NonAdditiveAnimatorTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class NonAdditiveAnimationTests: XCTestCase { var animator: MotionAnimator! - var traits: MDMAnimationTraits! + var timing: MotionTiming! var view: UIView! override func setUp() { @@ -33,7 +33,10 @@ class NonAdditiveAnimationTests: XCTestCase { animator.additive = false - traits = MDMAnimationTraits(duration: 1) + timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() window.makeKeyAndVisible() @@ -46,14 +49,14 @@ class NonAdditiveAnimationTests: XCTestCase { override func tearDown() { animator = nil - traits = nil + timing = nil view = nil super.tearDown() } func testNumericKeyPathsDontAnimateAdditively() { - animator.animate(with: traits, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -72,7 +75,7 @@ class NonAdditiveAnimationTests: XCTestCase { } func testSizeKeyPathsDontAnimateAdditively() { - animator.animate(with: traits, to: view.layer, + animator.animate(with: timing, to: view.layer, withValues: [CGSize(width: 0, height: 0), CGSize(width: 1, height: 2)], keyPath: .shadowOffset) @@ -93,7 +96,7 @@ class NonAdditiveAnimationTests: XCTestCase { } func testPositionKeyPathsDontAnimateAdditively() { - animator.animate(with: traits, to: view.layer, + animator.animate(with: timing, to: view.layer, withValues: [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 2)], keyPath: .position) @@ -114,7 +117,7 @@ class NonAdditiveAnimationTests: XCTestCase { } func testRectKeyPathsDontAnimateAdditively() { - animator.animate(with: traits, to: view.layer, + animator.animate(with: timing, to: view.layer, withValues: [CGRect(x: 0, y: 0, width: 0, height: 0), CGRect(x: 0, y: 0, width: 100, height: 50)], keyPath: .bounds) diff --git a/tests/unit/TimeScaleFactorTests.swift b/tests/unit/TimeScaleFactorTests.swift index c5695d0..9e89ad4 100644 --- a/tests/unit/TimeScaleFactorTests.swift +++ b/tests/unit/TimeScaleFactorTests.swift @@ -23,7 +23,10 @@ import MotionAnimator class TimeScaleFactorTests: XCTestCase { - let traits = MDMAnimationTraits(duration: 1) + let timing = MotionTiming(delay: 0, + duration: 1, + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), + repetition: .init(type: .none, amount: 0, autoreverses: false)) var layer: CALayer! var addedAnimations: [CAAnimation]! var animator: MotionAnimator! @@ -49,7 +52,7 @@ class TimeScaleFactorTests: XCTestCase { } func testDefaultTimeScaleFactorDoesNotModifyDuration() { - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! @@ -59,23 +62,23 @@ class TimeScaleFactorTests: XCTestCase { func testExplicitTimeScaleFactorChangesDuration() { animator.timeScaleFactor = 0.5 - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, traits.duration * 0.5) + XCTAssertEqual(animation.duration, timing.duration * 0.5) } func testTransactionTimeScaleFactorChangesDuration() { CATransaction.begin() CATransaction.mdm_setTimeScaleFactor(0.5) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, traits.duration * 0.5) + XCTAssertEqual(animation.duration, timing.duration * 0.5) } func testTransactionTimeScaleFactorOverridesAnimatorTimeScaleFactor() { @@ -84,13 +87,13 @@ class TimeScaleFactorTests: XCTestCase { CATransaction.begin() CATransaction.mdm_setTimeScaleFactor(0.5) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, traits.duration * 0.5) + XCTAssertEqual(animation.duration, timing.duration * 0.5) } func testNilTransactionTimeScaleFactorUsesAnimatorTimeScaleFactor() { @@ -100,12 +103,12 @@ class TimeScaleFactorTests: XCTestCase { CATransaction.mdm_setTimeScaleFactor(0.5) CATransaction.mdm_setTimeScaleFactor(nil) - animator.animate(with: traits, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, traits.duration * 2) + XCTAssertEqual(animation.duration, timing.duration * 2) } } From 67d903ed71fbc909ea06bb8097313a4218c8f566 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 6 Dec 2017 10:32:48 -0500 Subject: [PATCH 04/11] Update kokoro bazel runner for v4. --- .kokoro | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/.kokoro b/.kokoro index 89ec030..cda1d70 100755 --- a/.kokoro +++ b/.kokoro @@ -20,7 +20,35 @@ set -e # Display commands to stderr. set -x -KOKORO_RUNNER_VERSION="v3.*" +KOKORO_RUNNER_VERSION="v4.*" + +POSITIONAL=() +while [[ $# -gt 0 ]]; do + key="$1" + + case $key in + -v|--verbose) + VERBOSE_OUTPUT="1" + shift + ;; + *) + POSITIONAL+=("$1") + shift + ;; + esac +done +set -- "${POSITIONAL[@]}" # restore positional parameters + +if [ -n "$KOKORO_BUILD_NUMBER" ]; then + # Always enable verbose output on kokoro runs. + VERBOSE_OUTPUT=1 +fi + +if [ -n "$VERBOSE_OUTPUT" ]; then + # Display commands to stderr. + set -x + verbosity_args="-v" +fi if [ ! -d .kokoro-ios-runner ]; then git clone https://github.com/material-foundation/kokoro-ios-runner.git .kokoro-ios-runner @@ -33,6 +61,6 @@ TAG=$(git tag --sort=v:refname -l "$KOKORO_RUNNER_VERSION" | tail -n1) git checkout "$TAG" > /dev/null popd -./.kokoro-ios-runner/bazel.sh test //:UnitTests 8.1.0 +./.kokoro-ios-runner/bazel.sh test //:UnitTests --min-xcode-version 8.1.0 $verbosity_args echo "Success!" From 45d43aa23a88c963927f4f01669e1b0ae26fb9e5 Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 7 Dec 2017 09:05:28 -0500 Subject: [PATCH 05/11] Enable coverage on travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 546c252..d0d328e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,6 @@ before_install: - pod install --repo-update script: - set -o pipefail - - xcodebuild build -workspace MotionAnimator.xcworkspace -scheme MotionAnimatorCatalog -sdk "iphonesimulator10.3" -destination "name=iPhone 6s,OS=10.1" ONLY_ACTIVE_ARCH=YES | xcpretty -c; + - xcodebuild test -workspace MotionAnimator.xcworkspace -scheme MotionAnimatorCatalog -sdk "iphonesimulator10.3" -destination "name=iPhone 6s,OS=10.1" -enableCodeCoverage YES ONLY_ACTIVE_ARCH=YES | xcpretty -c; after_success: - bash <(curl -s https://codecov.io/bash) From 573b19269e155f15e05e9b146a1c324b937cfb1c Mon Sep 17 00:00:00 2001 From: featherless Date: Wed, 13 Dec 2017 17:42:15 -0500 Subject: [PATCH 06/11] Migrate to the Objective-C interchange format (#88) This change introduces new APIs that are compatible with the new interchange format introduced in MotionInterchange [v1.5.0](https://github.com/material-motion/motion-interchange-objc/releases/tag/v1.5.0). The API arguments have also been reordered to be somewhat more intuitive. Note that this is not intended to be a breaking change. --- MotionAnimator.podspec | 2 +- Podfile.lock | 9 +- WORKSPACE | 2 +- examples/CalendarCardExpansionExample.m | 80 +++++------ examples/CalendarChipMotionSpec.h | 24 ++-- examples/CalendarChipMotionSpec.m | 126 +++++++++++------- examples/TapToBounceExample.swift | 13 +- src/MDMMotionAnimator.h | 109 ++++++++++----- src/MDMMotionAnimator.m | 91 +++++++++---- src/private/CABasicAnimation+MotionAnimator.h | 6 +- src/private/CABasicAnimation+MotionAnimator.m | 114 +++++++--------- src/private/MDMAnimationRegistrar.h | 2 +- src/private/MDMAnimationRegistrar.m | 4 +- src/private/MDMUIKitValueCoercion.h | 3 +- tests/unit/AdditiveAnimatorTests.swift | 23 ++-- tests/unit/AnimationRemovalTests.swift | 17 +-- tests/unit/BeginFromCurrentStateTests.swift | 29 ++-- .../HeadlessLayerImplicitAnimationTests.swift | 22 ++- tests/unit/ImplicitAnimationTests.swift | 120 +++++++++-------- tests/unit/InitialVelocityTests.swift | 23 ++-- tests/unit/InstantAnimationTests.swift | 41 ++++-- .../unit/MotionAnimatorBehavioralTests.swift | 25 ++-- tests/unit/MotionAnimatorTests.m | 62 ++++----- tests/unit/MotionAnimatorTests.swift | 56 +------- tests/unit/NonAdditiveAnimatorTests.swift | 28 ++-- tests/unit/TimeScaleFactorTests.swift | 23 ++-- 26 files changed, 558 insertions(+), 496 deletions(-) diff --git a/MotionAnimator.podspec b/MotionAnimator.podspec index f58dccb..cc7ca7b 100644 --- a/MotionAnimator.podspec +++ b/MotionAnimator.podspec @@ -12,5 +12,5 @@ Pod::Spec.new do |s| s.public_header_files = "src/*.h" s.source_files = "src/*.{h,m,mm}", "src/private/*.{h,m,mm}" - s.dependency "MotionInterchange", "~> 1.3" + s.dependency "MotionInterchange", "~> 1.6" end diff --git a/Podfile.lock b/Podfile.lock index 2bbf1ff..f8eaead 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,21 +2,24 @@ PODS: - CatalogByConvention (2.2.0) - MotionAnimator (2.6.0): - MotionInterchange (~> 1.3) - - MotionInterchange (1.3.0) + - MotionInterchange (1.6.0) DEPENDENCIES: - CatalogByConvention - MotionAnimator (from `./`) + - MotionInterchange (from `../motion-interchange-objc/`) EXTERNAL SOURCES: MotionAnimator: :path: ./ + MotionInterchange: + :path: ../motion-interchange-objc/ SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f MotionAnimator: a4b0ba87a674bb3e89e25f0530b7e80a204ac1c1 - MotionInterchange: 988fc0011e4b806cc33f2fb4f9566f5eeb4159e8 + MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d -PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3 +PODFILE CHECKSUM: f354f45cd3f9eb0e6ac9a2bfd9429945eae8c0ad COCOAPODS: 1.3.1 diff --git a/WORKSPACE b/WORKSPACE index ac1508c..a57711a 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -27,5 +27,5 @@ git_repository( git_repository( name = "motion_interchange_objc", remote = "https://github.com/material-motion/motion-interchange-objc.git", - tag = "v1.3.0", + tag = "v1.6.0", ) diff --git a/examples/CalendarCardExpansionExample.m b/examples/CalendarCardExpansionExample.m index f424c33..3648e49 100644 --- a/examples/CalendarCardExpansionExample.m +++ b/examples/CalendarCardExpansionExample.m @@ -20,12 +20,12 @@ #import "MotionAnimator.h" -// This example demonstrates how to use a motion timing specification to build a complex +// This example demonstrates how to use a motion traits specification to build a complex // bi-directional animation using the MDMMotionAnimator object. MDMMotionAnimator is designed for // building fine-tuned explicit animations. Unlike UIView's implicit animation API, which can be // used to cause cascading animations on a variety of properties, MDMMotionAnimator will always add // exactly one animation per key path to the layer. This means you don't get as much for "free", but -// you do gain more control over the timing and motion of the animation. +// you do gain more control over the traits and motion of the animation. @implementation CalendarCardExpansionExampleViewController { // In a real-world scenario we'd likely create a separate view to manage all of these subviews so @@ -40,15 +40,15 @@ @implementation CalendarCardExpansionExampleViewController { - (void)didTap { _expanded = !_expanded; - CalendarChipTiming timing = (_expanded - ? CalendarChipMotionSpec.expansion - : CalendarChipMotionSpec.collapse); + id traits = (_expanded + ? CalendarChipMotionSpec.expansion + : CalendarChipMotionSpec.collapse); MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; animator.shouldReverseValues = !_expanded; animator.beginFromCurrentState = YES; - [animator animateWithTiming:timing.navigationBarY animations:^{ + [animator animateWithTraits:traits.navigationBarY animations:^{ [self.navigationController setNavigationBarHidden:_expanded animated:YES]; }]; @@ -56,65 +56,67 @@ - (void)didTap { CGRect headerFrame = [self frameForHeader]; // Animate the chip itself. - [animator animateWithTiming:timing.chipHeight - toLayer:_chipView.layer - withValues:@[ @(chipFrame.size.height), @(headerFrame.size.height) ] + [animator animateWithTraits:traits.chipHeight + between:@[ @(chipFrame.size.height), @(headerFrame.size.height) ] + layer:_chipView.layer keyPath:MDMKeyPathHeight]; - [animator animateWithTiming:timing.chipWidth - toLayer:_chipView.layer - withValues:@[ @(chipFrame.size.width), @(headerFrame.size.width) ] + [animator animateWithTraits:traits.chipWidth + between:@[ @(chipFrame.size.width), @(headerFrame.size.width) ] + layer:_chipView.layer keyPath:MDMKeyPathWidth]; - [animator animateWithTiming:timing.chipWidth - toLayer:_chipView.layer - withValues:@[ @(CGRectGetMidX(chipFrame)), @(CGRectGetMidX(headerFrame)) ] + [animator animateWithTraits:traits.chipWidth + between:@[ @(CGRectGetMidX(chipFrame)), @(CGRectGetMidX(headerFrame)) ] + layer:_chipView.layer keyPath:MDMKeyPathX]; - [animator animateWithTiming:timing.chipY - toLayer:_chipView.layer - withValues:@[ @(CGRectGetMidY(chipFrame)), @(CGRectGetMidY(headerFrame)) ] + [animator animateWithTraits:traits.chipY + between:@[ @(CGRectGetMidY(chipFrame)), @(CGRectGetMidY(headerFrame)) ] + layer:_chipView.layer keyPath:MDMKeyPathY]; - [animator animateWithTiming:timing.chipHeight - toLayer:_chipView.layer - withValues:@[ @([self chipCornerRadius]), @0 ] + [animator animateWithTraits:traits.chipHeight + between:@[ @([self chipCornerRadius]), @0 ] + layer:_chipView.layer keyPath:MDMKeyPathCornerRadius]; // Cross-fade the chip's contents. - [animator animateWithTiming:timing.chipContentOpacity - toLayer:_collapsedContent.layer - withValues:@[ @1, @0 ] + [animator animateWithTraits:traits.chipContentOpacity + between:@[ @1, @0 ] + layer:_collapsedContent.layer keyPath:MDMKeyPathOpacity]; - [animator animateWithTiming:timing.headerContentOpacity - toLayer:_expandedContent.layer - withValues:@[ @0, @1 ] + [animator animateWithTraits:traits.headerContentOpacity + between:@[ @0, @1 ] + layer:_expandedContent.layer keyPath:MDMKeyPathOpacity]; // Keeps the expandec content aligned to the bottom of the card by taking into consideration the // extra height. CGFloat excessTopMargin = chipFrame.size.height - headerFrame.size.height; - [animator animateWithTiming:timing.chipHeight - toLayer:_expandedContent.layer - withValues:@[ @(CGRectGetMidY([self expandedContentFrame]) + excessTopMargin), + [animator animateWithTraits:traits.chipHeight + between:@[ @(CGRectGetMidY([self expandedContentFrame]) + excessTopMargin), @(CGRectGetMidY([self expandedContentFrame])) ] + layer:_expandedContent.layer keyPath:MDMKeyPathY]; // Keeps the collapsed content aligned to its position on screen by taking into consideration the // excess left margin. CGFloat excessLeftMargin = chipFrame.origin.x - headerFrame.origin.x; - [animator animateWithTiming:timing.chipWidth - toLayer:_collapsedContent.layer - withValues:@[ @(CGRectGetMidX([self collapsedContentFrame])), + [animator animateWithTraits:traits.chipWidth + between:@[ @(CGRectGetMidX([self collapsedContentFrame])), @(CGRectGetMidX([self collapsedContentFrame]) + excessLeftMargin) ] + layer:_collapsedContent.layer keyPath:MDMKeyPathX]; // Keeps the shape anchored to the bottom right of the chip. CGRect shapeFrameInChip = [self shapeFrameInRect:chipFrame]; CGRect shapeFrameInHeader = [self shapeFrameInRect:headerFrame]; - [animator animateWithTiming:timing.chipWidth - toLayer:_shapeView.layer - withValues:@[ @(CGRectGetMidX(shapeFrameInChip)), @(CGRectGetMidX(shapeFrameInHeader)) ] + [animator animateWithTraits:traits.chipWidth + between:@[ @(CGRectGetMidX(shapeFrameInChip)), + @(CGRectGetMidX(shapeFrameInHeader)) ] + layer:_shapeView.layer keyPath:MDMKeyPathX]; - [animator animateWithTiming:timing.chipHeight - toLayer:_shapeView.layer - withValues:@[ @(CGRectGetMidY(shapeFrameInChip)), @(CGRectGetMidY(shapeFrameInHeader)) ] + [animator animateWithTraits:traits.chipHeight + between:@[ @(CGRectGetMidY(shapeFrameInChip)), + @(CGRectGetMidY(shapeFrameInHeader)) ] + layer:_shapeView.layer keyPath:MDMKeyPathY]; } diff --git a/examples/CalendarChipMotionSpec.h b/examples/CalendarChipMotionSpec.h index 019160b..d7989f2 100644 --- a/examples/CalendarChipMotionSpec.h +++ b/examples/CalendarChipMotionSpec.h @@ -17,24 +17,26 @@ #import #import -typedef struct CalendarChipTiming { - MDMMotionTiming chipWidth; - MDMMotionTiming chipHeight; - MDMMotionTiming chipY; +@protocol CalendarChipTiming - MDMMotionTiming chipContentOpacity; - MDMMotionTiming headerContentOpacity; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipWidth; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipHeight; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipY; - MDMMotionTiming navigationBarY; -} CalendarChipTiming; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *chipContentOpacity; +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *headerContentOpacity; + +@property(nonatomic, strong, nonnull, readonly) MDMAnimationTraits *navigationBarY; + +@end @interface CalendarChipMotionSpec: NSObject -@property(nonatomic, class, readonly) CalendarChipTiming expansion; -@property(nonatomic, class, readonly) CalendarChipTiming collapse; +@property(nonatomic, class, strong, nonnull, readonly) id expansion; +@property(nonatomic, class, strong, nonnull, readonly) id collapse; // This object is not meant to be instantiated. -- (instancetype)init NS_UNAVAILABLE; +- (nonnull instancetype)init NS_UNAVAILABLE; @end diff --git a/examples/CalendarChipMotionSpec.m b/examples/CalendarChipMotionSpec.m index b0ef2b8..51bffc3 100644 --- a/examples/CalendarChipMotionSpec.m +++ b/examples/CalendarChipMotionSpec.m @@ -16,58 +16,84 @@ #import "CalendarChipMotionSpec.h" +static id StandardTimingCurve(void) { + return [CAMediaTimingFunction functionWithControlPoints:0.4f :0.0f :0.2f :1.0f]; +} + +static id LinearTimingCurve(void) { + return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; +} + +@interface CalendarChipExpansionTiming: NSObject +@end + +@implementation CalendarChipExpansionTiming + +- (MDMAnimationTraits *)chipWidth { + return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.285 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipHeight { + return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipY { + return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipContentOpacity { + return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.075 timingCurve:LinearTimingCurve()]; +} + +- (MDMAnimationTraits *)headerContentOpacity { + return [[MDMAnimationTraits alloc] initWithDelay:0.075 duration:0.150 timingCurve:LinearTimingCurve()]; +} + +- (MDMAnimationTraits *)navigationBarY { + return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.360 timingCurve:StandardTimingCurve()]; +} + +@end + +@interface CalendarChipCollapseTiming: NSObject +@end + +@implementation CalendarChipCollapseTiming + +- (MDMAnimationTraits *)chipWidth { + return [[MDMAnimationTraits alloc] initWithDelay:0.045 duration:0.330 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipHeight { + return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.330 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipY { + return [[MDMAnimationTraits alloc] initWithDelay:0.015 duration:0.330 timingCurve:StandardTimingCurve()]; +} + +- (MDMAnimationTraits *)chipContentOpacity { + return [[MDMAnimationTraits alloc] initWithDelay:0.150 duration:0.150 timingCurve:LinearTimingCurve()]; +} + +- (MDMAnimationTraits *)headerContentOpacity { + return [[MDMAnimationTraits alloc] initWithDelay:0.000 duration:0.075 timingCurve:LinearTimingCurve()]; +} + +- (MDMAnimationTraits *)navigationBarY { + return [[MDMAnimationTraits alloc] initWithDelay:0.045 duration:0.150 timingCurve:StandardTimingCurve()]; +} + +@end + @implementation CalendarChipMotionSpec -+ (MDMMotionCurve)eightyForty { - return MDMMotionCurveMakeBezier(0.4f, 0.0f, 0.2f, 1.0f); -} - -+ (CalendarChipTiming)expansion { - MDMMotionCurve eightyForty = [self eightyForty]; - return (CalendarChipTiming){ - .chipWidth = { - .delay = 0.000, .duration = 0.285, .curve = eightyForty, - }, - .chipHeight = { - .delay = 0.015, .duration = 0.360, .curve = eightyForty, - }, - .chipY = { - .delay = 0.015, .duration = 0.360, .curve = eightyForty, - }, - .chipContentOpacity = { - .delay = 0.000, .duration = 0.075, .curve = MDMLinearMotionCurve, - }, - .headerContentOpacity = { - .delay = 0.075, .duration = 0.150, .curve = MDMLinearMotionCurve, - }, - .navigationBarY = { - .delay = 0.015, .duration = 0.360, .curve = eightyForty, - }, - }; -} - -+ (CalendarChipTiming)collapse { - MDMMotionCurve eightyForty = [self eightyForty]; - return (CalendarChipTiming){ - .chipWidth = { - .delay = 0.045, .duration = 0.330, .curve = eightyForty, - }, - .chipHeight = { - .delay = 0.000, .duration = 0.330, .curve = eightyForty, - }, - .chipY = { - .delay = 0.015, .duration = 0.330, .curve = eightyForty, - }, - .chipContentOpacity = { - .delay = 0.150, .duration = 0.150, .curve = MDMLinearMotionCurve, - }, - .headerContentOpacity = { - .delay = 0.000, .duration = 0.075, .curve = MDMLinearMotionCurve, - }, - .navigationBarY = { - .delay = 0.045, .duration = 0.150, .curve = eightyForty, - } - }; ++ (id)expansion { + return [[CalendarChipExpansionTiming alloc] init]; +} + ++ (id)collapse { + return [[CalendarChipCollapseTiming alloc] init]; } @end diff --git a/examples/TapToBounceExample.swift b/examples/TapToBounceExample.swift index c4db306..0974145 100644 --- a/examples/TapToBounceExample.swift +++ b/examples/TapToBounceExample.swift @@ -40,21 +40,22 @@ class TapToBounceExampleViewController: UIViewController { for: [.touchUpInside, .touchUpOutside, .touchDragExit]) } - let timing = MotionTiming(delay: 0, - duration: 0.5, - curve: MotionCurveMakeSpring(mass: 1, tension: 100, friction: 10), - repetition: .init()) + let traits = MDMAnimationTraits(delay: 0, + duration: 0.5, + timingCurve: MDMSpringTimingCurve(mass: 1, + tension: 100, + friction: 10)) func didFocus(_ sender: UIButton) { let animator = MotionAnimator() - animator.animate(with: timing) { + animator.animate(with: traits) { sender.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) } } func didUnfocus(_ sender: UIButton) { let animator = MotionAnimator() - animator.animate(with: timing) { + animator.animate(with: traits) { sender.transform = .identity } } diff --git a/src/MDMMotionAnimator.h b/src/MDMMotionAnimator.h index 1ac537d..630577c 100644 --- a/src/MDMMotionAnimator.h +++ b/src/MDMMotionAnimator.h @@ -27,11 +27,13 @@ #import "MDMCoreAnimationTraceable.h" /** - An animator adds Core Animation animations to a layer based on a provided motion timing. + An animator adds Core Animation animations to a layer using animation traits. */ NS_SWIFT_NAME(MotionAnimator) @interface MDMMotionAnimator : NSObject +#pragma mark - Configuring animation behavior + /** The scaling factor to apply to all time-related values. @@ -41,15 +43,6 @@ NS_SWIFT_NAME(MotionAnimator) */ @property(nonatomic, assign) CGFloat timeScaleFactor; -/** - If enabled, explicitly-provided values will be reversed before animating. - - This property does not affect the animateWithTiming:animations: family of methods. - - Disabled by default. - */ -@property(nonatomic, assign) BOOL shouldReverseValues; - /** If enabled, all animations will start from their current presentation value. @@ -69,33 +62,35 @@ NS_SWIFT_NAME(MotionAnimator) */ @property(nonatomic, assign) BOOL additive; +#pragma mark - Explicitly animating between values + /** - Adds a single animation to the layer with the given timing structure. + Adds a single animation to the layer with the given traits structure. If `additive` is disabled, the animation will be added to the layer with the keyPath as its key. In this case, multiple invocations of this function on the same key path will remove the animations added from prior invocations. - @param timing The timing to be used for the animation. + @param traits The traits to be used for the animation. @param layer The layer to be animated. @param values The values to be used in the animation. Must contain exactly two values. Supported UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include UIColor and UIBezierPath. @param keyPath The key path of the property to be animated. */ -- (void)animateWithTiming:(MDMMotionTiming)timing - toLayer:(nonnull CALayer *)layer - withValues:(nonnull NSArray *)values +- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits + between:(nonnull NSArray *)values + layer:(nonnull CALayer *)layer keyPath:(nonnull MDMAnimatableKeyPath)keyPath; /** - Adds a single animation to the layer with the given timing structure. + Adds a single animation to the layer with the given traits structure. If `additive` is disabled, the animation will be added to the layer with the keyPath as its key. In this case, multiple invocations of this function on the same key path will remove the animations added from prior invocations. - @param timing The timing to be used for the animation. + @param traits The traits to be used for the animation. @param layer The layer to be animated. @param values The values to be used in the animation. Must contain exactly two values. Supported UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include @@ -103,39 +98,55 @@ NS_SWIFT_NAME(MotionAnimator) @param keyPath The key path of the property to be animated. @param completion A block object to be executed when the animation ends or is removed from the animation hierarchy. If the duration of the animation is 0, this block is executed immediately. - The block is escaping and will be released once the animations have completed. + The block is escaping and will be released once the animations have completed. The provided + didComplete argument is currently always YES. */ -- (void)animateWithTiming:(MDMMotionTiming)timing - toLayer:(nonnull CALayer *)layer - withValues:(nonnull NSArray *)values +- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits + between:(nonnull NSArray *)values + layer:(nonnull CALayer *)layer keyPath:(nonnull MDMAnimatableKeyPath)keyPath - completion:(nullable void(^)(void))completion; + completion:(nullable void(^)(BOOL didComplete))completion; + +/** + If enabled, explicitly-provided values will be reversed before animating. + + This property only affects the animateWithTraits:between:... family of methods. + + Disabled by default. + */ +@property(nonatomic, assign) BOOL shouldReverseValues; + +#pragma mark - Implicitly animating /** - Performs `animations` using the timing provided. + Performs `animations` using the traits provided. - @param timing The timing to be used for the animation. + @param traits The traits to be used for the animation. @param animations The block to be executed. Any animatable properties changed within this block - will result in animations being added to the view's layer with the provided timing. The block is + will result in animations being added to the view's layer with the provided traits. The block is non-escaping. */ -- (void)animateWithTiming:(MDMMotionTiming)timing animations:(nonnull void(^)(void))animations; +- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits + animations:(nonnull void(^)(void))animations; /** - Performs `animations` using the timing provided and executes the completion handler once all added + Performs `animations` using the traits provided and executes the completion handler once all added animations have completed. - @param timing The timing to be used for the animation. + @param traits The traits to be used for the animation. @param animations The block to be executed. Any animatable properties changed within this block - will result in animations being added to the view's layer with the provided timing. The block is + will result in animations being added to the view's layer with the provided traits. The block is non-escaping. @param completion A block object to be executed once the animation sequence ends or it has been removed from the animation hierarchy. If the duration of the animation is 0, this block is executed - immediately. The block is escaping and will be released once the animation sequence has completed. + immediately. The block is escaping and will be released once the animation sequence has completed. The provided + didComplete argument is currently always YES. */ -- (void)animateWithTiming:(MDMMotionTiming)timing +- (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits animations:(nonnull void (^)(void))animations - completion:(nullable void(^)(void))completion; + completion:(nullable void(^)(BOOL didComplete))completion; + +#pragma mark - Managing active animations /** Removes every animation added by this animator. @@ -156,6 +167,40 @@ NS_SWIFT_NAME(MotionAnimator) @end +@interface MDMMotionAnimator (Legacy) + +/** + To be deprecated. Use animateWithTraits:between:layer:keyPath instead. + */ +- (void)animateWithTiming:(MDMMotionTiming)timing + toLayer:(nonnull CALayer *)layer + withValues:(nonnull NSArray *)values + keyPath:(nonnull MDMAnimatableKeyPath)keyPath; + +/** + To be deprecated. Use animateWithTraits:between:layer:keyPath:completion: instead. + */ +- (void)animateWithTiming:(MDMMotionTiming)timing + toLayer:(nonnull CALayer *)layer + withValues:(nonnull NSArray *)values + keyPath:(nonnull MDMAnimatableKeyPath)keyPath + completion:(nullable void(^)(void))completion; + +/** + To be deprecated. Use animateWithTraits:animations: instead. + */ +- (void)animateWithTiming:(MDMMotionTiming)timing + animations:(nonnull void(^)(void))animations; + +/** + To be deprecated. Use animateWithTraits:animations:completion: instead. + */ +- (void)animateWithTiming:(MDMMotionTiming)timing + animations:(nonnull void (^)(void))animations + completion:(nullable void(^)(void))completion; + +@end + @interface MDMMotionAnimator (ImplicitLayerAnimations) /** diff --git a/src/MDMMotionAnimator.m b/src/MDMMotionAnimator.m index f4a4c0f..310f690 100644 --- a/src/MDMMotionAnimator.m +++ b/src/MDMMotionAnimator.m @@ -40,18 +40,18 @@ - (instancetype)init { return self; } -- (void)animateWithTiming:(MDMMotionTiming)timing - toLayer:(CALayer *)layer - withValues:(NSArray *)values +- (void)animateWithTraits:(MDMAnimationTraits *)traits + between:(NSArray *)values + layer:(CALayer *)layer keyPath:(MDMAnimatableKeyPath)keyPath { - [self animateWithTiming:timing toLayer:layer withValues:values keyPath:keyPath completion:nil]; + [self animateWithTraits:traits between:values layer:layer keyPath:keyPath completion:nil]; } -- (void)animateWithTiming:(MDMMotionTiming)timing - toLayer:(CALayer *)layer - withValues:(NSArray *)values +- (void)animateWithTraits:(MDMAnimationTraits *)traits + between:(NSArray *)values + layer:(CALayer *)layer keyPath:(MDMAnimatableKeyPath)keyPath - completion:(void(^)(void))completion { + completion:(void(^)(BOOL))completion { NSAssert([values count] == 2, @"The values array must contain exactly two values."); if (_shouldReverseValues) { @@ -70,7 +70,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing commitToModelLayer(); if (completion) { - completion(); + completion(YES); } }; @@ -80,7 +80,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing return; } - CABasicAnimation *animation = MDMAnimationFromTiming(timing, timeScaleFactor); + CABasicAnimation *animation = MDMAnimationFromTraits(traits, timeScaleFactor); if (animation == nil) { exitEarly(); @@ -92,7 +92,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing [self addAnimation:animation toLayer:layer withKeyPath:keyPath - timing:timing + traits:traits timeScaleFactor:timeScaleFactor destination:[values lastObject] initialValue:^(BOOL wantsPresentationValue) { @@ -115,13 +115,13 @@ - (void)animateWithTiming:(MDMMotionTiming)timing } } -- (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations { - [self animateWithTiming:timing animations:animations completion:nil]; +- (void)animateWithTraits:(MDMAnimationTraits *)traits animations:(void (^)(void))animations { + [self animateWithTraits:traits animations:animations completion:nil]; } -- (void)animateWithTiming:(MDMMotionTiming)timing +- (void)animateWithTraits:(MDMAnimationTraits *)traits animations:(void (^)(void))animations - completion:(void(^)(void))completion { + completion:(void(^)(BOOL))completion { NSArray *actions = MDMAnimateImplicitly(animations); void (^exitEarly)(void) = ^{ @@ -131,7 +131,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing [CATransaction commit]; if (completion) { - completion(); + completion(YES); } }; @@ -142,14 +142,18 @@ - (void)animateWithTiming:(MDMMotionTiming)timing } // We'll reuse this animation template for each action. - CABasicAnimation *animationTemplate = MDMAnimationFromTiming(timing, timeScaleFactor); + CABasicAnimation *animationTemplate = MDMAnimationFromTraits(traits, timeScaleFactor); if (animationTemplate == nil) { exitEarly(); return; } [CATransaction begin]; - [CATransaction setCompletionBlock:completion]; + if (completion) { + [CATransaction setCompletionBlock:^{ + completion(YES); + }]; + } for (MDMImplicitAction *action in actions) { CABasicAnimation *animation = [animationTemplate copy]; @@ -157,7 +161,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing [self addAnimation:animation toLayer:action.layer withKeyPath:action.keyPath - timing:timing + traits:traits timeScaleFactor:timeScaleFactor destination:[action.layer valueForKeyPath:action.keyPath] initialValue:^(BOOL wantsPresentationValue) { @@ -193,6 +197,45 @@ - (void)stopAllAnimations { [_registrar removeAllAnimations]; } +#pragma mark - Legacy + +- (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations { + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing]; + [self animateWithTraits:traits animations:animations]; +} + +- (void)animateWithTiming:(MDMMotionTiming)timing + animations:(void (^)(void))animations + completion:(void (^)(void))completion { + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing]; + [self animateWithTraits:traits animations:animations completion:^(BOOL didComplete) { + completion(); + }]; +} + +- (void)animateWithTiming:(MDMMotionTiming)timing + toLayer:(CALayer *)layer + withValues:(NSArray *)values + keyPath:(MDMAnimatableKeyPath)keyPath { + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing]; + [self animateWithTraits:traits between:values layer:layer keyPath:keyPath]; +} + +- (void)animateWithTiming:(MDMMotionTiming)timing + toLayer:(CALayer *)layer + withValues:(NSArray *)values + keyPath:(MDMAnimatableKeyPath)keyPath + completion:(void (^)(void))completion { + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing]; + [self animateWithTraits:traits + between:values + layer:layer + keyPath:keyPath + completion:^(BOOL didComplete) { + completion(); + }]; +} + #pragma mark - Private - (CGFloat)computedTimeScaleFactor { @@ -214,11 +257,11 @@ - (CGFloat)computedTimeScaleFactor { - (void)addAnimation:(CABasicAnimation *)animation toLayer:(CALayer *)layer withKeyPath:(NSString *)keyPath - timing:(MDMMotionTiming)timing + traits:(MDMAnimationTraits *)traits timeScaleFactor:(CGFloat)timeScaleFactor destination:(id)destination initialValue:(id(^)(BOOL wantsPresentationValue))initialValueBlock - completion:(void(^)(void))completion { + completion:(void(^)(BOOL))completion { // Must configure the keyPath and toValue before we can identify whether the animation supports // being additive. animation.keyPath = keyPath; @@ -237,11 +280,11 @@ - (void)addAnimation:(CABasicAnimation *)animation NSString *key = animation.additive ? nil : keyPath; - MDMConfigureAnimation(animation, timing); + MDMConfigureAnimation(animation, traits); - if (timing.delay != 0) { + if (traits.delay != 0) { animation.beginTime = ([layer convertTime:CACurrentMediaTime() fromLayer:nil] - + timing.delay * timeScaleFactor); + + traits.delay * timeScaleFactor); animation.fillMode = kCAFillModeBackwards; } diff --git a/src/private/CABasicAnimation+MotionAnimator.h b/src/private/CABasicAnimation+MotionAnimator.h index b3e0ce6..c3a29f6 100644 --- a/src/private/CABasicAnimation+MotionAnimator.h +++ b/src/private/CABasicAnimation+MotionAnimator.h @@ -20,9 +20,9 @@ #import #import -// Returns a basic animation configured with the provided timing and scale factor. +// Returns a basic animation configured with the provided traits and scale factor. FOUNDATION_EXPORT -CABasicAnimation *MDMAnimationFromTiming(MDMMotionTiming timing, CGFloat timeScaleFactor); +CABasicAnimation *MDMAnimationFromTraits(MDMAnimationTraits *traits, CGFloat timeScaleFactor); // Returns a Boolean indicating whether or not an animation with the given key path and toValue // can be animated additively. @@ -33,4 +33,4 @@ FOUNDATION_EXPORT BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue); // // Not all animation value types support being additive. If an animation's value type was not // supported, the animation's values will not be modified. -FOUNDATION_EXPORT void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing); +FOUNDATION_EXPORT void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits *traits); diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index e3cf2e7..8557b21 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -65,44 +65,39 @@ static BOOL IsAnimationKeyPathAlwaysNonAdditive(NSString *keyPath) { #pragma mark - Public -CABasicAnimation *MDMAnimationFromTiming(MDMMotionTiming timing, CGFloat timeScaleFactor) { - CABasicAnimation *animation; - switch (timing.curve.type) { - case MDMMotionCurveTypeInstant: - animation = nil; - break; +CABasicAnimation *MDMAnimationFromTraits(MDMAnimationTraits *traits, CGFloat timeScaleFactor) { + if (traits.timingCurve == nil) { + return nil; + } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - case MDMMotionCurveTypeDefault: -#pragma clang diagnostic pop - case MDMMotionCurveTypeBezier: - animation = [CABasicAnimation animation]; - animation.timingFunction = MDMTimingFunctionWithControlPoints(timing.curve.data); - animation.duration = timing.duration * timeScaleFactor; + if ([traits.timingCurve isKindOfClass:[CAMediaTimingFunction class]]) { + CFTimeInterval duration = traits.duration * timeScaleFactor; + if (duration == 0) { + return nil; + } + CABasicAnimation *animation = [CABasicAnimation animation]; + animation.timingFunction = (CAMediaTimingFunction *)traits.timingCurve; + animation.duration = duration; + return animation; + } - if (animation.duration == 0) { - return nil; - } - break; + if ([traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]]) { + MDMSpringTimingCurve *springTiming = (MDMSpringTimingCurve *)traits.timingCurve; - case MDMMotionCurveTypeSpring: { #pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. #pragma clang diagnostic ignored "-Wpartial-availability" - CASpringAnimation *spring = [CASpringAnimation animation]; + CASpringAnimation *animation = [CASpringAnimation animation]; #pragma clang diagnostic pop - spring.mass = timing.curve.data[MDMSpringMotionCurveDataIndexMass]; - spring.stiffness = timing.curve.data[MDMSpringMotionCurveDataIndexTension]; - spring.damping = timing.curve.data[MDMSpringMotionCurveDataIndexFriction]; - spring.duration = timing.duration; - - animation = spring; - break; - } + animation.mass = springTiming.mass; + animation.stiffness = springTiming.tension; + animation.damping = springTiming.friction; + animation.duration = traits.duration; + return animation; } - return animation; + + return nil; } BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue) { @@ -115,8 +110,18 @@ BOOL MDMCanAnimationBeAdditive(NSString *keyPath, id toValue) { || IsCATransform3DType(toValue)); } -void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) { - if (!animation.additive && timing.curve.type != MDMMotionCurveTypeSpring) { +void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * traits) { +#pragma clang diagnostic push + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. +#pragma clang diagnostic ignored "-Wpartial-availability" + BOOL isSpringAnimation = ([animation isKindOfClass:[CASpringAnimation class]] + && [traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]]); + MDMSpringTimingCurve *springTimingCurve = (MDMSpringTimingCurve *)traits.timingCurve; + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop + + if (!animation.additive && !isSpringAnimation) { return; // Nothing to do here. } @@ -158,17 +163,10 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) animation.toValue = @0; } -#pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. -#pragma clang diagnostic ignored "-Wpartial-availability" - if ([animation isKindOfClass:[CASpringAnimation class]]) { - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop - - CGFloat absoluteInitialVelocity = timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; + if (isSpringAnimation) { + CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; - // Our timing's initialVelocity is in points per second, but Core Animation expects initial + // Our traits's initialVelocity is in points per second, but Core Animation expects initial // velocity to be in terms of displacement per second. // // From the UIView animateWithDuration header docs: @@ -220,13 +218,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) animation.toValue = [NSValue valueWithCGSize:CGSizeZero]; } -#pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. -#pragma clang diagnostic ignored "-Wpartial-availability" - if ([animation isKindOfClass:[CASpringAnimation class]]) { - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop + if (isSpringAnimation) { // Core Animation's velocity system is single dimensional, so we pick the dominant direction // of movement and normalize accordingly. CGFloat biggestDelta; @@ -236,8 +228,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) biggestDelta = additiveDisplacement.height; } CGFloat displacement = -biggestDelta; - CGFloat absoluteInitialVelocity = - timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; + CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; if (fabs(displacement) > 0.00001) { springAnimation.initialVelocity = absoluteInitialVelocity / displacement; } @@ -253,13 +244,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) animation.toValue = [NSValue valueWithCGPoint:CGPointZero]; } -#pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. -#pragma clang diagnostic ignored "-Wpartial-availability" - if ([animation isKindOfClass:[CASpringAnimation class]]) { - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop + if (isSpringAnimation) { // Core Animation's velocity system is single dimensional, so we pick the dominant direction // of movement and normalize accordingly. CGFloat biggestDelta; @@ -269,8 +254,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) biggestDelta = additiveDisplacement.y; } CGFloat displacement = -biggestDelta; - CGFloat absoluteInitialVelocity = - timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; + CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; if (fabs(displacement) > 0.00001) { springAnimation.initialVelocity = absoluteInitialVelocity / displacement; } @@ -287,13 +271,7 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMMotionTiming timing) } } - // Update the animation's duration to match the proposed settling duration. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - if ([animation isKindOfClass:[CASpringAnimation class]]) { - CASpringAnimation *springAnimation = (CASpringAnimation *)animation; -#pragma clang diagnostic pop - + if (isSpringAnimation) { // This API is only available on iOS 9+ if ([springAnimation respondsToSelector:@selector(settlingDuration)]) { animation.duration = springAnimation.settlingDuration; diff --git a/src/private/MDMAnimationRegistrar.h b/src/private/MDMAnimationRegistrar.h index 7aaadf5..96e567f 100644 --- a/src/private/MDMAnimationRegistrar.h +++ b/src/private/MDMAnimationRegistrar.h @@ -26,7 +26,7 @@ - (void)addAnimation:(nonnull CABasicAnimation *)animation toLayer:(nonnull CALayer *)layer forKey:(nonnull NSString *)key - completion:(void(^ __nullable)(void))completion; + completion:(void(^ __nullable)(BOOL))completion; // For every active animation, reads the associated layer's presentation layer key path and writes // it to the layer. diff --git a/src/private/MDMAnimationRegistrar.m b/src/private/MDMAnimationRegistrar.m index 01a6679..c1cade8 100644 --- a/src/private/MDMAnimationRegistrar.m +++ b/src/private/MDMAnimationRegistrar.m @@ -54,7 +54,7 @@ - (void)forEachAnimation:(void (^)(CALayer *, CABasicAnimation *, NSString *))wo - (void)addAnimation:(CABasicAnimation *)animation toLayer:(CALayer *)layer forKey:(NSString *)key - completion:(void(^)(void))completion { + completion:(void(^)(BOOL))completion { if (key == nil) { key = [NSUUID UUID].UUIDString; } @@ -73,7 +73,7 @@ - (void)addAnimation:(CABasicAnimation *)animation [animatedKeyPaths removeObject:keyPathAnimation]; if (completion) { - completion(); + completion(YES); } }]; diff --git a/src/private/MDMUIKitValueCoercion.h b/src/private/MDMUIKitValueCoercion.h index 3c50194..972e81c 100644 --- a/src/private/MDMUIKitValueCoercion.h +++ b/src/private/MDMUIKitValueCoercion.h @@ -16,10 +16,11 @@ #import -// Coerces the following UIKit values to Core Animation values: +// Coerces the following UIKit/CoreGraphics values to Core Animation values: // // - UIBezierPath -> CGPath // - UIColor -> CGColor +// - CGAffineTransform -> CATransform3D // // @param values All values of this array must be the same type. FOUNDATION_EXPORT NSArray* MDMCoerceUIKitValuesToCoreAnimationValues(NSArray *values); diff --git a/tests/unit/AdditiveAnimatorTests.swift b/tests/unit/AdditiveAnimatorTests.swift index f4e60ea..1a13169 100644 --- a/tests/unit/AdditiveAnimatorTests.swift +++ b/tests/unit/AdditiveAnimatorTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class AdditiveAnimationTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! override func setUp() { @@ -33,10 +33,7 @@ class AdditiveAnimationTests: XCTestCase { animator.additive = true - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 1, p2y: 1), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -49,14 +46,15 @@ class AdditiveAnimationTests: XCTestCase { override func tearDown() { animator = nil - timing = nil + traits = nil view = nil super.tearDown() } func testNumericKeyPathsAnimateAdditively() { - animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: traits, between: [1, 0], + layer: view.layer, keyPath: .cornerRadius) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -75,9 +73,9 @@ class AdditiveAnimationTests: XCTestCase { } func testCGSizeKeyPathsAnimateAdditively() { - animator.animate(with: timing, to: view.layer, - withValues: [CGSize(width: 0, height: 0), - CGSize(width: 1, height: 2)], keyPath: .shadowOffset) + animator.animate(with: traits, between: [CGSize(width: 0, height: 0), + CGSize(width: 1, height: 2)], + layer: view.layer, keyPath: .shadowOffset) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -96,9 +94,8 @@ class AdditiveAnimationTests: XCTestCase { } func testCGPointKeyPathsAnimateAdditively() { - animator.animate(with: timing, to: view.layer, - withValues: [CGPoint(x: 0, y: 0), - CGPoint(x: 1, y: 2)], keyPath: .position) + animator.animate(with: traits, between: [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 2)], + layer: view.layer, keyPath: .position) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") diff --git a/tests/unit/AnimationRemovalTests.swift b/tests/unit/AnimationRemovalTests.swift index 197c8d2..e62c117 100644 --- a/tests/unit/AnimationRemovalTests.swift +++ b/tests/unit/AnimationRemovalTests.swift @@ -24,7 +24,7 @@ import MotionAnimator class AnimationRemovalTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! var originalImplementation: IMP? @@ -32,10 +32,7 @@ class AnimationRemovalTests: XCTestCase { super.setUp() animator = MotionAnimator() - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -48,15 +45,15 @@ class AnimationRemovalTests: XCTestCase { override func tearDown() { animator = nil - timing = nil + traits = nil view = nil super.tearDown() } func testAllAdditiveAnimationsGetsRemoved() { - animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) + animator.animate(with: traits, between: [1, 0], layer: view.layer, keyPath: .cornerRadius) + animator.animate(with: traits, between: [0, 0.5], layer: view.layer, keyPath: .cornerRadius) XCTAssertEqual(view.layer.animationKeys()!.count, 2) @@ -73,8 +70,8 @@ class AnimationRemovalTests: XCTestCase { didComplete = true } - animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .cornerRadius) + animator.animate(with: traits, between: [1, 0], layer: view.layer, keyPath: .cornerRadius) + animator.animate(with: traits, between: [0, 0.5], layer: view.layer, keyPath: .cornerRadius) CATransaction.commit() diff --git a/tests/unit/BeginFromCurrentStateTests.swift b/tests/unit/BeginFromCurrentStateTests.swift index 7117089..86ad3b8 100644 --- a/tests/unit/BeginFromCurrentStateTests.swift +++ b/tests/unit/BeginFromCurrentStateTests.swift @@ -24,7 +24,7 @@ import MotionAnimator class BeginFromCurrentStateTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! var addedAnimations: [CAAnimation]! @@ -35,10 +35,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.beginFromCurrentState = true - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -56,7 +53,7 @@ class BeginFromCurrentStateTests: XCTestCase { override func tearDown() { animator = nil - timing = nil + traits = nil view = nil addedAnimations = nil @@ -68,7 +65,8 @@ class BeginFromCurrentStateTests: XCTestCase { animator.additive = false - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: traits, between: [0, 0.5], + layer: view.layer, keyPath: .opacity) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -103,7 +101,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.additive = false - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0.5 } @@ -138,7 +136,8 @@ class BeginFromCurrentStateTests: XCTestCase { func testExplicitlyAnimatesFromPresentationValue() { animator.additive = false - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: traits, between: [0, 0.5], + layer: view.layer, keyPath: .opacity) RunLoop.main.run(until: .init(timeIntervalSinceNow: 0.01)) XCTAssertNotNil(view.layer.presentation(), "No presentation layer found.") @@ -147,7 +146,8 @@ class BeginFromCurrentStateTests: XCTestCase { } let initialValue = presentation.opacity - animator.animate(with: timing, to: view.layer, withValues: [0, 0.2], keyPath: .opacity) + animator.animate(with: traits, between: [0, 0.2], + layer: view.layer, keyPath: .opacity) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -180,7 +180,8 @@ class BeginFromCurrentStateTests: XCTestCase { func testImplicitlyAnimatesFromPresentationValue() { animator.additive = false - animator.animate(with: timing, to: view.layer, withValues: [0, 0.5], keyPath: .opacity) + animator.animate(with: traits, between: [0, 0.5], + layer: view.layer, keyPath: .opacity) RunLoop.main.run(until: .init(timeIntervalSinceNow: 0.01)) @@ -190,7 +191,7 @@ class BeginFromCurrentStateTests: XCTestCase { } let initialValue = presentation.opacity - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0.2 } @@ -226,7 +227,7 @@ class BeginFromCurrentStateTests: XCTestCase { animator.beginFromCurrentState = true animator.additive = false - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0.5 } @@ -234,7 +235,7 @@ class BeginFromCurrentStateTests: XCTestCase { let initialValue = view.layer.presentation()!.opacity - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 1.0 } diff --git a/tests/unit/HeadlessLayerImplicitAnimationTests.swift b/tests/unit/HeadlessLayerImplicitAnimationTests.swift index 7b0e4f6..a811698 100644 --- a/tests/unit/HeadlessLayerImplicitAnimationTests.swift +++ b/tests/unit/HeadlessLayerImplicitAnimationTests.swift @@ -97,7 +97,7 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { // Verifies the somewhat counter-intuitive fact that CATransaction's animation duration always // takes precedence over UIView's animation duration. This means that animating a headless layer - // using UIView animation APIs may not result in the expected timings. + // using UIView animation APIs may not result in the expected traits. func testCATransactionTimingTakesPrecedenceOverUIViewTimingOutside() { CATransaction.begin() CATransaction.setAnimationDuration(0.2) @@ -146,18 +146,19 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { func testAnimatorTimingTakesPrecedenceOverCATransactionTiming() { let animator = MotionAnimator() animator.additive = false - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) - animator.animate(with: timing) { + let traits = MDMAnimationTraits(duration: 1) + + CATransaction.begin() + CATransaction.setAnimationDuration(0.5) + animator.animate(with: traits) { self.layer.opacity = 0.5 } + CATransaction.commit() let animation = layer.animation(forKey: "opacity") as! CABasicAnimation XCTAssertEqual(animation.keyPath, "opacity") - XCTAssertEqual(animation.duration, timing.duration) + XCTAssertEqual(animation.duration, traits.duration) } // MARK: Deprecated tests. @@ -204,12 +205,9 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { let animator = MotionAnimator() animator.additive = false - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) - animator.animate(with: timing) { + animator.animate(with: traits) { self.layer.opacity = 0.5 } diff --git a/tests/unit/ImplicitAnimationTests.swift b/tests/unit/ImplicitAnimationTests.swift index 80eab59..61cf5ec 100644 --- a/tests/unit/ImplicitAnimationTests.swift +++ b/tests/unit/ImplicitAnimationTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class ImplicitAnimationTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! var addedAnimations: [CAAnimation]! @@ -34,10 +34,7 @@ class ImplicitAnimationTests: XCTestCase { animator = MotionAnimator() animator.additive = false - timing = MotionTiming(delay: 0, - duration: 0.7, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 1, p2y: 1), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -69,7 +66,7 @@ class ImplicitAnimationTests: XCTestCase { } func testNoActionAddsNoAnimations() { - animator.animate(with: timing) { + animator.animate(with: traits) { // No-op } @@ -77,7 +74,7 @@ class ImplicitAnimationTests: XCTestCase { } func testOneActionAddsOneAnimation() { - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 } @@ -86,18 +83,21 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.opacity.rawValue) XCTAssertEqual(animation.fromValue as! CGFloat, 1) XCTAssertEqual(animation.toValue as! CGFloat, 0) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } func testTwoActionsAddsTwoAnimations() { - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 self.view.center = .init(x: 50, y: 50) } @@ -110,14 +110,17 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.opacity.rawValue) XCTAssertEqual(animation.fromValue as! CGFloat, 1) XCTAssertEqual(animation.toValue as! CGFloat, 0) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } do { let animation = addedAnimations[1] as! CABasicAnimation @@ -125,19 +128,22 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertEqual(animation.keyPath, AnimatableKeyPath.position.rawValue) XCTAssertEqual(animation.fromValue as! CGPoint, .init(x: 0, y: 0)) XCTAssertEqual(animation.toValue as! CGPoint, .init(x: 50, y: 50)) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } } func testFrameActionAddsTwoAnimations() { - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.frame = .init(x: 0, y: 0, width: 100, height: 100) } @@ -150,14 +156,17 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertFalse(animation.isAdditive) XCTAssertEqual(animation.fromValue as! CGPoint, .init(x: 0, y: 0)) XCTAssertEqual(animation.toValue as! CGPoint, .init(x: 50, y: 50)) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } do { let animation = addedAnimations @@ -166,14 +175,17 @@ class ImplicitAnimationTests: XCTestCase { XCTAssertFalse(animation.isAdditive) XCTAssertEqual(animation.fromValue as! CGRect, .init(x: 0, y: 0, width: 0, height: 0)) XCTAssertEqual(animation.toValue as! CGRect, .init(x: 0, y: 0, width: 100, height: 100)) - XCTAssertEqual(animation.duration, timing.duration) - - let addedCurve = MotionCurve(fromTimingFunction: animation.timingFunction!) - XCTAssertEqual(addedCurve.type, timing.curve.type) - XCTAssertEqual(addedCurve.data.0, timing.curve.data.0) - XCTAssertEqual(addedCurve.data.1, timing.curve.data.1) - XCTAssertEqual(addedCurve.data.2, timing.curve.data.2) - XCTAssertEqual(addedCurve.data.3, timing.curve.data.3) + XCTAssertEqual(animation.duration, traits.duration) + + let timingCurve = traits.timingCurve as! CAMediaTimingFunction + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.x, animation.timingFunction!.mdm_point1.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point1.y, animation.timingFunction!.mdm_point1.y, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.x, animation.timingFunction!.mdm_point2.x, + accuracy: 0.001) + XCTAssertEqualWithAccuracy(timingCurve.mdm_point2.y, animation.timingFunction!.mdm_point2.y, + accuracy: 0.001) } } @@ -181,7 +193,7 @@ class ImplicitAnimationTests: XCTestCase { CATransaction.begin() CATransaction.setDisableActions(true) - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 } @@ -211,9 +223,9 @@ class ImplicitAnimationTests: XCTestCase { } func testDurationOfZeroRunsAnimationsBlockButGeneratesNoAnimations() { - timing.duration = 0 + let traits = MDMAnimationTraits(duration: 0) - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 } @@ -224,7 +236,7 @@ class ImplicitAnimationTests: XCTestCase { func testTimeScaleFactorOfZeroRunsAnimationsBlockButGeneratesNoAnimations() { animator.timeScaleFactor = 0 - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.alpha = 0 } @@ -233,7 +245,7 @@ class ImplicitAnimationTests: XCTestCase { } func testUnsupportedAnimationKeyIsNotAnimated() { - animator.animate(with: timing) { + animator.animate(with: traits) { self.view.layer.sublayers = [] } diff --git a/tests/unit/InitialVelocityTests.swift b/tests/unit/InitialVelocityTests.swift index 06378f9..f432606 100644 --- a/tests/unit/InitialVelocityTests.swift +++ b/tests/unit/InitialVelocityTests.swift @@ -135,18 +135,17 @@ class InitialVelocityTests: XCTestCase { } private func animate(from: CGFloat, to: CGFloat, withVelocity velocity: CGFloat) { - let timing = MotionTiming(delay: 0, - duration: 0.7, - curve: .init(type: .spring, data: (1, 1, 1, velocity)), - repetition: .init(type: .none, amount: 0, autoreverses: false)) - animator.animate(with: timing, to: CALayer(), withValues: [from, to], - keyPath: .opacity) - animator.animate(with: timing, to: CALayer(), withValues: [CGPoint(x: from, y: from), - CGPoint(x: to, y: to)], - keyPath: .position) - animator.animate(with: timing, to: CALayer(), withValues: [CGSize(width: from, height: from), - CGSize(width: to, height: to)], - keyPath: .init(rawValue: "bounds.size")) + let springCurve = MDMSpringTimingCurve(mass: 1, tension: 1, friction: 1, + initialVelocity: velocity) + let traits = MDMAnimationTraits(delay: 0, duration: 0.7, timingCurve: springCurve) + animator.animate(with: traits, between: [from, to], + layer: CALayer(), keyPath: .opacity) + animator.animate(with: traits, between: [CGPoint(x: from, y: from), + CGPoint(x: to, y: to)], + layer: CALayer(), keyPath: .position) + animator.animate(with: traits, between: [CGSize(width: from, height: from), + CGSize(width: to, height: to)], + layer: CALayer(), keyPath: .init(rawValue: "bounds.size")) } } diff --git a/tests/unit/InstantAnimationTests.swift b/tests/unit/InstantAnimationTests.swift index 3a40426..5403947 100644 --- a/tests/unit/InstantAnimationTests.swift +++ b/tests/unit/InstantAnimationTests.swift @@ -24,7 +24,6 @@ import MotionAnimator class InstantAnimationTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! var view: UIView! var addedAnimations: [CAAnimation]! @@ -33,11 +32,6 @@ class InstantAnimationTests: XCTestCase { animator = MotionAnimator() - timing = MotionTiming(delay: 0, - duration: 0, - curve: .init(type: .instant, data: (0, 0, 0, 0)), - repetition: .init(type: .none, amount: 0, autoreverses: false)) - let window = UIWindow() window.makeKeyAndVisible() view = UIView() // Need to animate a view's layer to get implicit animations. @@ -61,18 +55,43 @@ class InstantAnimationTests: XCTestCase { } func testDoesNotGenerateImplicitAnimations() { - animator.animate(with: timing, to: view.layer, withValues: [1, 0.5], keyPath: .opacity) + let traits = MDMAnimationTraits(duration: 0) + + animator.animate(with: traits, between: [1, 0.5], + layer: view.layer, keyPath: .opacity) XCTAssertNil(view.layer.animationKeys()) XCTAssertEqual(addedAnimations.count, 0) } func testDoesNotGenerateImplicitAnimationsInUIViewAnimationBlock() { + let traits = MDMAnimationTraits(duration: 0) + + UIView.animate(withDuration: 0.5) { + self.animator.animate(with: traits, between: [1, 0.5], + layer: self.view.layer, keyPath: .opacity) + } + + XCTAssertNil(view.layer.animationKeys()) + XCTAssertEqual(addedAnimations.count, 0) + } + + func testDoesNotGenerateImplicitAnimationsWithNilCurve() { + let traits = MDMAnimationTraits(delay: 0, duration: 0.5, timingCurve: nil) + + animator.animate(with: traits, between: [1, 0.5], + layer: view.layer, keyPath: .opacity) + + XCTAssertNil(view.layer.animationKeys()) + XCTAssertEqual(addedAnimations.count, 0) + } + + func testDoesNotGenerateImplicitAnimationsInUIViewAnimationBlockWithNilCurve() { + let traits = MDMAnimationTraits(delay: 0, duration: 0.5, timingCurve: nil) + UIView.animate(withDuration: 0.5) { - self.animator.animate(with: self.timing, - to: self.view.layer, - withValues: [1, 0.5], - keyPath: .opacity) + self.animator.animate(with: traits, between: [1, 0.5], + layer: self.view.layer, keyPath: .opacity) } XCTAssertNil(view.layer.animationKeys()) diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index 5789f0c..e7c64d3 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class AnimatorBehavioralTests: XCTestCase { var window: UIWindow! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var originalImplementation: IMP? override func setUp() { @@ -32,14 +32,11 @@ class AnimatorBehavioralTests: XCTestCase { window = UIWindow() window.makeKeyAndVisible() - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) } override func tearDown() { - timing = nil + traits = nil window = nil super.tearDown() @@ -75,10 +72,8 @@ class AnimatorBehavioralTests: XCTestCase { let animator = MotionAnimator() let initialValue = view.layer.value(forKeyPath: keyPath.rawValue) ?? NSNull() - animator.animate(with: timing, - to: view.layer, - withValues: [initialValue, value], - keyPath: keyPath) + animator.animate(with: traits, between: [initialValue, value], + layer: view.layer, keyPath: keyPath) XCTAssertNotNil(view.layer.animationKeys(), "Expected \(keyPath.rawValue) to generate animations with the following " @@ -99,7 +94,7 @@ class AnimatorBehavioralTests: XCTestCase { CATransaction.flush() let animator = MotionAnimator() - animator.animate(with: timing) { + animator.animate(with: traits) { view.layer.setValue(value, forKeyPath: keyPath.rawValue) } @@ -123,10 +118,8 @@ class AnimatorBehavioralTests: XCTestCase { let animator = MotionAnimator() let initialValue = layer.value(forKeyPath: keyPath.rawValue) ?? NSNull() - animator.animate(with: timing, - to: layer, - withValues: [initialValue, value], - keyPath: keyPath) + animator.animate(with: traits, between: [initialValue, value], + layer: layer, keyPath: keyPath) XCTAssertNotNil(layer.animationKeys(), "Expected \(keyPath.rawValue) to generate animations with the following " @@ -147,7 +140,7 @@ class AnimatorBehavioralTests: XCTestCase { CATransaction.flush() let animator = MotionAnimator() - animator.animate(with: timing) { + animator.animate(with: traits) { layer.setValue(value, forKeyPath: keyPath.rawValue) } diff --git a/tests/unit/MotionAnimatorTests.m b/tests/unit/MotionAnimatorTests.m index 10a0eac..509eeef 100644 --- a/tests/unit/MotionAnimatorTests.m +++ b/tests/unit/MotionAnimatorTests.m @@ -27,13 +27,11 @@ - (void)testNoDurationSetsValueInstantly { CALayer *layer = [[CALayer alloc] init]; - MDMMotionTiming timing = { - .duration = 0, - }; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; layer.opacity = 0.5; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity"]; + [animator animateWithTraits:traits between:@[ @0, @1 ] layer:layer keyPath:@"opacity"]; XCTAssertEqual(layer.opacity, 1); } @@ -43,14 +41,14 @@ - (void)testNoDurationCallsCompletionHandler { CALayer *layer = [[CALayer alloc] init]; - MDMMotionTiming timing = { - .duration = 0, - }; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; layer.opacity = 0.5; __block BOOL didInvokeCompletion = false; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"opacity" completion:^{ + [animator animateWithTraits:traits between:@[ @0, @1 ] + layer:layer keyPath:@"opacity" + completion:^(BOOL didComplete) { didInvokeCompletion = true; }]; @@ -64,13 +62,11 @@ - (void)testReversingSetsTheFirstValue { CALayer *layer = [[CALayer alloc] init]; - MDMMotionTiming timing = { - .duration = 0, - }; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:0]; layer.opacity = 0.5; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:@"cornerRadius"]; + [animator animateWithTraits:traits between:@[ @0, @1 ] layer:layer keyPath:@"cornerRadius"]; XCTAssertEqual(layer.cornerRadius, 0); } @@ -85,11 +81,11 @@ - (void)testCubicBezierAnimationFloatValue { // Setting to some bogus value because it will be ignored with the default animator settings. layer.cornerRadius = 0.5; - MDMMotionTiming timing = { - .delay = 0.5, - .duration = 1, - .curve = MDMMotionCurveMakeBezier(0.1, 0.2, 0.3, 0.4), - }; + CAMediaTimingFunction *timingFunction = + [CAMediaTimingFunction functionWithControlPoints:0.1 :0.2 :0.3 :0.4]; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDelay:0.5 + duration:1 + timingCurve:timingFunction]; __block BOOL didAddAnimation = false; [animator addCoreAnimationTracer:^(CALayer *layer, CAAnimation *animation) { @@ -98,7 +94,7 @@ - (void)testCubicBezierAnimationFloatValue { XCTAssertEqual(basicAnimation.keyPath, keyPath); - XCTAssertEqual(basicAnimation.duration, timing.duration); + XCTAssertEqual(basicAnimation.duration, traits.duration); XCTAssertGreaterThan(basicAnimation.beginTime, 0); XCTAssertTrue(basicAnimation.additive); @@ -109,15 +105,15 @@ - (void)testCubicBezierAnimationFloatValue { float point2[2]; [basicAnimation.timingFunction getControlPointAtIndex:1 values:point1]; [basicAnimation.timingFunction getControlPointAtIndex:2 values:point2]; - XCTAssertEqualWithAccuracy(timing.curve.data[0], point1[0], 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[1], point1[1], 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[2], point2[0], 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[3], point2[1], 0.00001); + XCTAssertEqualWithAccuracy(timingFunction.mdm_point1.x, point1[0], 0.00001); + XCTAssertEqualWithAccuracy(timingFunction.mdm_point1.y, point1[1], 0.00001); + XCTAssertEqualWithAccuracy(timingFunction.mdm_point2.x, point2[0], 0.00001); + XCTAssertEqualWithAccuracy(timingFunction.mdm_point2.y, point2[1], 0.00001); didAddAnimation = true; }]; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; + [animator animateWithTraits:traits between:@[ @0, @1 ] layer:layer keyPath:keyPath]; XCTAssertEqual(layer.cornerRadius, 1); XCTAssertTrue(didAddAnimation); @@ -133,11 +129,11 @@ - (void)testSpringAnimationFloatValue { // Setting to some bogus value because it will be ignored with the default animator settings. layer.cornerRadius = 0.5; - MDMMotionTiming timing = { - .delay = 0.5, - .duration = 1, - .curve = MDMMotionCurveMakeSpring(0.1, 0.2, 0.3), - }; + MDMSpringTimingCurve *springCurve = + [[MDMSpringTimingCurve alloc] initWithMass:0.1 tension:0.2 friction:0.3]; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDelay:0.5 + duration:1 + timingCurve:springCurve]; __block BOOL didAddAnimation = false; [animator addCoreAnimationTracer:^(CALayer *layer, CAAnimation *animation) { @@ -149,7 +145,7 @@ - (void)testSpringAnimationFloatValue { if ([springAnimation respondsToSelector:@selector(settlingDuration)]) { XCTAssertEqual(springAnimation.duration, springAnimation.settlingDuration); } else { - XCTAssertEqual(springAnimation.duration, timing.duration); + XCTAssertEqual(springAnimation.duration, traits.duration); } XCTAssertGreaterThan(springAnimation.beginTime, 0); @@ -157,14 +153,14 @@ - (void)testSpringAnimationFloatValue { XCTAssertEqual([springAnimation.fromValue doubleValue], -1); XCTAssertEqual([springAnimation.toValue doubleValue], 0); - XCTAssertEqualWithAccuracy(timing.curve.data[0], springAnimation.mass, 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[1], springAnimation.stiffness, 0.00001); - XCTAssertEqualWithAccuracy(timing.curve.data[2], springAnimation.damping, 0.00001); + XCTAssertEqualWithAccuracy(springCurve.mass, springAnimation.mass, 0.00001); + XCTAssertEqualWithAccuracy(springCurve.tension, springAnimation.stiffness, 0.00001); + XCTAssertEqualWithAccuracy(springCurve.friction, springAnimation.damping, 0.00001); didAddAnimation = true; }]; - [animator animateWithTiming:timing toLayer:layer withValues:@[ @0, @1 ] keyPath:keyPath]; + [animator animateWithTraits:traits between:@[ @0, @1 ] layer:layer keyPath:keyPath]; XCTAssertEqual(layer.cornerRadius, 1); XCTAssertTrue(didAddAnimation); diff --git a/tests/unit/MotionAnimatorTests.swift b/tests/unit/MotionAnimatorTests.swift index c6649d6..2ae5215 100644 --- a/tests/unit/MotionAnimatorTests.swift +++ b/tests/unit/MotionAnimatorTests.swift @@ -24,52 +24,10 @@ import MotionAnimator class MotionAnimatorTests: XCTestCase { - func testAnimatorAPIsCompile() { - let animator = MotionAnimator() - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) - let layer = CALayer() - - animator.animate(with: timing, to: layer, - withValues: [UIColor.blue, UIColor.red], keyPath: .backgroundColor) - animator.animate(with: timing, to: layer, - withValues: [CGRect.zero, CGRect(x: 0, y: 0, width: 100, height: 50)], - keyPath: .bounds) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .cornerRadius) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .height) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .opacity) - animator.animate(with: timing, to: layer, - withValues: [CGPoint.zero, CGPoint(x: 1, y: 1)], keyPath: .position) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .scale) - animator.animate(with: timing, to: layer, - withValues: [CGSize.zero, CGSize(width: 1, height: 1)], keyPath: .shadowOffset) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .shadowOpacity) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .shadowRadius) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .strokeStart) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .strokeEnd) - animator.animate(with: timing, to: layer, - withValues: [CGAffineTransform(rotationAngle: 12), - CGAffineTransform(rotationAngle: 50)], keyPath: .transform) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .width) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .x) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .y) - - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .init(rawValue: "bounds.size.width")) - - XCTAssertTrue(true) - } - func testAnimatorOnlyUsesSingleNonAdditiveAnimationForKeyPath() { let animator = MotionAnimator() animator.additive = false - - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -79,7 +37,8 @@ class MotionAnimatorTests: XCTestCase { XCTAssertEqual(view.layer.delegate as? UIView, view) UIView.animate(withDuration: 0.5) { - animator.animate(with: timing, to: view.layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, between: [0, 1], + layer: view.layer, keyPath: .rotation) XCTAssertEqual(view.layer.animationKeys()?.count, 1) } @@ -87,11 +46,7 @@ class MotionAnimatorTests: XCTestCase { func testCompletionCallbackIsExecutedWithZeroDuration() { let animator = MotionAnimator() - - let timing = MotionTiming(delay: 0, - duration: 0, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -101,7 +56,8 @@ class MotionAnimatorTests: XCTestCase { XCTAssertEqual(view.layer.delegate as? UIView, view) let didComplete = expectation(description: "Did complete") - animator.animate(with: timing, to: view.layer, withValues: [0, 1], keyPath: .rotation) { + animator.animate(with: traits, between: [0, 1], + layer: view.layer, keyPath: .rotation) { _ in didComplete.fulfill() } diff --git a/tests/unit/NonAdditiveAnimatorTests.swift b/tests/unit/NonAdditiveAnimatorTests.swift index 8fa5fdf..453122c 100644 --- a/tests/unit/NonAdditiveAnimatorTests.swift +++ b/tests/unit/NonAdditiveAnimatorTests.swift @@ -23,7 +23,7 @@ import MotionAnimator class NonAdditiveAnimationTests: XCTestCase { var animator: MotionAnimator! - var timing: MotionTiming! + var traits: MDMAnimationTraits! var view: UIView! override func setUp() { @@ -33,10 +33,7 @@ class NonAdditiveAnimationTests: XCTestCase { animator.additive = false - timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + traits = MDMAnimationTraits(duration: 1) let window = UIWindow() window.makeKeyAndVisible() @@ -49,14 +46,14 @@ class NonAdditiveAnimationTests: XCTestCase { override func tearDown() { animator = nil - timing = nil + traits = nil view = nil super.tearDown() } func testNumericKeyPathsDontAnimateAdditively() { - animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius) + animator.animate(with: traits, between: [1, 0], layer: view.layer, keyPath: .cornerRadius) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -75,9 +72,9 @@ class NonAdditiveAnimationTests: XCTestCase { } func testSizeKeyPathsDontAnimateAdditively() { - animator.animate(with: timing, to: view.layer, - withValues: [CGSize(width: 0, height: 0), - CGSize(width: 1, height: 2)], keyPath: .shadowOffset) + animator.animate(with: traits, between: [CGSize(width: 0, height: 0), + CGSize(width: 1, height: 2)], + layer: view.layer, keyPath: .shadowOffset) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -96,9 +93,8 @@ class NonAdditiveAnimationTests: XCTestCase { } func testPositionKeyPathsDontAnimateAdditively() { - animator.animate(with: timing, to: view.layer, - withValues: [CGPoint(x: 0, y: 0), - CGPoint(x: 1, y: 2)], keyPath: .position) + animator.animate(with: traits, between: [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 2)], + layer: view.layer, keyPath: .position) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") @@ -117,9 +113,9 @@ class NonAdditiveAnimationTests: XCTestCase { } func testRectKeyPathsDontAnimateAdditively() { - animator.animate(with: timing, to: view.layer, - withValues: [CGRect(x: 0, y: 0, width: 0, height: 0), - CGRect(x: 0, y: 0, width: 100, height: 50)], keyPath: .bounds) + animator.animate(with: traits, between: [CGRect(x: 0, y: 0, width: 0, height: 0), + CGRect(x: 0, y: 0, width: 100, height: 50)], + layer: view.layer, keyPath: .bounds) XCTAssertNotNil(view.layer.animationKeys(), "Expected an animation to be added, but none were found.") diff --git a/tests/unit/TimeScaleFactorTests.swift b/tests/unit/TimeScaleFactorTests.swift index 9e89ad4..ba23c04 100644 --- a/tests/unit/TimeScaleFactorTests.swift +++ b/tests/unit/TimeScaleFactorTests.swift @@ -23,10 +23,7 @@ import MotionAnimator class TimeScaleFactorTests: XCTestCase { - let timing = MotionTiming(delay: 0, - duration: 1, - curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), - repetition: .init(type: .none, amount: 0, autoreverses: false)) + let traits = MDMAnimationTraits(duration: 1) var layer: CALayer! var addedAnimations: [CAAnimation]! var animator: MotionAnimator! @@ -52,7 +49,7 @@ class TimeScaleFactorTests: XCTestCase { } func testDefaultTimeScaleFactorDoesNotModifyDuration() { - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, between: [0, 1], layer: layer, keyPath: .rotation) XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! @@ -62,23 +59,23 @@ class TimeScaleFactorTests: XCTestCase { func testExplicitTimeScaleFactorChangesDuration() { animator.timeScaleFactor = 0.5 - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, between: [0, 1], layer: layer, keyPath: .rotation) XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, timing.duration * 0.5) + XCTAssertEqual(animation.duration, traits.duration * 0.5) } func testTransactionTimeScaleFactorChangesDuration() { CATransaction.begin() CATransaction.mdm_setTimeScaleFactor(0.5) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, between: [0, 1], layer: layer, keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, timing.duration * 0.5) + XCTAssertEqual(animation.duration, traits.duration * 0.5) } func testTransactionTimeScaleFactorOverridesAnimatorTimeScaleFactor() { @@ -87,13 +84,13 @@ class TimeScaleFactorTests: XCTestCase { CATransaction.begin() CATransaction.mdm_setTimeScaleFactor(0.5) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, between: [0, 1], layer: layer, keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, timing.duration * 0.5) + XCTAssertEqual(animation.duration, traits.duration * 0.5) } func testNilTransactionTimeScaleFactorUsesAnimatorTimeScaleFactor() { @@ -103,12 +100,12 @@ class TimeScaleFactorTests: XCTestCase { CATransaction.mdm_setTimeScaleFactor(0.5) CATransaction.mdm_setTimeScaleFactor(nil) - animator.animate(with: timing, to: layer, withValues: [0, 1], keyPath: .rotation) + animator.animate(with: traits, between: [0, 1], layer: layer, keyPath: .rotation) CATransaction.commit() XCTAssertEqual(addedAnimations.count, 1) let animation = addedAnimations.last! - XCTAssertEqual(animation.duration, timing.duration * 2) + XCTAssertEqual(animation.duration, traits.duration * 2) } } From 7e506cc37b7d64d010b69d4996621755ece26595 Mon Sep 17 00:00:00 2001 From: featherless Date: Wed, 13 Dec 2017 18:11:21 -0500 Subject: [PATCH 07/11] Fix pre-iOS 11 unit test failure. (#89) --- tests/unit/QuartzCoreBehavioralTests.swift | 11 +++----- tests/unit/UIKitBehavioralTests.swift | 31 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/tests/unit/QuartzCoreBehavioralTests.swift b/tests/unit/QuartzCoreBehavioralTests.swift index 032aff5..6599114 100644 --- a/tests/unit/QuartzCoreBehavioralTests.swift +++ b/tests/unit/QuartzCoreBehavioralTests.swift @@ -24,16 +24,13 @@ import MotionAnimator class QuartzCoreBehavioralTests: XCTestCase { var layer: CAShapeLayer! + var window: UIWindow! var originalImplementation: IMP? override func setUp() { super.setUp() - let window = UIWindow() + window = UIWindow() window.makeKeyAndVisible() - layer = CAShapeLayer() - window.layer.addSublayer(layer) - - rebuildLayer() } override func tearDown() { @@ -43,10 +40,8 @@ class QuartzCoreBehavioralTests: XCTestCase { } private func rebuildLayer() { - let oldSuperlayer = layer.superlayer! - layer.removeFromSuperlayer() layer = CAShapeLayer() - oldSuperlayer.addSublayer(layer) + window.layer.addSublayer(layer) // Connect our layers to the render server. CATransaction.flush() diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index c651de0..07211fc 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -70,9 +70,8 @@ class UIKitBehavioralTests: XCTestCase { // MARK: Each animatable property needs to be added to exactly one of the following three tests func testSomePropertiesImplicitlyAnimateAdditively() { - let properties: [AnimatableKeyPath: Any] = [ + let baselineProperties: [AnimatableKeyPath: Any] = [ .bounds: CGRect(x: 0, y: 0, width: 123, height: 456), - .cornerRadius: 3, .height: 100, .position: CGPoint(x: 50, y: 20), .rotation: 42, @@ -82,6 +81,15 @@ class UIKitBehavioralTests: XCTestCase { .x: 12, .y: 23, ] + let properties: [AnimatableKeyPath: Any] + if #available(iOS 11.0, *) { + // Corner radius became implicitly animatable in iOS 11. + var baselineWithCornerRadiusProperties = baselineProperties + baselineWithCornerRadiusProperties[.cornerRadius] = 3 + properties = baselineWithCornerRadiusProperties + } else { + properties = baselineProperties + } for (keyPath, value) in properties { rebuildView() @@ -103,11 +111,11 @@ class UIKitBehavioralTests: XCTestCase { } func testSomePropertiesImplicitlyAnimateButNotAdditively() { - let properties: [AnimatableKeyPath: Any] = [ + let baselineProperties: [AnimatableKeyPath: Any] = [ .backgroundColor: UIColor.blue, .opacity: 0.5, ] - for (keyPath, value) in properties { + for (keyPath, value) in baselineProperties { rebuildView() UIView.animate(withDuration: 0.01) { @@ -128,13 +136,26 @@ class UIKitBehavioralTests: XCTestCase { } func testSomePropertiesDoNotImplicitlyAnimate() { - let properties: [AnimatableKeyPath: Any] = [ + let baselineProperties: [AnimatableKeyPath: Any] = [ + .cornerRadius: 3, .shadowOffset: CGSize(width: 1, height: 1), .shadowOpacity: 0.3, .shadowRadius: 5, .strokeStart: 0.2, .strokeEnd: 0.5, ] + + let properties: [AnimatableKeyPath: Any] + if #available(iOS 11.0, *) { + // Corner radius became implicitly animatable in iOS 11. + var baselineWithOutCornerRadius = baselineProperties + baselineWithOutCornerRadius.removeValue(forKey: .cornerRadius) + properties = baselineWithOutCornerRadius + + } else { + properties = baselineProperties + } + for (keyPath, value) in properties { rebuildView() From 8f474dd545ec1b3e98db5ef783bca502351911e4 Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 09:39:37 -0500 Subject: [PATCH 08/11] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d0d328e..49a1955 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,6 @@ before_install: - pod install --repo-update script: - set -o pipefail - - xcodebuild test -workspace MotionAnimator.xcworkspace -scheme MotionAnimatorCatalog -sdk "iphonesimulator10.3" -destination "name=iPhone 6s,OS=10.1" -enableCodeCoverage YES ONLY_ACTIVE_ARCH=YES | xcpretty -c; + - xcodebuild test -workspace MotionAnimator.xcworkspace -scheme MotionAnimatorCatalog -sdk "iphonesimulator10.3" -destination "name=iPhone 6s,OS=10.3.1" -enableCodeCoverage YES ONLY_ACTIVE_ARCH=YES | xcpretty -c; after_success: - bash <(curl -s https://codecov.io/bash) From 797fbb1a3522b251d41bd67eeffa5b780c657e67 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 14 Dec 2017 09:48:20 -0500 Subject: [PATCH 09/11] Automatic changelog preparation for release. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 362062a..d452915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# #develop# + + TODO: Enumerate changes. + + # 2.6.0 This minor release increases test coverage, fixes a variety of bugs related to `beginFromCurrentState`, and generally improves the stability and robustness of the underlying implementation. From 99fb8d52a1a3e4699eccd959fe3a11e68069eb9f Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 14 Dec 2017 09:59:45 -0500 Subject: [PATCH 10/11] Update the changelog. --- CHANGELOG.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d452915..f7135a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,44 @@ # #develop# - TODO: Enumerate changes. +This minor release introduces support for the new [v1.5.0](https://github.com/material-motion/motion-interchange-objc/releases/tag/v1.5.0) MotionInterchange format. +## New features + +It is now possible to additively and implicitly animate the `transform` property of both UIView and CALayer. + +## Source changes + +* [Fix pre-iOS 11 unit test failure. (#89)](https://github.com/material-motion/motion-animator-objc/commit/7e506cc37b7d64d010b69d4996621755ece26595) (featherless) +* [Migrate to the Objective-C interchange format (#88)](https://github.com/material-motion/motion-animator-objc/commit/573b19269e155f15e05e9b146a1c324b937cfb1c) (featherless) +* [Revert "Update with ObjC implementation."](https://github.com/material-motion/motion-animator-objc/commit/f55625d9f63e857e878eff4e7687ddd40bad0fea) (Jeff Verkoeyen) +* [Update with ObjC implementation.](https://github.com/material-motion/motion-animator-objc/commit/be7f9081c0678484e034cc976aafcdab748b58bf) (Jeff Verkoeyen) +* [Add support for additively animating transform. (#85)](https://github.com/material-motion/motion-animator-objc/commit/e54ce3a118c1e877c5ca78a7d2fed9625d0ffc67) (featherless) + +## API changes + +Auto-generated by running: + + apidiff origin/stable release-candidate objc src/MotionAnimator.h + +#### MDMMotionAnimator + +*new* method: `-animateWithTraits:animations:completion:` in `MDMMotionAnimator` + +*new* method: `-animateWithTraits:between:layer:keyPath:` in `MDMMotionAnimator` + +*new* method: `-animateWithTraits:animations:` in `MDMMotionAnimator` + +*new* method: `-animateWithTraits:between:layer:keyPath:completion:` in `MDMMotionAnimator` + +#### MDMKeyPathTransform + +*new* constant: `MDMKeyPathTransform` + +## Non-source changes + +* [Update .travis.yml](https://github.com/material-motion/motion-animator-objc/commit/8f474dd545ec1b3e98db5ef783bca502351911e4) (featherless) +* [Enable coverage on travis](https://github.com/material-motion/motion-animator-objc/commit/45d43aa23a88c963927f4f01669e1b0ae26fb9e5) (featherless) +* [Update kokoro bazel runner for v4.](https://github.com/material-motion/motion-animator-objc/commit/67d903ed71fbc909ea06bb8097313a4218c8f566) (Jeff Verkoeyen) # 2.6.0 From 432585f2a29cefcae30471a86111b7a139d48d3b Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 14 Dec 2017 10:01:18 -0500 Subject: [PATCH 11/11] Bump the release. --- .jazzy.yaml | 4 ++-- CHANGELOG.md | 2 +- MotionAnimator.podspec | 2 +- Podfile.lock | 11 ++++------- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 26fd2e9..3f0fa00 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -1,7 +1,7 @@ module: MotionAnimator -module_version: 2.6.0 +module_version: 2.7.0 sdk: iphonesimulator umbrella_header: src/MotionAnimator.h objc: true github_url: https://github.com/material-motion/motion-animator-objc -github_file_prefix: https://github.com/material-motion/motion-animator-objc/tree/v2.6.0 +github_file_prefix: https://github.com/material-motion/motion-animator-objc/tree/v2.7.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index f7135a6..510f79d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# #develop# +# 2.7.0 This minor release introduces support for the new [v1.5.0](https://github.com/material-motion/motion-interchange-objc/releases/tag/v1.5.0) MotionInterchange format. diff --git a/MotionAnimator.podspec b/MotionAnimator.podspec index cc7ca7b..23fda17 100644 --- a/MotionAnimator.podspec +++ b/MotionAnimator.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MotionAnimator" s.summary = "A Motion Animator creates performant, interruptible animations from motion specs." - s.version = "2.6.0" + s.version = "2.7.0" s.authors = "The Material Motion Authors" s.license = "Apache 2.0" s.homepage = "https://github.com/material-motion/motion-animator-objc" diff --git a/Podfile.lock b/Podfile.lock index f8eaead..0b4e1d6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,25 +1,22 @@ PODS: - CatalogByConvention (2.2.0) - - MotionAnimator (2.6.0): - - MotionInterchange (~> 1.3) + - MotionAnimator (2.7.0): + - MotionInterchange (~> 1.6) - MotionInterchange (1.6.0) DEPENDENCIES: - CatalogByConvention - MotionAnimator (from `./`) - - MotionInterchange (from `../motion-interchange-objc/`) EXTERNAL SOURCES: MotionAnimator: :path: ./ - MotionInterchange: - :path: ../motion-interchange-objc/ SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f - MotionAnimator: a4b0ba87a674bb3e89e25f0530b7e80a204ac1c1 + MotionAnimator: fe012f4b344f091f95a621b0d0a97c4e2ea1c525 MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d -PODFILE CHECKSUM: f354f45cd3f9eb0e6ac9a2bfd9429945eae8c0ad +PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3 COCOAPODS: 1.3.1