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/.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!" diff --git a/.travis.yml b/.travis.yml index 546c252..49a1955 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.3.1" -enableCodeCoverage YES ONLY_ACTIVE_ARCH=YES | xcpretty -c; after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md index 362062a..510f79d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +# 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. + +## 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 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. diff --git a/MotionAnimator.podspec b/MotionAnimator.podspec index f58dccb..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" @@ -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..0b4e1d6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,8 +1,8 @@ PODS: - CatalogByConvention (2.2.0) - - MotionAnimator (2.6.0): - - MotionInterchange (~> 1.3) - - MotionInterchange (1.3.0) + - MotionAnimator (2.7.0): + - MotionInterchange (~> 1.6) + - MotionInterchange (1.6.0) DEPENDENCIES: - CatalogByConvention @@ -14,8 +14,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f - MotionAnimator: a4b0ba87a674bb3e89e25f0530b7e80a204ac1c1 - MotionInterchange: 988fc0011e4b806cc33f2fb4f9566f5eeb4159e8 + MotionAnimator: fe012f4b344f091f95a621b0d0a97c4e2ea1c525 + MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3 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 new file mode 100644 index 0000000..0974145 --- /dev/null +++ b/examples/TapToBounceExample.swift @@ -0,0 +1,63 @@ +/* + 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 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: traits) { + sender.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) + } + } + + func didUnfocus(_ sender: UIButton) { + let animator = MotionAnimator() + animator.animate(with: traits) { + 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/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 ea3a4fd..8557b21 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; @@ -56,55 +65,63 @@ 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) { 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) { - 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. } @@ -146,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: @@ -208,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; @@ -224,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; } @@ -241,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; @@ -257,21 +254,24 @@ 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; } } - } - // 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 + } 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]; + } + } + 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/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.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/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/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 f53b36c..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() @@ -59,6 +56,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, @@ -74,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 " @@ -98,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) } @@ -122,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 " @@ -146,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 6a6322e..2ae5215 100644 --- a/tests/unit/MotionAnimatorTests.swift +++ b/tests/unit/MotionAnimatorTests.swift @@ -24,49 +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: [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() @@ -76,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) } @@ -84,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() @@ -98,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/QuartzCoreBehavioralTests.swift b/tests/unit/QuartzCoreBehavioralTests.swift index db09318..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() @@ -86,6 +81,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/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) } } diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index 2bba001..07211fc 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -70,17 +70,26 @@ 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, .scale: 2.5, + .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), .width: 25, .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() @@ -102,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) { @@ -127,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() @@ -163,6 +185,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,