From 22e3bfc4a5bbe504f8f7dad3d41804257b50d28f Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 8 Nov 2017 15:36:49 -0500 Subject: [PATCH 01/13] Add jazzy yaml. --- .jazzy.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .jazzy.yaml diff --git a/.jazzy.yaml b/.jazzy.yaml new file mode 100644 index 0000000..4ff5cc6 --- /dev/null +++ b/.jazzy.yaml @@ -0,0 +1,7 @@ +module: MotionAnimator +module_version: 2.1.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.1.1 From f34172534d153ff461fc57943abc81605b3349da Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 8 Nov 2017 16:04:42 -0500 Subject: [PATCH 02/13] Add a guide on building motion specs. --- README.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 39a39b8..b9f1a6e 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ commands: ## Guides 1. [Architecture](#architecture) +2. [How to make a spec from existing animations](#how-to-make-a-spec-from-existing-animations) 2. [How to animate a transition](#how-to-animate-a-transition) 3. [How to animate an interruptible transition](#how-to-animate-an-interruptible-transition) @@ -106,11 +107,87 @@ animation to a CALayer instance, call one of the `animate` method variants and a be added to the layer. This library depends on [MotionInterchange](https://github.com/material-motion/motion-interchange-objc) -in order to represent motion timing in a consistent fashion. +in order to represent motion timing as *specifications*, or *specs* for short. -### How to animate a transition +### How to make a spec from existing animations + +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. + +Consider a simple example of animating a view on and off-screen. Without a spec, our code might look +like so: + +```objc +CGPoint before = dismissing ? onscreen : offscreen; +CGPoint after = dismissing ? offscreen : onscreen; +view.center = before; +[UIView animateWithDuration:0.5 animations:^{ + view.center = after; +}]; +``` + +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: + +```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]; +``` + +Now let's say we wrote the same code with a motion spec and animator: + +```objc +static const MDMMotionTiming kMotionSpec = { + .duration = 0.5, .curve = _MDMSpring(1, 100, 1), +}; -> This guide assumes that you are animating a two state bi-directional transition. +MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; +animator.shouldReverseValues = dismissing; +view.center = offscreen; +[_animator animateWithTiming:kMotionSpec animations:^{ + view.center = onscreen; +}] +``` + +Now if we want to change our motion back to an easing curve, we only have to change the spec: + +```objc +static const MDMMotionTiming kMotionSpec = { + .duration = 0.5, .curve = _MDMBezier(0.4f, 0.0f, 0.2f, 1.0f), +}; +``` + +The animator code stays the same. It's now possible to modify the motion parameters at runtime +without affecting any of the animation logic. + +This pattern is useful for building transitions and animations. To learn more through examples, +see the following implementations: + +**Material Components Activity Indicator** + +- [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) + +**Material Components Progress View** + +- [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) + +**Material Components Masked Transition** + +- [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) + +### How to animate a transition Start by creating an `MDMMotionAnimator` instance. From f25998e6d7161ed46f89bdab799c1be678dc98a9 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 8 Nov 2017 16:21:45 -0500 Subject: [PATCH 03/13] Add more tutorials and rework the introduction. --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b9f1a6e..29c6443 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,19 @@ [![Platform](https://img.shields.io/cocoapods/p/MotionAnimator.svg)](http://cocoadocs.org/docsets/MotionAnimator) [![Docs](https://img.shields.io/cocoapods/metrics/doc-percent/MotionAnimator.svg)](http://cocoadocs.org/docsets/MotionAnimator) -This library turns [Motion Interchange](https://github.com/material-motion/motion-interchange-objc) -data structures into performant Core Animation animations using a lightweight animator object. +This library provides APIs that turn [Motion Interchange](https://github.com/material-motion/motion-interchange-objc) +**motion specifications** into animations. + +## What's a motion specification? + +A motion specification defines the delay, duration, and acceleration of animations in a simple data +format that can live separate from your animation logic. + +For example, let's say we wanted to describe the motion for this animation: -In the above example we're animating the expansion and collapse of a calendar event using the -following motion specification: +We might create a specification like so: ```objc struct CalendarChipTiming { @@ -38,9 +44,34 @@ typedef struct CalendarChipMotionSpec CalendarChipMotionSpec; FOUNDATION_EXTERN struct CalendarChipMotionSpec CalendarChipSpec; ``` -In our application logic, we first determine which motion timing to use and then we create an -instance of `MDMMotionAnimator`. The animator allows us to create animations with the given -motion timing. +With our implementation of the spec looking like so: + +```objc +struct CalendarChipMotionSpec CalendarChipSpec = { + .expansion = { + .chipWidth = { + .delay = 0.000, .duration = 0.285, .curve = MDMEightyForty, + }, + .chipHeight = { + .delay = 0.015, .duration = 0.360, .curve = MDMEightyForty, + }, + ... + }, + .collapse = { + .chipWidth = { + .delay = 0.045, .duration = 0.330, .curve = MDMEightyForty, + }, + .chipHeight = { + .delay = 0.000, .duration = 0.330, .curve = MDMEightyForty, + }, + ... + }, +}; +``` + +Our spec defines two different transition states: expansion and collapse. At runtime, we determine +which of these two specs we intend to use and then use the timings to animate our views with an +instance of `MDMMotionAnimator`: ```objc CalendarChipTiming timing = _expanded ? CalendarChipSpec.expansion : CalendarChipSpec.collapse; @@ -94,20 +125,11 @@ commands: ## Guides -1. [Architecture](#architecture) -2. [How to make a spec from existing animations](#how-to-make-a-spec-from-existing-animations) -2. [How to animate a transition](#how-to-animate-a-transition) -3. [How to animate an interruptible transition](#how-to-animate-an-interruptible-transition) - -### Architecture - -`MDMMotionAnimator` is the primary API provided by this library. You can configure the animations -that an animator creates by modifying its configuration properties. When you're ready to add an -animation to a CALayer instance, call one of the `animate` method variants and an animation will -be added to the layer. - -This library depends on [MotionInterchange](https://github.com/material-motion/motion-interchange-objc) -in order to represent motion timing as *specifications*, or *specs* for short. +- [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) ### How to make a spec from existing animations @@ -187,6 +209,31 @@ see the following implementations: - [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) +### How to animate explicit layer properties + +`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. + +```objc +[animator animateWithTiming:timing.chipHeight + toLayer:chipView.layer + withValues:@[ @(chipFrame.size.height), @(headerFrame.size.height) ] + keyPath:MDMKeyPathHeight]; +``` + +### How to animate like UIView + +`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: + +```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. +``` + ### How to animate a transition Start by creating an `MDMMotionAnimator` instance. From 0a7dac13f196c5d9774fe5f712a0f8b1b0a4026e Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 8 Nov 2017 16:23:12 -0500 Subject: [PATCH 04/13] Formatting. --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 29c6443..f5b31eb 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,17 @@ [![Platform](https://img.shields.io/cocoapods/p/MotionAnimator.svg)](http://cocoadocs.org/docsets/MotionAnimator) [![Docs](https://img.shields.io/cocoapods/metrics/doc-percent/MotionAnimator.svg)](http://cocoadocs.org/docsets/MotionAnimator) +--- + This library provides APIs that turn [Motion Interchange](https://github.com/material-motion/motion-interchange-objc) **motion specifications** into animations. -## What's a motion specification? +--- + +#### What is a motion specification? -A motion specification defines the delay, duration, and acceleration of animations in a simple data -format that can live separate from your animation logic. +A **motion specification** defines the delay, duration, and acceleration of animations in a simple +data format that can live separate from your animation logic. For example, let's say we wanted to describe the motion for this animation: From 800b2996ba39746adbdfe4bf19c18ccc37f2bc91 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 8 Nov 2017 16:24:36 -0500 Subject: [PATCH 05/13] Move example project up. --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f5b31eb..5a03e95 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,18 @@ animator.shouldReverseValues = !_expanded; ... ``` +A working implementation of this example can be seen in the included example app. + +## Example apps/unit tests + +Check out a local copy of the repo to access the Catalog application by running the following +commands: + + git clone https://github.com/material-motion/motion-animator-objc.git + cd motion-animator-objc + pod install + open MotionAnimator.xcworkspace + ## Installation ### Installation with CocoaPods @@ -117,16 +129,6 @@ Import the framework: You will now have access to all of the APIs. -## Example apps/unit tests - -Check out a local copy of the repo to access the Catalog application by running the following -commands: - - git clone https://github.com/material-motion/motion-animator-objc.git - cd motion-animator-objc - pod install - open MotionAnimator.xcworkspace - ## Guides - [How to make a spec from existing animations](#how-to-make-a-spec-from-existing-animations) From b1289ea58130aba8e8dc2455989130db9f8be5ed Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 8 Nov 2017 19:31:23 -0500 Subject: [PATCH 06/13] Ensure that deprecations are treated as warnings, not errors, when building with CocoaPods. --- Podfile | 2 +- Podfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Podfile b/Podfile index cdbaced..7936bb7 100644 --- a/Podfile +++ b/Podfile @@ -17,7 +17,7 @@ post_install do |installer| target.build_configurations.each do |configuration| configuration.build_settings['SWIFT_VERSION'] = "3.0" if target.name.start_with?("Motion") - configuration.build_settings['WARNING_CFLAGS'] ="$(inherited) -Wall -Wcast-align -Wconversion -Werror -Wextra -Wimplicit-atomic-properties -Wmissing-prototypes -Wno-sign-conversion -Wno-unused-parameter -Woverlength-strings -Wshadow -Wstrict-selector-match -Wundeclared-selector -Wunreachable-code" + configuration.build_settings['WARNING_CFLAGS'] ="$(inherited) -Wall -Wcast-align -Wconversion -Werror -Wextra -Wimplicit-atomic-properties -Wmissing-prototypes -Wno-sign-conversion -Wno-unused-parameter -Woverlength-strings -Wshadow -Wstrict-selector-match -Wundeclared-selector -Wunreachable-code -Wno-error=deprecated -Wno-error=deprecated-implementations" end end end diff --git a/Podfile.lock b/Podfile.lock index cc5a609..9290364 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,7 +2,7 @@ PODS: - CatalogByConvention (2.2.0) - MotionAnimator (2.1.1): - MotionInterchange (~> 1.2) - - MotionInterchange (1.2.0) + - MotionInterchange (1.3.0) DEPENDENCIES: - CatalogByConvention @@ -15,8 +15,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f MotionAnimator: f306d8ee1a6600b039345c548cb543ebaa9cdec4 - MotionInterchange: 499c98e7628a8a078905749734dbfedbfae54cca + MotionInterchange: 988fc0011e4b806cc33f2fb4f9566f5eeb4159e8 -PODFILE CHECKSUM: 3c50d819e57d8329e39f3f5677139bf93ac34b8b +PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3 COCOAPODS: 1.3.1 From 29b551ae730f1a48f37793c65bf14d761a544b6b Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 8 Nov 2017 19:33:02 -0500 Subject: [PATCH 07/13] Silence a deprecation warning in motion interchange 1.3.0. --- src/private/CABasicAnimation+MotionAnimator.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index 42cb562..b03c6fe 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -27,7 +27,10 @@ animation = nil; break; +#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); From ab431bd2416b43ce601b16fbb4abdb3b9eba851e Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 9 Nov 2017 19:45:44 -0500 Subject: [PATCH 08/13] Extract initial velocity from the motion timing. (#37) * Extract initial velocity from the motion timing. * Add docs and tests. * Docs. * Remove brackets. * Bump version and add docs. * Fix type. * Add missing imports. --- WORKSPACE | 2 +- .../project.pbxproj | 4 + src/MDMMotionAnimator.m | 4 +- src/private/CABasicAnimation+MotionAnimator.h | 14 +- src/private/CABasicAnimation+MotionAnimator.m | 162 ++++++++++++++++-- tests/unit/InitialVelocityTests.swift | 118 +++++++++++++ 6 files changed, 277 insertions(+), 27 deletions(-) create mode 100644 tests/unit/InitialVelocityTests.swift diff --git a/WORKSPACE b/WORKSPACE index b408249..ac1508c 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.2.0", + tag = "v1.3.0", ) diff --git a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj index ae53e92..2ea580d 100644 --- a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 2AA864EDA683CEF5FAA721BE /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DBE814C7B88BAD6337052DB /* Pods_UnitTests.framework */; }; 660636021FACC24300C3DFB8 /* TimeScaleFactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */; }; + 6625876C1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.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 */; }; @@ -46,6 +47,7 @@ 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 = ""; }; 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeScaleFactorTests.swift; sourceTree = ""; }; + 6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialVelocityTests.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 = ""; }; @@ -209,6 +211,7 @@ 66FD99F91EE9FBBE00C53A82 /* MotionAnimatorTests.m */, 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */, 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */, + 6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */, 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */, ); path = unit; @@ -485,6 +488,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6625876C1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift in Sources */, 660636021FACC24300C3DFB8 /* TimeScaleFactorTests.swift in Sources */, 66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */, 6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */, diff --git a/src/MDMMotionAnimator.m b/src/MDMMotionAnimator.m index 2cf4169..a983cb5 100644 --- a/src/MDMMotionAnimator.m +++ b/src/MDMMotionAnimator.m @@ -85,9 +85,7 @@ - (void)animateWithTiming:(MDMMotionTiming)timing animation.toValue = [values lastObject]; if (![animation.fromValue isEqual:animation.toValue]) { - if (self.additive) { - MDMMakeAnimationAdditive(animation); - } + MDMConfigureAnimation(animation, self.additive, timing); if (timing.delay != 0) { animation.beginTime = ([layer convertTime:CACurrentMediaTime() fromLayer:nil] diff --git a/src/private/CABasicAnimation+MotionAnimator.h b/src/private/CABasicAnimation+MotionAnimator.h index a555da6..6adc7ac 100644 --- a/src/private/CABasicAnimation+MotionAnimator.h +++ b/src/private/CABasicAnimation+MotionAnimator.h @@ -24,7 +24,13 @@ FOUNDATION_EXPORT CABasicAnimation *MDMAnimationFromTiming(MDMMotionTiming timing, CGFloat timeScaleFactor); -// Attemps to configure the provided animation to be additive. -// Not all animation value types are supported. If an animation's value type was not supported, -// the animation will not be modified. -FOUNDATION_EXPORT void MDMMakeAnimationAdditive(CABasicAnimation *animation); +// Attempts to configure the provided animation to be additive and, if the animation is a spring +// animation, will extract the initial velocity from the timing and apply it to the animation. +// +// 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. +// +// If the from and to values of the animation match then the behavior is undefined. +FOUNDATION_EXPORT void MDMConfigureAnimation(CABasicAnimation *animation, + BOOL wantsAdditive, + MDMMotionTiming timing); diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index b03c6fe..9e179fa 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -61,40 +61,164 @@ return animation; } -void MDMMakeAnimationAdditive(CABasicAnimation *animation) { +void MDMConfigureAnimation(CABasicAnimation *animation, + BOOL wantsAdditive, + MDMMotionTiming timing) { + if (!wantsAdditive && timing.curve.type != MDMMotionCurveTypeSpring) { + return; // Nothing to do here. + } + + // We can't infer the from/to value types from the animation if the values are NSValue types, so + // we map known key paths to their data types here: static NSSet *sizeKeyPaths = nil; static NSSet *positionKeyPaths = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sizeKeyPaths = [NSSet setWithArray:@[@"bounds.size"]]; - - positionKeyPaths = [NSSet setWithArray:@[@"position", - @"anchorPoint"]]; + positionKeyPaths = [NSSet setWithArray:@[@"position", @"anchorPoint"]]; }); if ([animation.toValue isKindOfClass:[NSNumber class]]) { - CGFloat currentValue = (CGFloat)[animation.fromValue doubleValue]; - CGFloat delta = currentValue - (CGFloat)[animation.toValue doubleValue]; - animation.fromValue = @(delta); - animation.toValue = @0; - animation.additive = true; + // Non-additive animations animate along a direct path between fromValue and toValue, regardless + // of the model layer. Additive animations, on the other hand, animate towards the layer's model + // value by applying this formula: + // + // presentationLayer.value = modelLayer.value + additiveAnim1.value ... additiveAnimN.value + // + // This formula is what allows additive animations to give the appearance of conservation of + // momentum when multiple additive animations are added to the same key path. + // + // To transform a non-additive animation into an additive animation, use the following formula: + // + // additiveAnimation.from = -(animation.to - animation.from) + // additiveAnimation.to = 0 + // + // For example, if we're animating from 50 to 100, our additive animation's from value will + // equal -(100 - 50) = -50. Because the accumulator is animating to 0 and our model layer is + // set to the destination value, our animation will give the appearance of animating from 50 to + // 100: + // + // | model value | accumulator | presentation value | + // |-------------|-------------|--------------------| + // | 100 | -50 | 50 | + // | 100 | -25 | 75 | + // | 100 | -10 | 90 | + // | 100 | -5 | 95 | + // | 100 | 0 | 100 | + + CGFloat from = (CGFloat)[animation.fromValue doubleValue]; + CGFloat to = (CGFloat)[animation.toValue doubleValue]; + CGFloat displacement = to - from; + CGFloat additiveDisplacement = -displacement; + + if (wantsAdditive) { + animation.fromValue = @(additiveDisplacement); + animation.toValue = @0; + animation.additive = true; + } + +#pragma clang diagnostic push + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. +#pragma clang diagnostic ignored "-Wpartial-availability" + if ([animation isKindOfClass:[CASpringAnimation class]]) { + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop + + CGFloat absoluteInitialVelocity = timing.curve.data[MDMSpringMotionCurveDataIndexInitialVelocity]; + + // Our timing's initialVelocity is in points per second, but Core Animation expects initial + // velocity to be in terms of displacement per second. + // + // From the UIView animateWithDuration header docs: + // + // "initialVelocity is a unit coordinate system, where 1 is defined as traveling the total + // animation distance in a second. So if you're changing an object's position by 200pt in + // this animation, and you want the animation to behave as if the object was moving at + // 100pt/s before the animation started, you'd pass 0.5. You'll typically want to pass 0 for + // the velocity." + // + // It's also important to know that an initial velocity > 0 indicates movement towards the + // destination, while an initial velocity < 0 indicates movement away from the destination. + // + // With this in mind, consider Core Animation's initialVelocity as having two bits of + // information: + // + // - Its sign. Positive is towards the destination. Negative is away. + // - Its amplitude, where amplitude * displacement = absolute initial velocity + // + // For example: If our absolute initial velocity is +200/s, and our displacement is -100, then + // Core Animation's initialVelocity is -2, with the (-) indicating that we're moving away from + // the destination and the 2 indicating we're moving twice the displacement over a second. + // Similarly, if our absolute initial velocity is -200/s, and our displacement is still -100 + // points, then Core Animation's initialVelocity is 2; only the sign has changed. + // + // We want to know amplitude, so we do some basic arithmetic to turn: + // + // amplitude * displacement = absolute initial velocity + // + // into: + // + // amplitude = absolute initial velocity / displacement + // + // As for our sign, if absoluteInitialVelocity matches the direction of displacement, then our + // sign will be positive. Otherwise, our sign will be negative, as expected by Core Animation. + + springAnimation.initialVelocity = absoluteInitialVelocity / displacement; + } } else if ([sizeKeyPaths containsObject:animation.keyPath]) { - CGSize currentValue = [animation.fromValue CGSizeValue]; - CGSize destinationValue = [animation.toValue CGSizeValue]; - CGSize delta = CGSizeMake(currentValue.width - destinationValue.width, - currentValue.height - destinationValue.height); - animation.fromValue = [NSValue valueWithCGSize:delta]; + CGSize from = [animation.fromValue CGSizeValue]; + CGSize to = [animation.toValue CGSizeValue]; + CGSize additiveDisplacement = CGSizeMake(from.width - to.width, from.height - to.height); + animation.fromValue = [NSValue valueWithCGSize:additiveDisplacement]; animation.toValue = [NSValue valueWithCGSize:CGSizeZero]; animation.additive = true; +#pragma clang diagnostic push + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. +#pragma clang diagnostic ignored "-Wpartial-availability" + if ([animation isKindOfClass:[CASpringAnimation class]]) { + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop + // Core Animation's velocity system is single dimensional, so we pick the dominant direction + // of movement and normalize accordingly. + CGFloat biggestDelta; + if (fabs(additiveDisplacement.width) > fabs(additiveDisplacement.height)) { + biggestDelta = additiveDisplacement.width; + } else { + biggestDelta = additiveDisplacement.height; + } + CGFloat displacement = -biggestDelta; + springAnimation.initialVelocity = springAnimation.initialVelocity / displacement; + } + } else if ([positionKeyPaths containsObject:animation.keyPath]) { - CGPoint currentValue = [animation.fromValue CGPointValue]; - CGPoint destinationValue = [animation.toValue CGPointValue]; - CGPoint delta = CGPointMake(currentValue.x - destinationValue.x, - currentValue.y - destinationValue.y); - animation.fromValue = [NSValue valueWithCGPoint:delta]; + CGPoint from = [animation.fromValue CGPointValue]; + CGPoint to = [animation.toValue CGPointValue]; + CGPoint additiveDisplacement = CGPointMake(from.x - to.x, from.y - to.y); + animation.fromValue = [NSValue valueWithCGPoint:additiveDisplacement]; animation.toValue = [NSValue valueWithCGPoint:CGPointZero]; animation.additive = true; + +#pragma clang diagnostic push + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. +#pragma clang diagnostic ignored "-Wpartial-availability" + if ([animation isKindOfClass:[CASpringAnimation class]]) { + CASpringAnimation *springAnimation = (CASpringAnimation *)animation; +#pragma clang diagnostic pop + // Core Animation's velocity system is single dimensional, so we pick the dominant direction + // of movement and normalize accordingly. + CGFloat biggestDelta; + if (fabs(additiveDisplacement.x) > fabs(additiveDisplacement.y)) { + biggestDelta = additiveDisplacement.x; + } else { + biggestDelta = additiveDisplacement.y; + } + CGFloat displacement = -biggestDelta; + springAnimation.initialVelocity = springAnimation.initialVelocity / displacement; + } } } diff --git a/tests/unit/InitialVelocityTests.swift b/tests/unit/InitialVelocityTests.swift new file mode 100644 index 0000000..ef36029 --- /dev/null +++ b/tests/unit/InitialVelocityTests.swift @@ -0,0 +1,118 @@ +/* + 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 XCTest +#if IS_BAZEL_BUILD +import _MotionAnimator +#else +import MotionAnimator +#endif + +@available(iOS 9.0, *) +class InitialVelocityTests: XCTestCase { + + var animator: MotionAnimator! + var addedAnimations: [CAAnimation]! + override func setUp() { + super.setUp() + + animator = MotionAnimator() + addedAnimations = [] + animator.addCoreAnimationTracer { (_, animation) in + self.addedAnimations.append(animation) + } + } + + override func tearDown() { + animator = nil + addedAnimations = nil + + super.tearDown() + } + + func testVelocityAmplitudeMatchesDisplacementWithPositiveDisplacement() { + let timing = MotionTiming(delay: 0, + duration: 0.7, + curve: .init(type: .spring, data: (1, 1, 1, 50)), + repetition: .init(type: .none, amount: 0, autoreverses: false)) + animator.animate(with: timing, to: CALayer(), withValues: [0, 100], keyPath: .opacity) + + XCTAssertEqual(addedAnimations.count, 1) + let animation = addedAnimations.first as! CASpringAnimation + XCTAssertEqual(animation.initialVelocity, 0.5) + } + + func testVelocityAmplitudeMatchesDisplacementWithNegativeDisplacement() { + let timing = MotionTiming(delay: 0, + duration: 0.7, + curve: .init(type: .spring, data: (1, 1, 1, -50)), + repetition: .init(type: .none, amount: 0, autoreverses: false)) + animator.animate(with: timing, to: CALayer(), withValues: [100, 0], keyPath: .opacity) + + XCTAssertEqual(addedAnimations.count, 1) + let animation = addedAnimations.first as! CASpringAnimation + XCTAssertEqual(animation.initialVelocity, 0.5) + } + + func testVelocityTowardsDestinationIsPositiveWithPositiveDisplacement() { + let timing = MotionTiming(delay: 0, + duration: 0.7, + curve: .init(type: .spring, data: (1, 1, 1, 100)), + repetition: .init(type: .none, amount: 0, autoreverses: false)) + animator.animate(with: timing, to: CALayer(), withValues: [0, 100], keyPath: .opacity) + + XCTAssertEqual(addedAnimations.count, 1) + let animation = addedAnimations.first as! CASpringAnimation + XCTAssertGreaterThan(animation.initialVelocity, 0) + } + + func testVelocityAwayFromDestinationIsNegativeWithPositiveDisplacement() { + let timing = MotionTiming(delay: 0, + duration: 0.7, + curve: .init(type: .spring, data: (1, 1, 1, -100)), + repetition: .init(type: .none, amount: 0, autoreverses: false)) + animator.animate(with: timing, to: CALayer(), withValues: [0, 100], keyPath: .opacity) + + XCTAssertEqual(addedAnimations.count, 1) + let animation = addedAnimations.first as! CASpringAnimation + XCTAssertLessThan(animation.initialVelocity, 0) + } + + func testVelocityTowardsDestinationIsPositiveWithNegativeDisplacement() { + let timing = MotionTiming(delay: 0, + duration: 0.7, + curve: .init(type: .spring, data: (1, 1, 1, -100)), + repetition: .init(type: .none, amount: 0, autoreverses: false)) + animator.animate(with: timing, to: CALayer(), withValues: [100, 0], keyPath: .opacity) + + XCTAssertEqual(addedAnimations.count, 1) + let animation = addedAnimations.first as! CASpringAnimation + XCTAssertGreaterThan(animation.initialVelocity, 0) + } + + func testVelocityAwayFromDestinationIsNegativeWithNegativeDisplacement() { + let timing = MotionTiming(delay: 0, + duration: 0.7, + curve: .init(type: .spring, data: (1, 1, 1, 100)), + repetition: .init(type: .none, amount: 0, autoreverses: false)) + animator.animate(with: timing, to: CALayer(), withValues: [100, 0], keyPath: .opacity) + + XCTAssertEqual(addedAnimations.count, 1) + let animation = addedAnimations.first as! CASpringAnimation + XCTAssertLessThan(animation.initialVelocity, 0) + } +} + From f80203d711126130a5d58c880bae0cea4c72b6e7 Mon Sep 17 00:00:00 2001 From: featherless Date: Fri, 10 Nov 2017 10:14:07 -0500 Subject: [PATCH 09/13] Use MotionCurve make methods to create motion timings in the tests. (#38) --- tests/unit/ImplicitAnimationTests.swift | 2 +- tests/unit/MotionAnimatorTests.swift | 6 +++--- tests/unit/TimeScaleFactorTests.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/ImplicitAnimationTests.swift b/tests/unit/ImplicitAnimationTests.swift index 0dbb9e4..51e9d5f 100644 --- a/tests/unit/ImplicitAnimationTests.swift +++ b/tests/unit/ImplicitAnimationTests.swift @@ -36,7 +36,7 @@ class ImplicitAnimationTests: XCTestCase { timing = MotionTiming(delay: 0, duration: 0.7, - curve: .init(type: .bezier, data: (0, 0, 1, 1)), + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 1, p2y: 1), repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() diff --git a/tests/unit/MotionAnimatorTests.swift b/tests/unit/MotionAnimatorTests.swift index 9bc2082..09f507a 100644 --- a/tests/unit/MotionAnimatorTests.swift +++ b/tests/unit/MotionAnimatorTests.swift @@ -28,7 +28,7 @@ class MotionAnimatorTests: XCTestCase { let animator = MotionAnimator() let timing = MotionTiming(delay: 0, duration: 1, - curve: .init(type: .bezier, data: (0, 0, 0, 0)), + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), repetition: .init(type: .none, amount: 0, autoreverses: false)) let layer = CALayer() @@ -58,7 +58,7 @@ class MotionAnimatorTests: XCTestCase { let timing = MotionTiming(delay: 0, duration: 1, - curve: .init(type: .bezier, data: (0, 0, 0, 0)), + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() @@ -80,7 +80,7 @@ class MotionAnimatorTests: XCTestCase { let timing = MotionTiming(delay: 0, duration: 0, - curve: .init(type: .bezier, data: (0, 0, 0, 0)), + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), repetition: .init(type: .none, amount: 0, autoreverses: false)) let window = UIWindow() diff --git a/tests/unit/TimeScaleFactorTests.swift b/tests/unit/TimeScaleFactorTests.swift index 2d8c0d1..9e89ad4 100644 --- a/tests/unit/TimeScaleFactorTests.swift +++ b/tests/unit/TimeScaleFactorTests.swift @@ -25,7 +25,7 @@ class TimeScaleFactorTests: XCTestCase { let timing = MotionTiming(delay: 0, duration: 1, - curve: .init(type: .bezier, data: (0, 0, 0, 0)), + curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0), repetition: .init(type: .none, amount: 0, autoreverses: false)) var layer: CALayer! var addedAnimations: [CAAnimation]! From 3b543b856df8543d8af4127b8d5536fb31100d3b Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 10 Nov 2017 11:37:49 -0500 Subject: [PATCH 10/13] Bump MotionInterchange dependency to 1.3. --- MotionAnimator.podspec | 2 +- Podfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MotionAnimator.podspec b/MotionAnimator.podspec index c974696..9a826f5 100644 --- a/MotionAnimator.podspec +++ b/MotionAnimator.podspec @@ -12,5 +12,5 @@ Pod::Spec.new do |s| s.public_header_files = "src/*.h" s.source_files = "src/*.{h,m,mm}", "src/private/*.{h,m,mm}" - s.dependency "MotionInterchange", "~> 1.2" + s.dependency "MotionInterchange", "~> 1.3" end diff --git a/Podfile.lock b/Podfile.lock index 9290364..434b47b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,7 +1,7 @@ PODS: - CatalogByConvention (2.2.0) - MotionAnimator (2.1.1): - - MotionInterchange (~> 1.2) + - MotionInterchange (~> 1.3) - MotionInterchange (1.3.0) DEPENDENCIES: @@ -14,7 +14,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f - MotionAnimator: f306d8ee1a6600b039345c548cb543ebaa9cdec4 + MotionAnimator: e703a058f93ee5d4ddf895d469fe0c973d2b8c29 MotionInterchange: 988fc0011e4b806cc33f2fb4f9566f5eeb4159e8 PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3 From 8e1e14f1f9cae6d1b29078352fd212d957f7de7f Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 10 Nov 2017 11:38:06 -0500 Subject: [PATCH 11/13] Automatic changelog preparation for release. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5fd7e3..1a750e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# #develop# + + TODO: Enumerate changes. + + # 2.1.1 This patch release fixes issues with downstream bazel builds. From 3f29e061bce84c3d60457bf1c1f19a11709f11c1 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 10 Nov 2017 11:43:33 -0500 Subject: [PATCH 12/13] Update changelog. --- CHANGELOG.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a750e1..86576cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,31 @@ -# #develop# +# 2.2.0 - TODO: Enumerate changes. +This minor release introduces support for the new initial velocity spring curve value in +MotionInterchange v1.3.0. This release also includes additional public and internal documentation. +## Dependency changes + +The minimum MotionInterchange version has been increased to v1.3.0. + +## New features + +`MDMMotionAnimator` now supports initial velocity for spring curves. + +## Source changes + +* [Use MotionCurve make methods to create motion timings in the tests. (#38)](https://github.com/material-motion/motion-animator-objc/commit/f80203d711126130a5d58c880bae0cea4c72b6e7) (featherless) +* [Extract initial velocity from the motion timing. (#37)](https://github.com/material-motion/motion-animator-objc/commit/ab431bd2416b43ce601b16fbb4abdb3b9eba851e) (featherless) +* [Silence a deprecation warning in motion interchange 1.3.0.](https://github.com/material-motion/motion-animator-objc/commit/29b551ae730f1a48f37793c65bf14d761a544b6b) (Jeff Verkoeyen) + +## Non-source changes + +* [Bump MotionInterchange dependency to 1.3.](https://github.com/material-motion/motion-animator-objc/commit/3b543b856df8543d8af4127b8d5536fb31100d3b) (Jeff Verkoeyen) +* [Ensure that deprecations are treated as warnings, not errors, when building with CocoaPods.](https://github.com/material-motion/motion-animator-objc/commit/b1289ea58130aba8e8dc2455989130db9f8be5ed) (Jeff Verkoeyen) +* [Move example project up.](https://github.com/material-motion/motion-animator-objc/commit/800b2996ba39746adbdfe4bf19c18ccc37f2bc91) (Jeff Verkoeyen) +* [Formatting.](https://github.com/material-motion/motion-animator-objc/commit/0a7dac13f196c5d9774fe5f712a0f8b1b0a4026e) (Jeff Verkoeyen) +* [Add more tutorials and rework the introduction.](https://github.com/material-motion/motion-animator-objc/commit/f25998e6d7161ed46f89bdab799c1be678dc98a9) (Jeff Verkoeyen) +* [Add a guide on building motion specs.](https://github.com/material-motion/motion-animator-objc/commit/f34172534d153ff461fc57943abc81605b3349da) (Jeff Verkoeyen) +* [Add jazzy yaml.](https://github.com/material-motion/motion-animator-objc/commit/22e3bfc4a5bbe504f8f7dad3d41804257b50d28f) (Jeff Verkoeyen) # 2.1.1 From f1c48bdcc126704ef65c48fc74f08d800644ae6e Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 10 Nov 2017 11:43:44 -0500 Subject: [PATCH 13/13] Bump the release. --- .jazzy.yaml | 4 ++-- MotionAnimator.podspec | 2 +- Podfile.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 4ff5cc6..ad72f53 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -1,7 +1,7 @@ module: MotionAnimator -module_version: 2.1.1 +module_version: 2.2.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.1.1 +github_file_prefix: https://github.com/material-motion/motion-animator-objc/tree/v2.2.0 diff --git a/MotionAnimator.podspec b/MotionAnimator.podspec index 9a826f5..890a928 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.1.1" + s.version = "2.2.0" s.authors = "The Material Motion Authors" s.license = "Apache 2.0" s.homepage = "https://github.com/material-motion/motion-animator-objc" diff --git a/Podfile.lock b/Podfile.lock index 434b47b..15b21d4 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,6 @@ PODS: - CatalogByConvention (2.2.0) - - MotionAnimator (2.1.1): + - MotionAnimator (2.2.0): - MotionInterchange (~> 1.3) - MotionInterchange (1.3.0) @@ -14,7 +14,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f - MotionAnimator: e703a058f93ee5d4ddf895d469fe0c973d2b8c29 + MotionAnimator: 10d23fe5af75b53bfe43b1b046fc7052c52c870f MotionInterchange: 988fc0011e4b806cc33f2fb4f9566f5eeb4159e8 PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3