From 0632c668e26347208d60c464e683774cd9dab5b7 Mon Sep 17 00:00:00 2001 From: featherless Date: Tue, 19 Dec 2017 14:22:42 -0500 Subject: [PATCH 01/14] Add a feature table to the readme. (#104) --- README.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f3224d..9d6a6e7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,45 @@ [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/MotionAnimator.svg)](https://cocoapods.org/pods/MotionAnimator) [![Platform](https://img.shields.io/cocoapods/p/MotionAnimator.svg)](http://cocoadocs.org/docsets/MotionAnimator) - + + + + + + + +
🎉Implicit and explicit additive animations.
🎉Parameterized motion with the Interchange.
🎉Provide velocity to animations directly from gesture recognizers.
🎉Maximize frame rates by relying more on Core Animation.
🎉Animatable properties are Swift enum types.
🎉Consistent model layer value expectations.
+ +The following properties can be implicitly animated using the MotionAnimator on iOS 8 and up: + + + + + + + + + + + + + + + + + + + + + + + + +
CALayer anchorPoint
CALayer backgroundColorUIView backgroundColor
CALayer boundsUIView bounds
CALayer borderWidth
CALayer borderColor
CALayer cornerRadius
CALayer heightUIView height
CALayer opacityUIView alpha
CALayer positionUIView center
CALayer rotationUIView rotation
CALayer scaleUIView scale
CALayer shadowColor
CALayer shadowOffset
CALayer shadowOpacity
CALayer shadowRadius
CALayer transformUIView transform
CALayer widthUIView width
CALayer xUIView x
CALayer yUIView y
CALayer z
CAShapeLayer strokeStart
CAShapeLayer strokeEnd
+ +Note: any animatable property can also be animated with MotionAnimator's explicit animation APIs, even if it's not listed in the table above. + +> Is a property missing from this list? [We welcome pull requests](https://github.com/material-motion/motion-animator-objc/edit/develop/src/MDMAnimatableKeyPaths.h)! ## Example apps/unit tests From d53f753a976b1d067d226ed9bfd1893875d4ab8d Mon Sep 17 00:00:00 2001 From: featherless Date: Tue, 19 Dec 2017 15:43:24 -0500 Subject: [PATCH 02/14] Add drop in replacement APIs section to the readme (#105) --- README.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/README.md b/README.md index 9d6a6e7..eeab1c0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,119 @@ Note: any animatable property can also be animated with MotionAnimator's explici > Is a property missing from this list? [We welcome pull requests](https://github.com/material-motion/motion-animator-objc/edit/develop/src/MDMAnimatableKeyPaths.h)! +## MotionAnimator: a drop-in replacement + +UIView's implicit animation APIs are also available on the MotionAnimator: + +```swift +// Animating implicitly with UIView APIs +UIView.animate(withDuration: 1.0, animations: { + view.alpha = 0.5 +}) + +// Equivalent MotionAnimator API +MotionAnimator.animate(withDuration: 1.0, animations: { + view.alpha = 0.5 +}) +``` + +But the MotionAnimator allows you to animate more properties — and on more iOS versions: + +```swift +UIView.animate(withDuration: 1.0, animations: { + view.layer.cornerRadius = 10 // Only works on iOS 11 and up +}) + +MotionAnimator.animate(withDuration: 1.0, animations: { + view.layer.cornerRadius = 10 // Works on iOS 8 and up +}) +``` + +MotionAnimator makes use of the [MotionInterchange](https://github.com/material-motion/motion-interchange-objc), a standardized format for representing animation traits. This makes it possible to tweak the traits of an animation without rewriting the code that ultimately creates the animation, useful for building tweaking tools and making motion "stylesheets". + +```swift +// Want to change a trait of your animation? You'll need to use a different function altogether +// to do so: +UIView.animate(withDuration: 1.0, animations: { + view.alpha = 0.5 +}) +UIView.animate(withDuration: 1.0, delay: 0.5, options: [], animations: { + view.alpha = 0.5 +}, completion: nil) + +// But with the MotionInterchange, you can create and manipulate the traits of an animation +// separately from its execution. +let traits = MDMAnimationTraits(duration: 1.0) +traits.delay = 0.5 + +let animator = MotionAnimator() +animator.animate(with: traits, animations: { + view.alpha = 0.5 +}) +``` + +The MotionAnimator can also be used to replace explicit Core Animation code with additive explicit animations: + +```swift +let from = 0 +let to = 10 +// Animating expicitly with Core Animation APIs +let animation = CABasicAnimation(keyPath: "cornerRadius") +animation.fromValue = (from - to) +animation.toValue = 0 +animation.isAdditive = true +animation.duration = 1.0 +view.layer.add(animation, forKey: animation.keyPath) +view.layer.cornerRadius = to + +// Equivalent implicit MotionAnimator API. cornerRadius will be animated additively by default. +view.layer.cornerRadius = 0 +MotionAnimator.animate(withDuration: 1, animations: { + view.layer.cornerRadius = 10 +}) + +// Equivalent explicit MotionAnimator API +// Note that this API will also set the final animation value to the layer's model layer, similar +// to how implicit animations work, and unlike the explicit pure Core Animation implementation +// above. +let animator = MotionAnimator() +animator.animate(with: MDMAnimationTraits(duration: 1.0), + between: [0, 10], + layer: view.layer, + keyPath: .cornerRadius) +``` + +Springs on iOS require an initial velocity that's normalized by the displacement of the animation. MotionAnimator calculates this for you so that you can directly provide gesture recognizer velocity values: + +```swift +// Common variables +let gestureYVelocity = gestureRecognizer.velocity(in: someContainerView).y +let destinationY = 75 + +// Animating springs implicitly with UIView APIs +let displacement = destinationY - view.position.y +UIView.animate(withDuration: 1.0, + delay: 0, + usingSpringWithDamping: 1.0, + initialSpringVelocity: gestureYVelocity / displacement, + options: [], + animations: { + view.layer.position = CGPoint(x: view.position.x, y: destinationY) + }, + completion: nil) + +// Equivalent MotionAnimator API +let animator = MotionAnimator() +let traits = MDMAnimationTraits(duration: 1.0) +traits.timingCurve = MDMSpringTimingCurveGenerator(duration: traits.duration, + dampingRatio: 1.0, + initialVelocity: gestureYVelocity) +animator.animate(with: traits, + between: [view.layer.position.y, destinationY], + layer: view.layer, + keyPath: .y) +``` + ## Example apps/unit tests Check out a local copy of the repo to access the Catalog application by running the following From 823e0ffa286a6918aaf67b3a6070dab16cfa775d Mon Sep 17 00:00:00 2001 From: featherless Date: Tue, 19 Dec 2017 16:07:45 -0500 Subject: [PATCH 03/14] Add API snippets section. (#106) --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/README.md b/README.md index eeab1c0..6e501a8 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,103 @@ animator.animate(with: traits, keyPath: .y) ``` +## API snippets + +### Implicit animations + +```swift +MotionAnimator.animate(withDuration: <#T##TimeInterval#>) { + <#code#> +} +``` + +```swift +MotionAnimator.animate(withDuration: <#T##TimeInterval#>, + delay: <#T##TimeInterval#>, + options: <#T##UIViewAnimationOptions#>, + animations: { + <#code#> +}) +``` + +### Explicit animations + +```swift +let traits = MDMAnimationTraits(delay: <#T##TimeInterval#>, + duration: <#T##TimeInterval#>, + animationCurve: <#T##UIViewAnimationCurve#>) +let animator = MotionAnimator() +animator.animate(with: <#T##MDMAnimationTraits#>, + between: [<#T##[From (Any)]#>, <#T##[To (Any)]#>], + layer: <#T##CALayer#>, + keyPath: <#T##AnimatableKeyPath#>) +``` + +### Animating transitions + +```swift +let animator = MotionAnimator() +animator.shouldReverseValues = transition.direction == .backwards + +let traits = MDMAnimationTraits(delay: <#T##TimeInterval#>, + duration: <#T##TimeInterval#>, + animationCurve: <#T##UIViewAnimationCurve#>) +animator.animate(with: <#T##MDMAnimationTraits#>, + between: [<#T##[From (Any)]#>, <#T##[To (Any)]#>], + layer: <#T##CALayer#>, + keyPath: <#T##AnimatableKeyPath#>) +``` + +### Creating motion specifications + +```swift +class MotionSpec { + static let chipWidth = MDMAnimationTraits(delay: 0.000, duration: 0.350) + static let chipHeight = MDMAnimationTraits(delay: 0.000, duration: 0.500) +} + +let animator = MotionAnimator() +animator.shouldReverseValues = transition.direction == .backwards + +animator.animate(with: MotionSpec.chipWidth, + between: [<#T##[From (Any)]#>, <#T##[To (Any)]#>], + layer: <#T##CALayer#>, + keyPath: <#T##AnimatableKeyPath#>) +animator.animate(with: MotionSpec.chipHeight, + between: [<#T##[From (Any)]#>, <#T##[To (Any)]#>], + layer: <#T##CALayer#>, + keyPath: <#T##AnimatableKeyPath#>) +``` + +### Animating from the current state + +```swift +// Will animate any non-additive animations from their current presentation layer value +animator.beginFromCurrentState = true +``` + +### Debugging animations + +```swift +animator.addCoreAnimationTracer { layer, animation in + print(animation.debugDescription) +} +``` + +### Stopping animations in reaction to a gesture recognizer + +```swift +if gesture.state == .began { + animator.stopAllAnimations() +} +``` + +### Removing all animations + +```swift +animator.removeAllAnimations() +``` + ## Example apps/unit tests Check out a local copy of the repo to access the Catalog application by running the following From ccd350d1fbdb8a95b6922bf99680d10183a91f90 Mon Sep 17 00:00:00 2001 From: featherless Date: Tue, 19 Dec 2017 16:17:17 -0500 Subject: [PATCH 04/14] Add readme section on main thread animations vs Core Animation. (#107) --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 6e501a8..1a7dec1 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,26 @@ if gesture.state == .began { animator.removeAllAnimations() ``` +## Main thread animations vs Core Animation + +Animation systems on iOS can be split into two general categories: main thread-based and Core Animation. + +**Main thread**-based animation systems include UIDynamics, Facebook's [POP](https://github.com/facebook/pop), or anything driven by a CADisplayLink. These animation systems share CPU time with your app's main thread, meaning they're sharing resources with UIKit, text rendering, and any other main-thread bound processes. This also means the animations are subject to *main thread jank*, in other words: dropped frames of animation or "stuttering". + +**Core Animation** makes use of the *render server*, an operating system-wide process for animations on iOS. This independence from an app's process allows the render server to avoid main thread jank altogether. + +The primary benefit of main thread animations over Core Animation is that Core Animation's list of animatable properties is small and unchangeable, while main thread animations can animate anything in your application. A good example of this is using POP to animate a "time" property, and to map that time to the hands of a clock. This type of behavior cannot be implemented in Core Animation without moving code out of the render server and in to the main thread. + +The primary benefit of Core Animation over main thread animations, on the other hand, is that your animations will be much less likely to drop frames simply because your app is busy on its main thread. + +When evaluating whether to use a main thread-based animation system or not, check first whether the same animations can be performed in Core Animation instead. If they can, you may be able to offload the animations from your app's main thread by using Core Animation, saving you valuable processing time for other main thread-bound operations. + +MotionAnimator is a purely Core Animation-based animator. If you are looking for main thread solutions then check out the following technologies: + +- [UIDynamics](https://developer.apple.com/documentation/uikit/animation_and_haptics/uikit_dynamics) +- [POP](https://github.com/facebook/pop) +- [CADisplayLink](https://developer.apple.com/documentation/quartzcore/cadisplaylink) + ## Example apps/unit tests Check out a local copy of the repo to access the Catalog application by running the following From 05ad80b7074eadb3131543292b11f18837d91e6b Mon Sep 17 00:00:00 2001 From: featherless Date: Fri, 22 Dec 2017 11:03:12 -0500 Subject: [PATCH 05/14] Add core animation quiz to the readme. (#108) --- README.md | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/README.md b/README.md index 1a7dec1..3ab81e2 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,147 @@ MotionAnimator is a purely Core Animation-based animator. If you are looking for - [POP](https://github.com/facebook/pop) - [CADisplayLink](https://developer.apple.com/documentation/quartzcore/cadisplaylink) +# Core Animation: a deep dive + +> Recommended reading: +> +> - [Building Animation Driven Interfaces](http://asciiwwdc.com/2010/sessions/123) +> - [Core Animation in Practice, Part 1](http://asciiwwdc.com/2010/sessions/424) +> - [Core Animation in Practice, Part 2](http://asciiwwdc.com/2010/sessions/425) +> - [Building Interruptible and Responsive Interactions](http://asciiwwdc.com/2014/sessions/236) +> - [Advanced Graphics and Animations for iOS Apps](http://asciiwwdc.com/2014/sessions/419) +> - [Advances in UIKit Animations and Transitions](http://asciiwwdc.com/2016/sessions/216) +> - [Animating Layer Content](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/CreatingBasicAnimations/CreatingBasicAnimations.html) +> - [Advanced Animation Tricks](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html) +> - [Additive animations: animateWithDuration in iOS 8](http://iosoteric.com/additive-animations-animatewithduration-in-ios-8/) + +There are two primary ways to animate with Core Animation on iOS: + +1. **implicitly**, with the UIView `animateWithDuration:` APIs, or by setting properties on standalone CALayer instances (those that are **not** backing a UIView), and +2. **explicitly**, with the CALayer `addAnimation:forKey:` APIs. + +A subset of UIView's and CALayer's public APIs is animatable by Core Animation. Of these animatable properties, some are implicitly animatable while some are not. Whether a property is animatable or not depends on the context within which it's being animated, and whether an animation is additive or not depends on which animation API is being used. With this matrix of conditions it's understandable that it can sometimes be difficult to know how to effectively make use of Core Animation. + +The following quiz helps illustrate that the UIKit and Core Animation APIs can often lead to unintuitive behavior. Try to guess which of the following snippets will generate an animation and, if they do, what the generated animation's duration will be: + +> Imagine that each code snippet is a standalone unit test (because [they are](tests/unit/HeadlessLayerImplicitAnimationTests.swift)!). + +```swift +let view = UIView() +UIView.animate(withDuration: 0.8, animations: { + view.alpha = 0.5 +}) +``` + +
+ Click to see the answer + Generates an animation with duration of 0.8. +
+ +--- + +```swift +let view = UIView() +UIView.animate(withDuration: 0.8, animations: { + view.layer.opacity = 0.5 +}) +``` + +
+ Click to see the answer + Generates an animation with duration of 0.8. +
+ +--- + +```swift +let view = UIView() +UIView.animate(withDuration: 0.8, animations: { + view.layer.cornerRadius = 3 +}) +``` + +
+ Click to see the answer + On iOS 11 and up, generates an animation with duration of 0.8. Older operating systems will not generate an animation. +
+ +--- + +```swift +let view = UIView() +view.alpha = 0.5 +``` + +
+ Click to see the answer + Does not generate an animation. +
+ +--- + +```swift +let view = UIView() +view.layer.opacity = 0.5 +``` + +
+ Click to see the answer + Does not generate an animation. +
+ +--- + +```swift +let layer = CALayer() +layer.opacity = 0.5 +``` + +
+ Click to see the answer + Does not generate an animation. +
+ +--- + +```swift +let view = UIView() +window.addSubview(view) +let layer = CALayer() +view.layer.addSublayer(layer) + +// Pump the run loop once. +RunLoop.main.run(mode: .defaultRunLoopMode, before: .distantFuture) + +layer.opacity = 0.5 +``` + +
+ Click to see the answer + Generates an animation with duration of 0.25. +
+ +--- + +```swift +let view = UIView() +window.addSubview(view) +let layer = CALayer() +view.layer.addSublayer(layer) + +// Pump the run loop once. +RunLoop.main.run(mode: .defaultRunLoopMode, before: .distantFuture) + +UIView.animate(withDuration: 0.8, animations: { + layer.opacity = 0.5 +}) +``` + +
+ Click to see the answer + Generates an animation with duration of 0.25. This isn't a typo: standalone layers read from the current CATransaction rather than UIView's parameters when implicitly animating, even when the change happens within a UIView animation block. +
+ ## Example apps/unit tests Check out a local copy of the repo to access the Catalog application by running the following From fd9710d220bb48a64ac008a399be74d04f6ab8b7 Mon Sep 17 00:00:00 2001 From: Robert Moore Date: Thu, 25 Jan 2018 16:50:20 -0500 Subject: [PATCH 06/14] Fix crash in Legacy API for nil completion blocks (#110) When passed a `nil` completion block value, the "legacy" API would crash by calling the block without performing a nil check. Adding 2 tests to verify that the animation works as expected with a `nil` completion block. --- src/MDMMotionAnimator.m | 10 +++++--- tests/unit/MotionAnimatorTests.m | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/MDMMotionAnimator.m b/src/MDMMotionAnimator.m index 7c139c8..03d1dbb 100644 --- a/src/MDMMotionAnimator.m +++ b/src/MDMMotionAnimator.m @@ -261,7 +261,9 @@ - (void)animateWithTiming:(MDMMotionTiming)timing completion:(void (^)(void))completion { MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing]; [self animateWithTraits:traits animations:animations completion:^(BOOL didComplete) { - completion(); + if (completion) { + completion(); + } }]; } @@ -284,8 +286,10 @@ - (void)animateWithTiming:(MDMMotionTiming)timing layer:layer keyPath:keyPath completion:^(BOOL didComplete) { - completion(); - }]; + if (completion) { + completion(); + } + }]; } #pragma mark - Private diff --git a/tests/unit/MotionAnimatorTests.m b/tests/unit/MotionAnimatorTests.m index 509eeef..15d7fb9 100644 --- a/tests/unit/MotionAnimatorTests.m +++ b/tests/unit/MotionAnimatorTests.m @@ -166,4 +166,47 @@ - (void)testSpringAnimationFloatValue { XCTAssertTrue(didAddAnimation); } +#pragma mark - Legacy API + +- (void)testAnimationWithTimingNilCompletion { + // Given + MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; + + CALayer *layer = [[CALayer alloc] init]; + MDMMotionTiming timing = (MDMMotionTiming) { + .duration = 0.250, .curve = MDMMotionCurveMakeBezier(0.42f, 0.00f, 0.58f, 1.00f) + }; + + // When + [animator animateWithTiming:timing + animations:^{ + layer.opacity = 0.7; + } + completion:nil]; + + // Then + XCTAssertEqualWithAccuracy(layer.opacity, 0.7, 0.0001); +} + +- (void)testAnimationWithTimingToLayerWithValuesKeyPathNilCompletion { + // Given + MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; + + CALayer *layer = [[CALayer alloc] init]; + CALayer *anotherLayer = [[CALayer alloc] init]; + MDMMotionTiming timing = (MDMMotionTiming) { + .duration = 0.250, .curve = MDMMotionCurveMakeBezier(0.42f, 0.00f, 0.58f, 1.00f) + }; + + // When + [animator animateWithTiming:timing + toLayer:anotherLayer + withValues:@[@(0), @(1)] + keyPath:@"opacity" + completion:nil]; + + // Then + XCTAssertEqualWithAccuracy(layer.opacity, 1, 0.0001); +} + @end From 53255ab590908cc18841e3eed4af440e52132376 Mon Sep 17 00:00:00 2001 From: Robert Moore Date: Thu, 25 Jan 2018 18:36:28 -0500 Subject: [PATCH 07/14] Return `nil` CAAction when swapping implementation (#109) When swapping the implementation of `actionForKey:`, returning `NSNull` would result in a crash because it could not respond to other messages being passed (like `runAction:forKey:`). Instead, returning `nil` will safely receive additional messages. This was easily reproducible by running the unit test suite on iOS 8.1 or 8.3 simulators. Returning `nil` matches the documented behavior of `actionForKey:` in the CALayer headers: > Returns the action object associated with the event named by the > string 'event'. The default implementation searches for an action > object in the following places: > > 1. if defined, call the delegate method -actionForLayer:forKey: > 2. look in the layer's `actions' dictionary > 3. look in any `actions' dictionaries in the `style' hierarchy > 4. call +defaultActionForKey: on the layer's class > > If any of these steps results in a non-nil action object, the > following steps are ignored. If the final result is an instance of > NSNull, it is converted to `nil'. > > - (nullable id)actionForKey:(NSString *)event; Also fixed a crash with CASpringAnimation on iOS 8.x because the desired selectors aren't available. --- src/private/CABasicAnimation+MotionAnimator.m | 3 +- src/private/MDMBlockAnimations.m | 2 +- tests/unit/InitialVelocityTests.swift | 70 +++++++++++-------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index cd68e9c..da966e7 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -138,7 +138,8 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * tra // 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]]); + && [traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]] + && [animation respondsToSelector:@selector(setInitialVelocity:)]); MDMSpringTimingCurve *springTimingCurve = (MDMSpringTimingCurve *)traits.timingCurve; CASpringAnimation *springAnimation = (CASpringAnimation *)animation; #pragma clang diagnostic pop diff --git a/src/private/MDMBlockAnimations.m b/src/private/MDMBlockAnimations.m index 1e8ad0a..5d410cb 100644 --- a/src/private/MDMBlockAnimations.m +++ b/src/private/MDMBlockAnimations.m @@ -124,7 +124,7 @@ @interface MDMLayerDelegate: NSObject // animations, we queue up the modified actions and then add them all at the end of our // MDMAnimateImplicitly invocation. [context addActionForLayer:layer keyPath:event]; - return [NSNull null]; + return nil; } NSArray *MDMAnimateImplicitly(void (^work)(void)) { diff --git a/tests/unit/InitialVelocityTests.swift b/tests/unit/InitialVelocityTests.swift index f432606..fa4b5fe 100644 --- a/tests/unit/InitialVelocityTests.swift +++ b/tests/unit/InitialVelocityTests.swift @@ -49,10 +49,12 @@ class InitialVelocityTests: XCTestCase { XCTAssertEqual(addedAnimations.count, 3) addedAnimations.flatMap { $0 as? CASpringAnimation }.forEach { animation in - XCTAssertEqual(animation.initialVelocity, 0.5, - "from: \(animation.fromValue!), " - + "to: \(animation.toValue!), " - + "withVelocity: \(velocity)") + if (animation.responds(to: #selector(getter: CASpringAnimation.initialVelocity))) { + XCTAssertEqual(animation.initialVelocity, 0.5, + "from: \(animation.fromValue!), " + + "to: \(animation.toValue!), " + + "withVelocity: \(velocity)") + } } } @@ -62,10 +64,12 @@ class InitialVelocityTests: XCTestCase { XCTAssertEqual(addedAnimations.count, 3) addedAnimations.flatMap { $0 as? CASpringAnimation }.forEach { animation in - XCTAssertEqual(animation.initialVelocity, 0.5, - "from: \(animation.fromValue!), " - + "to: \(animation.toValue!), " - + "withVelocity: \(velocity)") + if (animation.responds(to: #selector(getter: CASpringAnimation.initialVelocity))) { + XCTAssertEqual(animation.initialVelocity, 0.5, + "from: \(animation.fromValue!), " + + "to: \(animation.toValue!), " + + "withVelocity: \(velocity)") + } } } @@ -75,10 +79,12 @@ class InitialVelocityTests: XCTestCase { XCTAssertEqual(addedAnimations.count, 3) addedAnimations.flatMap { $0 as? CASpringAnimation }.forEach { animation in - XCTAssertGreaterThan(animation.initialVelocity, 0, - "from: \(animation.fromValue!), " - + "to: \(animation.toValue!), " - + "withVelocity: \(velocity)") + if (animation.responds(to: #selector(getter: CASpringAnimation.initialVelocity))) { + XCTAssertGreaterThan(animation.initialVelocity, 0, + "from: \(animation.fromValue!), " + + "to: \(animation.toValue!), " + + "withVelocity: \(velocity)") + } } } @@ -88,10 +94,12 @@ class InitialVelocityTests: XCTestCase { XCTAssertEqual(addedAnimations.count, 3) addedAnimations.flatMap { $0 as? CASpringAnimation }.forEach { animation in - XCTAssertLessThan(animation.initialVelocity, 0, - "from: \(animation.fromValue!), " - + "to: \(animation.toValue!), " - + "withVelocity: \(velocity)") + if (animation.responds(to: #selector(getter: CASpringAnimation.initialVelocity))) { + XCTAssertLessThan(animation.initialVelocity, 0, + "from: \(animation.fromValue!), " + + "to: \(animation.toValue!), " + + "withVelocity: \(velocity)") + } } } @@ -101,10 +109,12 @@ class InitialVelocityTests: XCTestCase { XCTAssertEqual(addedAnimations.count, 3) addedAnimations.flatMap { $0 as? CASpringAnimation }.forEach { animation in - XCTAssertGreaterThan(animation.initialVelocity, 0, - "from: \(animation.fromValue!), " - + "to: \(animation.toValue!), " - + "withVelocity: \(velocity)") + if (animation.responds(to: #selector(getter: CASpringAnimation.initialVelocity))) { + XCTAssertGreaterThan(animation.initialVelocity, 0, + "from: \(animation.fromValue!), " + + "to: \(animation.toValue!), " + + "withVelocity: \(velocity)") + } } } @@ -114,10 +124,12 @@ class InitialVelocityTests: XCTestCase { XCTAssertEqual(addedAnimations.count, 3) addedAnimations.flatMap { $0 as? CASpringAnimation }.forEach { animation in - XCTAssertLessThan(animation.initialVelocity, 0, - "from: \(animation.fromValue!), " - + "to: \(animation.toValue!), " - + "withVelocity: \(velocity)") + if (animation.responds(to: #selector(getter: CASpringAnimation.settlingDuration))) { + XCTAssertLessThan(animation.initialVelocity, 0, + "from: \(animation.fromValue!), " + + "to: \(animation.toValue!), " + + "withVelocity: \(velocity)") + } } } @@ -127,10 +139,12 @@ class InitialVelocityTests: XCTestCase { XCTAssertEqual(addedAnimations.count, 3) addedAnimations.flatMap { $0 as? CASpringAnimation }.forEach { animation in - XCTAssertEqual(animation.duration, animation.settlingDuration, - "from: \(animation.fromValue!), " - + "to: \(animation.toValue!), " - + "withVelocity: \(velocity)") + if (animation.responds(to: #selector(getter: CASpringAnimation.settlingDuration))) { + XCTAssertEqual(animation.duration, animation.settlingDuration, + "from: \(animation.fromValue!), " + + "to: \(animation.toValue!), " + + "withVelocity: \(velocity)") + } } } From b45a6c4a20db5529a11fb5c9c64b52a916a15fa7 Mon Sep 17 00:00:00 2001 From: featherless Date: Tue, 6 Mar 2018 09:03:00 -0500 Subject: [PATCH 08/14] Reduce flakiness in UIKitBehavioralTests. (#113) --- .../project.pbxproj | 4 +++ tests/unit/AdditiveAnimatorTests.swift | 3 +- tests/unit/AnimationRemovalTests.swift | 4 +-- tests/unit/BeginFromCurrentStateTests.swift | 3 +- .../HeadlessLayerImplicitAnimationTests.swift | 3 +- tests/unit/ImplicitAnimationTests.swift | 3 +- tests/unit/InstantAnimationTests.swift | 3 +- .../unit/MotionAnimatorBehavioralTests.swift | 3 +- tests/unit/MotionAnimatorTests.swift | 3 +- tests/unit/NonAdditiveAnimatorTests.swift | 3 +- tests/unit/QuartzCoreBehavioralTests.swift | 3 +- tests/unit/SpringTimingCurveTests.swift | 3 +- tests/unit/UIKitBehavioralTests.swift | 13 +++---- tests/unit/UIKitEquivalencyTests.swift | 10 +++--- tests/unit/WindowManagement.swift | 35 +++++++++++++++++++ 15 files changed, 60 insertions(+), 36 deletions(-) create mode 100644 tests/unit/WindowManagement.swift diff --git a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj index 9ee6750..f5e5a1f 100644 --- a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 664F59981FCE5CE2002EC56D /* BeginFromCurrentStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59971FCE5CE2002EC56D /* BeginFromCurrentStateTests.swift */; }; 664F599A1FCE6661002EC56D /* NonAdditiveAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59991FCE6661002EC56D /* NonAdditiveAnimatorTests.swift */; }; 664F599C1FCE67DB002EC56D /* AdditiveAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F599B1FCE67DB002EC56D /* AdditiveAnimatorTests.swift */; }; + 666696D0204E0B78008D9B67 /* WindowManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666696CF204E0B78008D9B67 /* WindowManagement.swift */; }; 666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666FAA831D384A6B000363DA /* AppDelegate.swift */; }; 666FAA8B1D384A6B000363DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 666FAA8A1D384A6B000363DA /* Assets.xcassets */; }; 666FAA8E1D384A6B000363DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 666FAA8C1D384A6B000363DA /* LaunchScreen.storyboard */; }; @@ -68,6 +69,7 @@ 664F59971FCE5CE2002EC56D /* BeginFromCurrentStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginFromCurrentStateTests.swift; sourceTree = ""; }; 664F59991FCE6661002EC56D /* NonAdditiveAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonAdditiveAnimatorTests.swift; sourceTree = ""; }; 664F599B1FCE67DB002EC56D /* AdditiveAnimatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdditiveAnimatorTests.swift; sourceTree = ""; }; + 666696CF204E0B78008D9B67 /* WindowManagement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowManagement.swift; sourceTree = ""; }; 666FAA801D384A6B000363DA /* MotionAnimatorCatalog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MotionAnimatorCatalog.app; sourceTree = BUILT_PRODUCTS_DIR; }; 666FAA831D384A6B000363DA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Catalog/AppDelegate.swift; sourceTree = ""; }; 666FAA8A1D384A6B000363DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -252,6 +254,7 @@ 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */, 664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */, 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */, + 666696CF204E0B78008D9B67 /* WindowManagement.swift */, ); path = unit; sourceTree = ""; @@ -542,6 +545,7 @@ 66A6A6681FBA158000DE54CB /* AnimationRemovalTests.swift in Sources */, 6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */, 664F59981FCE5CE2002EC56D /* BeginFromCurrentStateTests.swift in Sources */, + 666696D0204E0B78008D9B67 /* WindowManagement.swift in Sources */, 66FD99FA1EE9FBBE00C53A82 /* MotionAnimatorTests.m in Sources */, 669B6CA91FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift in Sources */, 668819FA1FE2EB36003A9420 /* UIKitEquivalencyTests.swift in Sources */, diff --git a/tests/unit/AdditiveAnimatorTests.swift b/tests/unit/AdditiveAnimatorTests.swift index 1a13169..b77d428 100644 --- a/tests/unit/AdditiveAnimatorTests.swift +++ b/tests/unit/AdditiveAnimatorTests.swift @@ -35,8 +35,7 @@ class AdditiveAnimationTests: XCTestCase { traits = MDMAnimationTraits(duration: 1) - let window = UIWindow() - window.makeKeyAndVisible() + let window = getTestHarnessKeyWindow() view = UIView() // Need to animate a view's layer to get implicit animations. window.addSubview(view) diff --git a/tests/unit/AnimationRemovalTests.swift b/tests/unit/AnimationRemovalTests.swift index e62c117..0442c91 100644 --- a/tests/unit/AnimationRemovalTests.swift +++ b/tests/unit/AnimationRemovalTests.swift @@ -27,15 +27,13 @@ class AnimationRemovalTests: XCTestCase { var traits: MDMAnimationTraits! var view: UIView! - var originalImplementation: IMP? override func setUp() { super.setUp() animator = MotionAnimator() traits = MDMAnimationTraits(duration: 1) - let window = UIWindow() - window.makeKeyAndVisible() + let window = getTestHarnessKeyWindow() view = UIView() // Need to animate a view's layer to get implicit animations. window.addSubview(view) diff --git a/tests/unit/BeginFromCurrentStateTests.swift b/tests/unit/BeginFromCurrentStateTests.swift index 86ad3b8..430825e 100644 --- a/tests/unit/BeginFromCurrentStateTests.swift +++ b/tests/unit/BeginFromCurrentStateTests.swift @@ -37,8 +37,7 @@ class BeginFromCurrentStateTests: XCTestCase { traits = MDMAnimationTraits(duration: 1) - let window = UIWindow() - window.makeKeyAndVisible() + let window = getTestHarnessKeyWindow() view = UIView() // Need to animate a view's layer to get implicit animations. window.addSubview(view) diff --git a/tests/unit/HeadlessLayerImplicitAnimationTests.swift b/tests/unit/HeadlessLayerImplicitAnimationTests.swift index a811698..8549103 100644 --- a/tests/unit/HeadlessLayerImplicitAnimationTests.swift +++ b/tests/unit/HeadlessLayerImplicitAnimationTests.swift @@ -33,8 +33,7 @@ class HeadlessLayerImplicitAnimationTests: XCTestCase { override func setUp() { super.setUp() - window = UIWindow() - window.makeKeyAndVisible() + window = getTestHarnessKeyWindow() layer = CALayer() diff --git a/tests/unit/ImplicitAnimationTests.swift b/tests/unit/ImplicitAnimationTests.swift index 61cf5ec..4f27042 100644 --- a/tests/unit/ImplicitAnimationTests.swift +++ b/tests/unit/ImplicitAnimationTests.swift @@ -36,8 +36,7 @@ class ImplicitAnimationTests: XCTestCase { traits = MDMAnimationTraits(duration: 1) - let window = UIWindow() - window.makeKeyAndVisible() + let window = getTestHarnessKeyWindow() view = UIView() // Need to animate a view's layer to get implicit animations. window.addSubview(view) diff --git a/tests/unit/InstantAnimationTests.swift b/tests/unit/InstantAnimationTests.swift index 5403947..c311572 100644 --- a/tests/unit/InstantAnimationTests.swift +++ b/tests/unit/InstantAnimationTests.swift @@ -32,8 +32,7 @@ class InstantAnimationTests: XCTestCase { animator = MotionAnimator() - let window = UIWindow() - window.makeKeyAndVisible() + let window = getTestHarnessKeyWindow() view = UIView() // Need to animate a view's layer to get implicit animations. window.addSubview(view) diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index 9001b23..987826a 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -29,8 +29,7 @@ class AnimatorBehavioralTests: XCTestCase { override func setUp() { super.setUp() - window = UIWindow() - window.makeKeyAndVisible() + window = getTestHarnessKeyWindow() traits = MDMAnimationTraits(duration: 1) } diff --git a/tests/unit/MotionAnimatorTests.swift b/tests/unit/MotionAnimatorTests.swift index 2ae5215..a25bc91 100644 --- a/tests/unit/MotionAnimatorTests.swift +++ b/tests/unit/MotionAnimatorTests.swift @@ -29,8 +29,7 @@ class MotionAnimatorTests: XCTestCase { animator.additive = false let traits = MDMAnimationTraits(duration: 1) - let window = UIWindow() - window.makeKeyAndVisible() + let window = getTestHarnessKeyWindow() let view = UIView() // Need to animate a view's layer to get implicit animations. window.addSubview(view) diff --git a/tests/unit/NonAdditiveAnimatorTests.swift b/tests/unit/NonAdditiveAnimatorTests.swift index 453122c..fe9731a 100644 --- a/tests/unit/NonAdditiveAnimatorTests.swift +++ b/tests/unit/NonAdditiveAnimatorTests.swift @@ -35,8 +35,7 @@ class NonAdditiveAnimationTests: XCTestCase { traits = MDMAnimationTraits(duration: 1) - let window = UIWindow() - window.makeKeyAndVisible() + let window = getTestHarnessKeyWindow() view = UIView() // Need to animate a view's layer to get implicit animations. window.addSubview(view) diff --git a/tests/unit/QuartzCoreBehavioralTests.swift b/tests/unit/QuartzCoreBehavioralTests.swift index b2f353b..a640c73 100644 --- a/tests/unit/QuartzCoreBehavioralTests.swift +++ b/tests/unit/QuartzCoreBehavioralTests.swift @@ -29,8 +29,7 @@ class QuartzCoreBehavioralTests: XCTestCase { override func setUp() { super.setUp() - window = UIWindow() - window.makeKeyAndVisible() + window = getTestHarnessKeyWindow() } override func tearDown() { diff --git a/tests/unit/SpringTimingCurveTests.swift b/tests/unit/SpringTimingCurveTests.swift index 92ebed2..f49fdf4 100644 --- a/tests/unit/SpringTimingCurveTests.swift +++ b/tests/unit/SpringTimingCurveTests.swift @@ -33,8 +33,7 @@ class SpringTimingCurveTests: XCTestCase { animator = MotionAnimator() traits = MDMAnimationTraits(duration: 1) - let window = UIWindow() - window.makeKeyAndVisible() + let window = getTestHarnessKeyWindow() view = UIView() // Need to animate a view's layer to get implicit animations. window.addSubview(view) diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index 19d1d32..b17179a 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -38,31 +38,28 @@ class ShapeLayerBackedView: UIView { class UIKitBehavioralTests: XCTestCase { var view: UIView! + var window: UIWindow! - var originalImplementation: IMP? override func setUp() { super.setUp() - let window = UIWindow() - window.makeKeyAndVisible() - view = ShapeLayerBackedView() - window.addSubview(view) + window = getTestHarnessKeyWindow() rebuildView() } override func tearDown() { view = nil + window = nil super.tearDown() } private func rebuildView() { - let oldSuperview = view.superview! - view.removeFromSuperview() + window.subviews.forEach { $0.removeFromSuperview() } view = ShapeLayerBackedView() // Need to animate a view's layer to get implicit animations. view.layer.anchorPoint = .zero - oldSuperview.addSubview(view) + window.addSubview(view) // Connect our layers to the render server. CATransaction.flush() diff --git a/tests/unit/UIKitEquivalencyTests.swift b/tests/unit/UIKitEquivalencyTests.swift index 3610053..c330796 100644 --- a/tests/unit/UIKitEquivalencyTests.swift +++ b/tests/unit/UIKitEquivalencyTests.swift @@ -23,13 +23,13 @@ import MotionAnimator class UIKitEquivalencyTests: XCTestCase { var view: UIView! + var window: UIWindow! var originalImplementation: IMP? override func setUp() { super.setUp() - let window = UIWindow() - window.makeKeyAndVisible() + window = getTestHarnessKeyWindow() view = ShapeLayerBackedView() window.addSubview(view) @@ -38,16 +38,16 @@ class UIKitEquivalencyTests: XCTestCase { override func tearDown() { view = nil + window = nil super.tearDown() } private func rebuildView() { - let oldSuperview = view.superview! view.removeFromSuperview() view = ShapeLayerBackedView() // Need to animate a view's layer to get implicit animations. - view.layer.anchorPoint = .zero // Needed to ensure that animating the width/height don't also - oldSuperview.addSubview(view) + view.layer.anchorPoint = .zero + window.addSubview(view) // Connect our layers to the render server. CATransaction.flush() diff --git a/tests/unit/WindowManagement.swift b/tests/unit/WindowManagement.swift new file mode 100644 index 0000000..b4bae2b --- /dev/null +++ b/tests/unit/WindowManagement.swift @@ -0,0 +1,35 @@ +/* + Copyright 2018-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 Foundation +import UIKit + +// Certain test targets (UIKitBehavioralTests's +// testDefaultsAnimatesPositionAdditivelyFromItsModelLayerState and +// testBeginFromCurrentStateAnimatesPositionAdditivelyFromItsModelLayerState) become flaky on iOS +// 8.1 when multiple UIWindow instances are created across multiple tests. Reusing the same key +// window between test instances reduces this flakiness and improves the testing performance +// by a factor of 3 (from ~30s to ~10s overall test time). +func getTestHarnessKeyWindow() -> UIWindow { + let window: UIWindow + if let keyWindow = UIApplication.shared.keyWindow { + window = keyWindow + } else { + window = UIWindow() + window.makeKeyAndVisible() + } + return window +} From 66842d1c8fd865c39e0955b3a25becb775023818 Mon Sep 17 00:00:00 2001 From: featherless Date: Tue, 6 Mar 2018 09:21:30 -0500 Subject: [PATCH 09/14] Ensure that zero duration test is testing with zero duration. (#115) Was testing with a non-zero duration. Is now testing with a zero duration. --- tests/unit/MotionAnimatorTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/MotionAnimatorTests.swift b/tests/unit/MotionAnimatorTests.swift index a25bc91..1f8afe9 100644 --- a/tests/unit/MotionAnimatorTests.swift +++ b/tests/unit/MotionAnimatorTests.swift @@ -45,7 +45,7 @@ class MotionAnimatorTests: XCTestCase { func testCompletionCallbackIsExecutedWithZeroDuration() { let animator = MotionAnimator() - let traits = MDMAnimationTraits(duration: 1) + let traits = MDMAnimationTraits(duration: 0) let window = UIWindow() window.makeKeyAndVisible() From 891f25c08e6e7ae5105867a97bfe6823f09e55fd Mon Sep 17 00:00:00 2001 From: featherless Date: Tue, 6 Mar 2018 11:00:54 -0500 Subject: [PATCH 10/14] Update .travis.yml (#114) Expanded the Travis CI testing matrix to include older iOS versions. --- .travis.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 49a1955..35095c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,31 @@ language: objective-c -osx_image: xcode8.3 +osx_image: xcode9.2 sudo: false +env: + global: + - LC_CTYPE=en_US.UTF-8 + - LANG=en_US.UTF-8 + - LANGUAGE=en_US.UTF-8 +matrix: + include: + - osx_image: xcode9.2 + env: COVERAGE=code_coverage SDK="iphonesimulator11.2" DESTINATION="name=iPhone 6s,OS=11.2" + - osx_image: xcode9.2 + env: SDK="iphonesimulator11.2" DESTINATION="name=iPhone 6s,OS=10.3.1" + - osx_image: xcode9.2 + env: SDK="iphonesimulator11.2" DESTINATION="name=iPhone 6s,OS=9.3" + - osx_image: xcode9.2 + env: SDK="iphonesimulator11.2" DESTINATION="name=iPhone 6,OS=8.4" + - osx_image: xcode8.3 + env: SDK="iphonesimulator10.3" DESTINATION="name=iPhone 6,OS=8.1" before_install: - gem install cocoapods --no-rdoc --no-ri --no-document --quiet - pod install --repo-update script: - set -o pipefail - - 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; + - xcodebuild test -workspace MotionAnimator.xcworkspace -scheme MotionAnimatorCatalog -sdk "$SDK" -destination "$DESTINATION" -enableCodeCoverage YES ONLY_ACTIVE_ARCH=YES | xcpretty -c; after_success: + - if [ "$COVERAGE" == "code_coverage" ]; then + bash <(curl -s https://codecov.io/bash); + fi - bash <(curl -s https://codecov.io/bash) From 97d7bcd7d8549398e9ac176a15ab028b898781a6 Mon Sep 17 00:00:00 2001 From: featherless Date: Tue, 6 Mar 2018 11:24:37 -0500 Subject: [PATCH 11/14] Iterating on the readme. (#102) --- README.md | 326 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 196 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 3ab81e2..6b99a75 100644 --- a/README.md +++ b/README.md @@ -418,189 +418,255 @@ UIView.animate(withDuration: 0.8, animations: { Generates an animation with duration of 0.25. This isn't a typo: standalone layers read from the current CATransaction rather than UIView's parameters when implicitly animating, even when the change happens within a UIView animation block. -## Example apps/unit tests +### What properties can be explicitly animated? -Check out a local copy of the repo to access the Catalog application by running the following -commands: +For a full list of animatable CALayer properties, see the [Apple documentation](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/AnimatableProperties/AnimatableProperties.html). - git clone https://github.com/material-motion/motion-animator-objc.git - cd motion-animator-objc - pod install - open MotionAnimator.xcworkspace +MotionAnimator's explicit APIs can be used to animate any property that is animatable by Core Animation. -## Installation +### What properties can be implicitly animated? -### Installation with CocoaPods +UIKit and Core Animation have different rules about when and how a property can be implicitly animated. -> CocoaPods is a dependency manager for Objective-C and Swift libraries. CocoaPods automates the -> process of using third-party libraries in your projects. See -> [the Getting Started guide](https://guides.cocoapods.org/using/getting-started.html) for more -> information. You can install it with the following command: -> -> gem install cocoapods - -Add `motion-animator` to your `Podfile`: +UIView properties generate implicit animations **only** when they are changed within an `animateWithDuration:` animation block. - pod 'MotionAnimator' +CALayer properties generate implicit animations **only** when they are changed under either of the following conditions: -Then run the following command: +1. if the CALayer is backing a UIView, the CALayer property is a supported implicitly animatable property (this is not documented anywhere), and the property is changed within an `animateWithDuration:` block, or +2. if: the CALayer is **not** backing a UIView (an "unhosted layer"), the layer has been around for at least one CATransaction flush — either by invoking `CATransaction.flush()` or by letting the run loop pump at least once — and the property is changed at all. - pod install +This behavior can be somewhat difficult to reason through, most notably when trying to animate CALayer properties using the UIView `animateWithDuration:` APIs. For example, CALayer's cornerRadius was not animatable using `animateWithDuration:` up until iOS 11, and many other CALayer properties are still not implicitly animatable. -### Usage +```swift +// This doesn't work until iOS 11. +UIView.animate(withDuration: 0.8, animations: { + view.layer.borderWidth = 10 +}, completion: nil) -Import the framework: +// This works all the way back to iOS 8. +MotionAnimator.animate(withDuration: 0.8, animations: { + view.layer.borderWidth = 10 +}, completion: nil) +``` - @import MotionAnimator; +The MotionAnimator provides a more consistent implicit animation API with a well-defined set of supported properties. -You will now have access to all of the APIs. +### In general, when will changing a property cause an implicit animation? -## Guides +The following charts describe when changing a property on a given object will cause an implicit animation to be generated. -- [How to make a spec from existing animations](#how-to-make-a-spec-from-existing-animations) -- [How to animate explicit layer properties](#how-to-animate-explicit-layer-properties) -- [How to animate like UIView](#how-to-animate-like-UIView) -- [How to animate a transition](#how-to-animate-a-transition) -- [How to animate an interruptible transition](#how-to-animate-an-interruptible-transition) +#### UIView -### How to make a spec from existing animations +```swift +let view = UIView() -A *motion spec* is a complete representation of the motion curves that meant to be applied during an -animation. Your motion spec might consist of a single `MDMMotionTiming` instance, or it might be a -nested structure of `MDMMotionTiming` instances, each representing motion for a different part of a -larger animation. In either case, your magic motion constants now have a place to live. +// inside animation block +UIView.animate(withDuration: 0.8, animations: { + view.alpha = 0.5 // Will generate an animation with a duration of 0.8 +}) -Consider a simple example of animating a view on and off-screen. Without a spec, our code might look -like so: +// outside animation block +view.alpha = 0.5 // Will not animate -```objc -CGPoint before = dismissing ? onscreen : offscreen; -CGPoint after = dismissing ? offscreen : onscreen; -view.center = before; -[UIView animateWithDuration:0.5 animations:^{ - view.center = after; -}]; +// inside MotionAnimator animation block +MotionAnimator.animate(withDuration: 0.8, animations: { + view.alpha = 0.5 // Will generate an animation with a duration of 0.8 +}) ``` -What if we want to change this animation to use a spring curve instead of a cubic bezier? To do so -we'll need to change our code to use a new API: +| UIVIew key path | inside animation block | outside animation block | inside MotionAnimator animation block | +|:-----------------------|:-----------------------|:------------------------|:--------------------------------------| +| `alpha` | ✓ | | ✓ | +| `backgroundColor` | ✓ | | ✓ | +| `bounds` | ✓ | | ✓ | +| `bounds.size.height` | ✓ | | ✓ | +| `bounds.size.width` | ✓ | | ✓ | +| `center` | ✓ | | ✓ | +| `center.x` | ✓ | | ✓ | +| `center.y` | ✓ | | ✓ | +| `transform` | ✓ | | ✓ | +| `transform.rotation.z` | ✓ | | ✓ | +| `transform.scale` | ✓ | | ✓ | -```objc -CGPoint before = dismissing ? onscreen : offscreen; -CGPoint after = dismissing ? offscreen : onscreen; -view.center = before; -[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:0 options:0 animations:^{ - view.center = after; -} completion:nil]; -``` +#### Backing CALayer -Now let's say we wrote the same code with a motion spec and animator: +Every UIView has a backing CALayer. -```objc -MDMMotionTiming motionSpec = { - .duration = 0.5, .curve = MDMMotionCurveMakeSpring(1, 100, 1), -}; +```swift +let view = UIView() -MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; -animator.shouldReverseValues = dismissing; -view.center = offscreen; -[_animator animateWithTiming:kMotionSpec animations:^{ - view.center = onscreen; -}]; -``` +// inside animation block +UIView.animate(withDuration: 0.8, animations: { + view.layer.opacity = 0.5 // Will generate an animation with a duration of 0.8 +}) -Now if we want to change our motion back to an easing curve, we only have to change the spec: +// outside animation block +view.layer.opacity = 0.5 // Will not animate -```objc -MDMMotionTiming motionSpec = { - .duration = 0.5, .curve = MDMMotionCurveMakeBezier(0.4f, 0.0f, 0.2f, 1.0f), -}; +// inside MotionAnimator animation block +MotionAnimator.animate(withDuration: 0.8, animations: { + view.layer.opacity = 0.5 // Will generate an animation with a duration of 0.8 +}) ``` -The animator code stays the same. It's now possible to modify the motion parameters at runtime -without affecting any of the animation logic. +| CALayer key path | inside animation block | outside animation block | inside MotionAnimator animation block | +|:-------------------------------|:-----------------------|:------------------------|:--------------------------------------| +| `anchorPoint` | ✓ (starting in iOS 11) | | ✓ | +| `backgroundColor` | | | ✓ | +| `bounds` | ✓ | | ✓ | +| `borderWidth` | | | ✓ | +| `borderColor` | | | ✓ | +| `cornerRadius` | ✓ (starting in iOS 11) | | ✓ | +| `bounds.size.height` | ✓ | | ✓ | +| `opacity` | ✓ | | ✓ | +| `position` | ✓ | | ✓ | +| `transform.rotation.z` | ✓ | | ✓ | +| `transform.scale` | ✓ | | ✓ | +| `shadowColor` | | | ✓ | +| `shadowOffset` | | | ✓ | +| `shadowOpacity` | | | ✓ | +| `shadowRadius` | | | ✓ | +| `strokeStart` | | | ✓ | +| `strokeEnd` | | | ✓ | +| `transform` | ✓ | | ✓ | +| `bounds.size.width` | ✓ | | ✓ | +| `position.x` | ✓ | | ✓ | +| `position.y` | ✓ | | ✓ | +| `zPosition` | | | ✓ | + +#### Unflushed, unhosted CALayer + +CALayers are unflushed until the next `CATransaction.flush()` invocation, which can happen either directly or at the end of the current run loop. -This pattern is useful for building transitions and animations. To learn more through examples, -see the following implementations: +```swift +let layer = CALayer() -**Material Components Activity Indicator** +// inside animation block +UIView.animate(withDuration: 0.8, animations: { + layer.opacity = 0.5 // Will not animate +}) -- [Motion spec declaration](https://github.com/material-components/material-components-ios/blob/develop/components/ActivityIndicator/src/private/MDCActivityIndicatorMotionSpec.h) -- [Motion spec definition](https://github.com/material-components/material-components-ios/blob/develop/components/ActivityIndicator/src/private/MDCActivityIndicatorMotionSpec.m) -- [Motion spec usage](https://github.com/material-components/material-components-ios/blob/develop/components/ActivityIndicator/src/MDCActivityIndicator.m#L461) +// outside animation block +layer.opacity = 0.5 // Will not animate -**Material Components Progress View** +// inside MotionAnimator animation block +MotionAnimator.animate(withDuration: 0.8, animations: { + layer.opacity = 0.5 // Will generate an animation with a duration of 0.8 +}) +``` -- [Motion spec declaration](https://github.com/material-components/material-components-ios/blob/develop/components/ProgressView/src/private/MDCProgressView%2BMotionSpec.h#L21) -- [Motion spec definition](https://github.com/material-components/material-components-ios/blob/develop/components/ProgressView/src/private/MDCProgressView%2BMotionSpec.m#L19) -- [Motion spec usage](https://github.com/material-components/material-components-ios/blob/develop/components/ProgressView/src/MDCProgressView.m#L155) +| CALayer key path | inside animation block | outside animation block | inside MotionAnimator animation block | +|:-------------------------------|:-----------------------|:------------------------|:--------------------------------------| +| `anchorPoint` | | | ✓ | +| `backgroundColor` | | | ✓ | +| `bounds` | | | ✓ | +| `borderWidth` | | | ✓ | +| `borderColor` | | | ✓ | +| `cornerRadius` | | | ✓ | +| `bounds.size.height` | | | ✓ | +| `opacity` | | | ✓ | +| `position` | | | ✓ | +| `transform.rotation.z` | | | ✓ | +| `transform.scale` | | | ✓ | +| `shadowColor` | | | ✓ | +| `shadowOffset` | | | ✓ | +| `shadowOpacity` | | | ✓ | +| `shadowRadius` | | | ✓ | +| `strokeStart` | | | ✓ | +| `strokeEnd` | | | ✓ | +| `transform` | | | ✓ | +| `bounds.size.width` | | | ✓ | +| `position.x` | | | ✓ | +| `position.y` | | | ✓ | +| `zPosition` | | | ✓ | + +#### Flushed, unhosted CALayer -**Material Components Masked Transition** +```swift +let layer = CALayer() -- [Motion spec declaration](https://github.com/material-components/material-components-ios/blob/develop/components/MaskedTransition/src/private/MDCMaskedTransitionMotionSpec.h#L20) -- [Motion spec definition](https://github.com/material-components/material-components-ios/blob/develop/components/MaskedTransition/src/private/MDCMaskedTransitionMotionSpec.m#L23) -- [Motion spec usage](https://github.com/material-components/material-components-ios/blob/develop/components/MaskedTransition/src/MDCMaskedTransition.m#L183) +// It's usually unnecessary to flush the transaction, unless you want to be able to implicitly +// animate it without using a MotionAnimator. +CATransaction.flush() -### How to animate explicit layer properties +// inside animation block +UIView.animate(withDuration: 0.8, animations: { + // Will generate an animation with a duration of 0.25 because it uses the CATransaction duration + // rather than the UIKit duration. + layer.opacity = 0.5 +}) -`MDMMotionAnimator` provides an explicit API for adding animations to animatable CALayer key paths. -This API is similar to creating a `CABasicAnimation` and adding it to the layer. +// outside animation block +// Will generate an animation with a duration of 0.25 +layer.opacity = 0.5 -```objc -[animator animateWithTiming:timing.chipHeight - toLayer:chipView.layer - withValues:@[ @(chipFrame.size.height), @(headerFrame.size.height) ] - keyPath:MDMKeyPathHeight]; +// inside MotionAnimator animation block +MotionAnimator.animate(withDuration: 0.8, animations: { + layer.opacity = 0.5 // Will generate an animation with a duration of 0.8 +}) ``` -### How to animate like UIView +| CALayer key path | inside animation block | outside animation block | inside MotionAnimator animation block | +|:-------------------------------|:-----------------------|:------------------------|:--------------------------------------| +| `anchorPoint` | ✓ | ✓ | ✓ | +| `backgroundColor` | | | ✓ | +| `bounds` | ✓ | ✓ | ✓ | +| `borderWidth` | ✓ | ✓ | ✓ | +| `borderColor` | ✓ | ✓ | ✓ | +| `cornerRadius` | ✓ | ✓ | ✓ | +| `bounds.size.height` | ✓ | ✓ | ✓ | +| `opacity` | ✓ | ✓ | ✓ | +| `position` | ✓ | ✓ | ✓ | +| `transform.rotation.z` | ✓ | ✓ | ✓ | +| `transform.scale` | ✓ | ✓ | ✓ | +| `shadowColor` | ✓ | ✓ | ✓ | +| `shadowOffset` | ✓ | ✓ | ✓ | +| `shadowOpacity` | ✓ | ✓ | ✓ | +| `shadowRadius` | ✓ | ✓ | ✓ | +| `strokeStart` | ✓ | ✓ | ✓ | +| `strokeEnd` | ✓ | ✓ | ✓ | +| `transform` | ✓ | ✓ | ✓ | +| `bounds.size.width` | ✓ | ✓ | ✓ | +| `position.x` | ✓ | ✓ | ✓ | +| `position.y` | ✓ | ✓ | ✓ | +| `zPosition` | ✓ | ✓ | ✓ | -`MDMMotionAnimator` provides an API that is similar to UIView's `animateWithDuration:`. Use this API -when you want to apply the same timing to a block of animations: +## Example apps/unit tests -```objc -chipView.frame = chipFrame; -[animator animateWithTiming:timing.chipHeight animations:^{ - chipView.frame = headerFrame; -}]; -// chipView.layer's position and bounds will now be animated with timing.chipHeight's timing. -``` +Check out a local copy of the repo to access the Catalog application by running the following +commands: -### How to animate a transition + git clone https://github.com/material-motion/motion-animator-objc.git + cd motion-animator-objc + pod install + open MotionAnimator.xcworkspace -Start by creating an `MDMMotionAnimator` instance. +## Installation -```objc -MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; -``` +### Installation with CocoaPods -When we describe our transition we'll describe it as though we're moving forward and take advantage -of the `shouldReverseValues` property on our animator to handle the reverse direction. +> CocoaPods is a dependency manager for Objective-C and Swift libraries. CocoaPods automates the +> process of using third-party libraries in your projects. See +> [the Getting Started guide](https://guides.cocoapods.org/using/getting-started.html) for more +> information. You can install it with the following command: +> +> gem install cocoapods -```objc -animator.shouldReverseValues = isTransitionReversed; -``` +Add `motion-animator` to your `Podfile`: -To animate a property on a view, we invoke the `animate` method. We must provide a timing, values, -and a key path: + pod 'MotionAnimator' -```objc -[animator animateWithTiming:timing - toLayer:view.layer - withValues:@[ @(collapsedHeight), @(expandedHeight) ] - keyPath:MDMKeyPathHeight]; -``` +Then run the following command: + + pod install -### How to animate an interruptible transition +### Usage -`MDMMotionAnimator` is configured by default to generate interruptible animations using Core -Animation's additive animation APIs. You can simply re-execute the `animate` calls when your -transition's direction changes and the animator will add new animations for the updated direction. +Import the framework: -## Helpful literature + @import MotionAnimator; -- [Additive animations: animateWithDuration in iOS 8](http://iosoteric.com/additive-animations-animatewithduration-in-ios-8/) -- [WWDC 2014 video on additive animations](https://developer.apple.com/videos/play/wwdc2014/236/) +You will now have access to all of the APIs. ## Contributing From ae163ca2975d8e3863c2c75d80808cb469ed820d Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 6 Mar 2018 11:30:36 -0500 Subject: [PATCH 12/14] Automatic changelog preparation for release. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e18d33..f3e3c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# #develop# + + TODO: Enumerate changes. + + # 2.8.0 This minor release introduces support for animating more key paths and support for drop-in UIView animation API replacements. From 1bd269d8e9db3a8ce2f390f567c469708998973c Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 6 Mar 2018 11:36:39 -0500 Subject: [PATCH 13/14] Update changelog. --- CHANGELOG.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e3c4d..f61a8fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,29 @@ -# #develop# +# 2.8.1 - TODO: Enumerate changes. +This patch release resolves some runtime crashes, improves the stability of our unit tests, and features an improved README.md. +## Bug fixes + +Fixed unrecognized selector crashes on iOS 8 devices. + +Fixed crashes in Legacy API when providing nil completion blocks. + +## Source changes + +* [Ensure that zero duration test is testing with zero duration. (#115)](https://github.com/material-motion/motion-animator-objc/commit/66842d1c8fd865c39e0955b3a25becb775023818) (featherless) +* [Reduce flakiness in UIKitBehavioralTests. (#113)](https://github.com/material-motion/motion-animator-objc/commit/b45a6c4a20db5529a11fb5c9c64b52a916a15fa7) (featherless) +* [Return `nil` CAAction when swapping implementation (#109)](https://github.com/material-motion/motion-animator-objc/commit/53255ab590908cc18841e3eed4af440e52132376) (Robert Moore) +* [Fix crash in Legacy API for nil completion blocks (#110)](https://github.com/material-motion/motion-animator-objc/commit/fd9710d220bb48a64ac008a399be74d04f6ab8b7) (Robert Moore) + +## Non-source changes + +* [Iterating on the readme. (#102)](https://github.com/material-motion/motion-animator-objc/commit/97d7bcd7d8549398e9ac176a15ab028b898781a6) (featherless) +* [Update .travis.yml (#114)](https://github.com/material-motion/motion-animator-objc/commit/891f25c08e6e7ae5105867a97bfe6823f09e55fd) (featherless) +* [Add core animation quiz to the readme. (#108)](https://github.com/material-motion/motion-animator-objc/commit/05ad80b7074eadb3131543292b11f18837d91e6b) (featherless) +* [Add readme section on main thread animations vs Core Animation. (#107)](https://github.com/material-motion/motion-animator-objc/commit/ccd350d1fbdb8a95b6922bf99680d10183a91f90) (featherless) +* [Add API snippets section. (#106)](https://github.com/material-motion/motion-animator-objc/commit/823e0ffa286a6918aaf67b3a6070dab16cfa775d) (featherless) +* [Add drop in replacement APIs section to the readme (#105)](https://github.com/material-motion/motion-animator-objc/commit/d53f753a976b1d067d226ed9bfd1893875d4ab8d) (featherless) +* [Add a feature table to the readme. (#104)](https://github.com/material-motion/motion-animator-objc/commit/0632c668e26347208d60c464e683774cd9dab5b7) (featherless) # 2.8.0 From 693106eb26f63c42a424fc6af04d35494c9e8daa Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 6 Mar 2018 11:37:40 -0500 Subject: [PATCH 14/14] Bump the release. --- .jazzy.yaml | 4 ++-- MotionAnimator.podspec | 2 +- Podfile.lock | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 39bf2ae..a3243f5 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -1,7 +1,7 @@ module: MotionAnimator -module_version: 2.8.0 +module_version: 2.8.1 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.8.0 +github_file_prefix: https://github.com/material-motion/motion-animator-objc/tree/v2.8.1 diff --git a/MotionAnimator.podspec b/MotionAnimator.podspec index eaf7bfd..cdaad98 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.8.0" + s.version = "2.8.1" s.authors = "The Material Motion Authors" s.license = "Apache 2.0" s.homepage = "https://github.com/material-motion/motion-animator-objc" diff --git a/Podfile.lock b/Podfile.lock index 3cac659..393c140 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,6 @@ PODS: - - CatalogByConvention (2.2.0) - - MotionAnimator (2.8.0): + - CatalogByConvention (2.4.1) + - MotionAnimator (2.8.1): - MotionInterchange (~> 1.6) - MotionInterchange (1.6.0) @@ -13,10 +13,10 @@ EXTERNAL SOURCES: :path: ./ SPEC CHECKSUMS: - CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f - MotionAnimator: 8af077dac084b7880a4d2ddae31a26171087bd87 + CatalogByConvention: 16cd56d7e75b816e4eda0d62f9c5f0c82da8baff + MotionAnimator: 07399ec033ab44256276d71037402413922fbb89 MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3 -COCOAPODS: 1.3.1 +COCOAPODS: 1.4.0