From 0f2a0381e824f9e759a12c9b20fe98c6016a11d8 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 6 Oct 2016 15:58:27 -0400 Subject: [PATCH] Implement Tween plan + unit test Summary: Spec: https://material-motion.gitbooks.io/material-motion-starmap/content/specifications/motion_family/tween.html Closes https://github.com/material-motion/material-motion-family-coreanimation-swift/issues/7 Reviewers: O4 Material Motion Apple platform reviewers, O2 Material Motion, markwei Reviewed By: O4 Material Motion Apple platform reviewers, O2 Material Motion, markwei Subscribers: markwei Tags: #material_motion Differential Revision: http://codereview.cc/D1683 --- .../Catalog/Catalog.xcodeproj/project.pbxproj | 8 +++ src/Tween.swift | 64 +++++++++++++++++++ src/private/TweenPerformer.swift | 49 ++++++++++++++ tests/unit/ObjectiveCAPITests.m | 36 +++++++++++ tests/unit/TweenTests.swift | 41 ++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 src/Tween.swift create mode 100644 src/private/TweenPerformer.swift create mode 100644 tests/unit/ObjectiveCAPITests.m create mode 100644 tests/unit/TweenTests.swift diff --git a/examples/apps/Catalog/Catalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/Catalog.xcodeproj/project.pbxproj index 05a6522..0f9b247 100644 --- a/examples/apps/Catalog/Catalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/Catalog.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 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 */; }; + 66A277841DA2E9DD00AFA57B /* TweenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A277831DA2E9DD00AFA57B /* TweenTests.swift */; }; + 66A2778D1DA6CDE800AFA57B /* ObjectiveCAPITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 66A2778C1DA6CDE800AFA57B /* ObjectiveCAPITests.m */; }; 66E6202E1D46B4250021B138 /* CoreAnimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E6202D1D46B4250021B138 /* CoreAnimationTests.swift */; }; 66E620301D47D2340021B138 /* SchedulerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E6202F1D47D2340021B138 /* SchedulerDelegate.swift */; }; 66FB71CE1FA2B5DC89A7883C /* Pods_MaterialMotionCoreAnimationFamily_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 443B77A9B48B8E8395819E7E /* Pods_MaterialMotionCoreAnimationFamily_UnitTests.framework */; }; @@ -38,6 +40,8 @@ 666FAA941D384A6B000363DA /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 666FAA9A1D384A6B000363DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../UnitTests/Info.plist; sourceTree = ""; }; 666FAAA71D384D51000363DA /* UnitTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "UnitTests-Bridging-Header.h"; path = "../UnitTests/UnitTests-Bridging-Header.h"; sourceTree = ""; }; + 66A277831DA2E9DD00AFA57B /* TweenTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweenTests.swift; sourceTree = ""; }; + 66A2778C1DA6CDE800AFA57B /* ObjectiveCAPITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectiveCAPITests.m; sourceTree = ""; }; 66E6202D1D46B4250021B138 /* CoreAnimationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreAnimationTests.swift; sourceTree = ""; }; 66E6202F1D47D2340021B138 /* SchedulerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchedulerDelegate.swift; sourceTree = ""; }; B2AEAB79F993F15CB0185C24 /* Pods-MaterialMotionCoreAnimationFamily-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MaterialMotionCoreAnimationFamily-UnitTests.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MaterialMotionCoreAnimationFamily-UnitTests/Pods-MaterialMotionCoreAnimationFamily-UnitTests.debug.xcconfig"; sourceTree = ""; }; @@ -115,6 +119,8 @@ children = ( 66E6202D1D46B4250021B138 /* CoreAnimationTests.swift */, 66E6202F1D47D2340021B138 /* SchedulerDelegate.swift */, + 66A277831DA2E9DD00AFA57B /* TweenTests.swift */, + 66A2778C1DA6CDE800AFA57B /* ObjectiveCAPITests.m */, ); name = tests; path = ../../../tests/unit; @@ -376,6 +382,8 @@ buildActionMask = 2147483647; files = ( 66E620301D47D2340021B138 /* SchedulerDelegate.swift in Sources */, + 66A277841DA2E9DD00AFA57B /* TweenTests.swift in Sources */, + 66A2778D1DA6CDE800AFA57B /* ObjectiveCAPITests.m in Sources */, 66E6202E1D46B4250021B138 /* CoreAnimationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/src/Tween.swift b/src/Tween.swift new file mode 100644 index 0000000..9c966a5 --- /dev/null +++ b/src/Tween.swift @@ -0,0 +1,64 @@ +/* + Copyright 2016-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 MaterialMotionRuntime + +/** Interpolate a CALayer property from one value to another. */ +@objc(MDMTween) +public final class Tween: NSObject, Plan { + /** The key path of the property whose value will be tweened. */ + public var keyPath: String + + /** The duration of the animation in seconds. */ + public var duration: CFTimeInterval + + /** The delay of the animation in seconds. */ + public var delay: CFTimeInterval = 0 + + /** The initial value of the tween. See CABasicAnimation documentation for more details. */ + public var from: Any? + + /** The final value of the tween. See CABasicAnimation documentation for more details. */ + public var to: Any? + + /** + The timing function to apply to the animation. + + A nil timing function indicates linear pacing. + */ + public var timingFunction: CAMediaTimingFunction? + + @objc(initWithKeyPath:duration:) + public init(_ keyPath: String, duration: CFTimeInterval) { + self.keyPath = keyPath + self.duration = duration + super.init() + } + + /** The performer that will fulfill this plan. */ + public func performerClass() -> AnyClass { + return TweenPerformer.self + } + /** Returns a copy of this plan. */ + public func copy(with zone: NSZone? = nil) -> Any { + let tween = Tween(keyPath, duration: duration) + tween.from = from + tween.to = to + tween.timingFunction = timingFunction + tween.delay = delay + return tween + } +} diff --git a/src/private/TweenPerformer.swift b/src/private/TweenPerformer.swift new file mode 100644 index 0000000..ca5f2df --- /dev/null +++ b/src/private/TweenPerformer.swift @@ -0,0 +1,49 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit +import MaterialMotionRuntime + +class TweenPerformer: NSObject, PlanPerforming, ComposablePerforming { + let target: CALayer + required init(target: Any) { + if let view = target as? UIView { + self.target = view.layer + } else { + self.target = target as! CALayer + } + } + + func add(plan: Plan) { + let tween = plan as! Tween + + let animation = CABasicAnimation(keyPath: tween.keyPath) + animation.fromValue = tween.from + animation.toValue = tween.to + animation.timingFunction = tween.timingFunction + animation.duration = tween.duration + animation.beginTime = tween.delay + + let transaction = Transaction() + transaction.add(plan: animation, to: target) + emitter.emit(transaction: transaction) + } + + var emitter: TransactionEmitting! + func set(transactionEmitter: TransactionEmitting) { + emitter = transactionEmitter + } +} diff --git a/tests/unit/ObjectiveCAPITests.m b/tests/unit/ObjectiveCAPITests.m new file mode 100644 index 0000000..786f03a --- /dev/null +++ b/tests/unit/ObjectiveCAPITests.m @@ -0,0 +1,36 @@ +/* + Copyright 2016-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 + +@import MaterialMotionCoreAnimationFamily; + +@interface ObjectiveCAPITests : XCTestCase +@end + +@implementation ObjectiveCAPITests + +- (void)testTweenAPI { + MDMTween *tween = [[MDMTween alloc] initWithKeyPath:@"opacity" duration:0.1]; + tween.delay = 0.1; + tween.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; + tween.from = @2; + tween.to = @2; + XCTAssertEqual(tween.keyPath, @"opacity"); + XCTAssertEqual(tween.duration, 0.1); +} + +@end diff --git a/tests/unit/TweenTests.swift b/tests/unit/TweenTests.swift new file mode 100644 index 0000000..023b6aa --- /dev/null +++ b/tests/unit/TweenTests.swift @@ -0,0 +1,41 @@ +/* + Copyright 2016-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 +import MaterialMotionRuntime +import MaterialMotionCoreAnimationFamily + +class TweenTests: XCTestCase { + + func testDidPerformAndIdle() { + let animation = Tween("opacity", duration: 0.1) + + let view = UIView() + + let scheduler = Scheduler() + let delegate = TestableSchedulerDelegate() + delegate.didIdleExpectation = expectation(description: "Did idle") + scheduler.delegate = delegate + + let transaction = Transaction() + transaction.add(plan: animation, to: view) + scheduler.commit(transaction: transaction) + + waitForExpectations(timeout: 0.3) + + XCTAssertEqual(scheduler.activityState, .idle) + } +}