Skip to content

Commit

Permalink
Add Pinchable, Rotatable and DirectlyManipulable plans
Browse files Browse the repository at this point in the history
Summary:
closes #13 ([[ #13 | Add a DirectlyManipulable plan ]])

closes #12 ([[ #12 | Add a Rotatable plan ]])

closes #11 ([[ #11 | Add a Pinchable plan ]])

closes #9 ([[ #9 | GesturePerformer likely shouldn't be adding the gesture recognizer to the target ]])

Reviewers: O4 Material Motion Apple platform reviewers, O2 Material Motion, featherless

Reviewed By: O4 Material Motion Apple platform reviewers, O2 Material Motion, featherless

Subscribers: featherless

Tags: #material_motion

Differential Revision: http://codereview.cc/D1620
  • Loading branch information
rcameron committed Sep 14, 2016
1 parent 5813d0d commit 3167a04
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 76 deletions.
2 changes: 1 addition & 1 deletion Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ EXTERNAL SOURCES:

CHECKOUT OPTIONS:
MaterialMotionRuntime:
:commit: e002e18e8878f67ca5ae17ee0ce8a05357254bc4
:commit: f31cbc6d9b78576e85410b82942a25a53a4e5f6e
:git: https://github.com/material-motion/material-motion-runtime-objc.git

SPEC CHECKSUMS:
Expand Down
24 changes: 16 additions & 8 deletions examples/apps/Catalog/Catalog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
666FAA8B1D384A6B000363DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 666FAA8A1D384A6B000363DA /* Assets.xcassets */; };
666FAA8E1D384A6B000363DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 666FAA8C1D384A6B000363DA /* LaunchScreen.storyboard */; };
73D1CC614139358D78678E9F /* Pods_MaterialMotionDirectManipulationFamily_Catalog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47AD29C808E6149118D934CA /* Pods_MaterialMotionDirectManipulationFamily_Catalog.framework */; };
DE0294BD1D5CE6E300A5BBA5 /* GestureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0294BB1D5CE6B400A5BBA5 /* GestureTests.swift */; };
DE7977851D4FF53900691A95 /* DraggableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7977841D4FF53900691A95 /* DraggableViewController.swift */; };
DE0294BD1D5CE6E300A5BBA5 /* SimpleGestureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0294BB1D5CE6B400A5BBA5 /* SimpleGestureTests.swift */; };
DE73309F1D88A188003BBF53 /* ComposedGestureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE73309D1D88A14B003BBF53 /* ComposedGestureTests.swift */; };
DE7330A21D88BD9C003BBF53 /* TestableGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7330A01D88B9A4003BBF53 /* TestableGestureRecognizer.swift */; };
DE7977851D4FF53900691A95 /* DirectManipulationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7977841D4FF53900691A95 /* DirectManipulationViewController.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -41,8 +43,10 @@
797B1E98CB9449E4A6927255 /* Pods_MaterialMotionGesturesFamily_Catalog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MaterialMotionGesturesFamily_Catalog.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DB1C7A2F80E7D1E0F3503AFD /* Pods-MaterialMotionDirectManipulationFamily-UnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MaterialMotionDirectManipulationFamily-UnitTests.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MaterialMotionDirectManipulationFamily-UnitTests/Pods-MaterialMotionDirectManipulationFamily-UnitTests.release.xcconfig"; sourceTree = "<group>"; };
DC36123760B2FCBFD565C0E7 /* Pods-MaterialMotionDirectManipulationFamily-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MaterialMotionDirectManipulationFamily-UnitTests.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MaterialMotionDirectManipulationFamily-UnitTests/Pods-MaterialMotionDirectManipulationFamily-UnitTests.debug.xcconfig"; sourceTree = "<group>"; };
DE0294BB1D5CE6B400A5BBA5 /* GestureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureTests.swift; sourceTree = "<group>"; };
DE7977841D4FF53900691A95 /* DraggableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DraggableViewController.swift; sourceTree = "<group>"; };
DE0294BB1D5CE6B400A5BBA5 /* SimpleGestureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleGestureTests.swift; sourceTree = "<group>"; };
DE73309D1D88A14B003BBF53 /* ComposedGestureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposedGestureTests.swift; sourceTree = "<group>"; };
DE7330A01D88B9A4003BBF53 /* TestableGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestableGestureRecognizer.swift; sourceTree = "<group>"; };
DE7977841D4FF53900691A95 /* DirectManipulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectManipulationViewController.swift; sourceTree = "<group>"; };
F3DF17AC229B9E4FE3049EC8 /* Pods-MaterialMotionDirectManipulationFamily-Catalog.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MaterialMotionDirectManipulationFamily-Catalog.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MaterialMotionDirectManipulationFamily-Catalog/Pods-MaterialMotionDirectManipulationFamily-Catalog.release.xcconfig"; sourceTree = "<group>"; };
F4B078EA77710D032C55A942 /* Pods-MaterialMotionDirectManipulationFamily-Catalog.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MaterialMotionDirectManipulationFamily-Catalog.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MaterialMotionDirectManipulationFamily-Catalog/Pods-MaterialMotionDirectManipulationFamily-Catalog.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -102,7 +106,9 @@
666FAA971D384A6B000363DA /* tests */ = {
isa = PBXGroup;
children = (
DE0294BB1D5CE6B400A5BBA5 /* GestureTests.swift */,
DE0294BB1D5CE6B400A5BBA5 /* SimpleGestureTests.swift */,
DE73309D1D88A14B003BBF53 /* ComposedGestureTests.swift */,
DE7330A01D88B9A4003BBF53 /* TestableGestureRecognizer.swift */,
);
name = tests;
path = ../../../tests/unit;
Expand Down Expand Up @@ -139,7 +145,7 @@
isa = PBXGroup;
children = (
666FAA831D384A6B000363DA /* AppDelegate.swift */,
DE7977841D4FF53900691A95 /* DraggableViewController.swift */,
DE7977841D4FF53900691A95 /* DirectManipulationViewController.swift */,
);
name = catalog;
sourceTree = "<group>";
Expand Down Expand Up @@ -368,7 +374,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DE7977851D4FF53900691A95 /* DraggableViewController.swift in Sources */,
DE7977851D4FF53900691A95 /* DirectManipulationViewController.swift in Sources */,
666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -377,7 +383,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DE0294BD1D5CE6E300A5BBA5 /* GestureTests.swift in Sources */,
DE0294BD1D5CE6E300A5BBA5 /* SimpleGestureTests.swift in Sources */,
DE7330A21D88BD9C003BBF53 /* TestableGestureRecognizer.swift in Sources */,
DE73309F1D88A188003BBF53 /* ComposedGestureTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
Expand Down
2 changes: 1 addition & 1 deletion examples/apps/Catalog/Catalog/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
window.rootViewController = UINavigationController(rootViewController: DraggableViewController())
window.rootViewController = UINavigationController(rootViewController: DirectManipulationViewController())
window.makeKeyAndVisible()
return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ import UIKit
import MaterialMotionDirectManipulationFamily
import MaterialMotionRuntime

class DraggableViewController: UIViewController {
class DirectManipulationViewController: UIViewController {

let scheduler = Scheduler()

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white

let draggableView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
draggableView.center = CGPoint(x: view.frame.midX, y: view.frame.midY)
draggableView.backgroundColor = UIColor.red
view.addSubview(draggableView)
let targetView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
targetView.center = CGPoint(x: view.frame.midX, y: view.frame.midY)
targetView.backgroundColor = UIColor.red
view.addSubview(targetView)

let transaction = Transaction()

transaction.add(plan: Draggable(), to: draggableView)
transaction.add(plan: DirectlyManipulable(), to: targetView)

scheduler.commit(transaction: transaction)
}
Expand Down
207 changes: 205 additions & 2 deletions src/DirectManipulationMotionFamily.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import UIKit
import MaterialMotionRuntime

/// A Draggable plan
/// A plan that enables a target to be dragged.
public final class Draggable: NSObject, Plan {
public let panGestureRecognizer: UIPanGestureRecognizer

Expand All @@ -29,9 +29,13 @@ public final class Draggable: NSObject, Plan {
public func performerClass() -> AnyClass {
return DraggablePerformer.self
}

public func copy(with zone: NSZone? = nil) -> Any {
return Draggable(withGestureRecognizer: panGestureRecognizer)
}
}

/// A DraggablePerformer
/// A gesture performer that enables its target to be dragged.
final class DraggablePerformer: NSObject, PlanPerforming {
let target: UIView

Expand Down Expand Up @@ -70,3 +74,202 @@ final class DraggablePerformer: NSObject, PlanPerforming {
target.transform = target.transform.translatedBy(x: translation.x, y: translation.y)
}
}

/// A plan that enables a target to be scaled by pinching.
public final class Pinchable: NSObject, Plan {
public let pinchGestureRecognizer: UIPinchGestureRecognizer

public init(withGestureRecognizer recognizer: UIPinchGestureRecognizer = UIPinchGestureRecognizer()) {
self.pinchGestureRecognizer = recognizer
super.init()
}

public func performerClass() -> AnyClass {
return PinchablePerformer.self
}

public func copy(with zone: NSZone? = nil) -> Any {
return Pinchable(withGestureRecognizer: pinchGestureRecognizer)
}
}

/// A gesture performer that enables its target to be scaled by pinching.
private final class PinchablePerformer: NSObject, PlanPerforming {
let target: UIView

private var previousScale: CGFloat = 1

required init(target: Any) {
self.target = target as! UIView
super.init()
}

func add(plan: Plan) {
guard let plan = plan as? Pinchable else {
fatalError("DraggablePerformer can only add Draggable plans.")
}

let recognizer = plan.pinchGestureRecognizer
recognizer.addTarget(self, action: #selector(handle(gesture:)))

if recognizer.view == nil {
target.addGestureRecognizer(recognizer)
}
}

func handle(gesture: UIPinchGestureRecognizer) {
if gesture.state == .began {
previousScale = 1
}

let newScale = 1 + (gesture.scale - previousScale)
target.transform = target.transform.scaledBy(x: newScale, y: newScale)
previousScale = gesture.scale
}
}

/// A plan that enables a target to be rotated using a two-finger rotation gesture.
public final class Rotatable: NSObject, Plan {
public let rotationGestureRecognizer: UIRotationGestureRecognizer

public init(withGestureRecognizer recognizer: UIRotationGestureRecognizer = UIRotationGestureRecognizer()) {
self.rotationGestureRecognizer = recognizer
super.init()
}

public func performerClass() -> AnyClass {
return RotatablePerformer.self
}

public func copy(with zone: NSZone? = nil) -> Any {
return Rotatable(withGestureRecognizer: rotationGestureRecognizer)
}
}

/// A gesture performer that enables its target to be rotated using a two-finger rotation gesture.
private final class RotatablePerformer: NSObject, PlanPerforming {
let target: UIView

private var previousRotation: CGFloat = 0

required init(target: Any) {
self.target = target as! UIView
super.init()
}

func add(plan: Plan) {
guard let plan = plan as? Rotatable else {
fatalError("RotatablePerformer can only add Rotatable plans.")
}

let recognizer = plan.rotationGestureRecognizer
recognizer.addTarget(self, action: #selector(handle(gesture:)))

if recognizer.view == nil {
target.addGestureRecognizer(recognizer)
}
}

func handle(gesture: UIGestureRecognizer) {
guard let gesture = gesture as? UIRotationGestureRecognizer else { return }

if gesture.state == .began {
previousRotation = 0
}

let rotation = gesture.rotation - previousRotation
target.transform = target.transform.rotated(by: rotation)
previousRotation = gesture.rotation
}
}

/// A plan that enables its target to be dragged, pinched and rotated simultaneously.
public final class DirectlyManipulable: NSObject, Plan {
public var panGestureRecognizer: UIPanGestureRecognizer {
return draggable.panGestureRecognizer
}
public var pinchGestureRecognizer: UIPinchGestureRecognizer {
return pinchable.pinchGestureRecognizer
}
public var rotationGestureRecognizer: UIRotationGestureRecognizer {
return rotatable.rotationGestureRecognizer
}

fileprivate let draggable: Draggable
fileprivate let pinchable: Pinchable
fileprivate let rotatable: Rotatable

public override convenience init() {
self.init(draggable: Draggable(), pinchable: Pinchable(), rotatable: Rotatable())
}

/// A private init to assist in making copies
///
/// Note that we can't provide defaults for the parameters,
/// else it will collide with the convenience init()
private init(draggable: Draggable, pinchable: Pinchable, rotatable: Rotatable) {
self.draggable = draggable
self.pinchable = pinchable
self.rotatable = rotatable
super.init()
}

public func performerClass() -> AnyClass {
return DirectlyManipulablePerformer.self
}

public func copy(with zone: NSZone? = nil) -> Any {
return DirectlyManipulable(draggable: draggable, pinchable: pinchable, rotatable: rotatable)
}
}

final class DirectlyManipulablePerformer: NSObject, PlanPerforming, ComposablePerforming {
let target: UIView

fileprivate var gestureRecognizers: [UIGestureRecognizer] = []

required init(target: Any) {
self.target = target as! UIView
super.init()
}

func add(plan: Plan) {
guard let plan = plan as? DirectlyManipulable else {
fatalError("DirectlyManipulablePerformer can only add DirectlyManipulable plans.")
}

gestureRecognizers = [
plan.draggable.panGestureRecognizer,
plan.pinchable.pinchGestureRecognizer,
plan.rotatable.rotationGestureRecognizer
]

let transaction = Transaction()
transaction.add(plan: plan.draggable, to: target)
transaction.add(plan: plan.pinchable, to: target)
transaction.add(plan: plan.rotatable, to: target)

// Set ourselves as each recognizer's delegate, if possible,
// in order to allow simultaneous recognition
for recognizer in gestureRecognizers {
if recognizer.delegate == nil {
recognizer.delegate = self
}
}

emitter.emit(transaction: transaction)
}

private var emitter: TransactionEmitting!

func set(transactionEmitter: TransactionEmitting) {
emitter = transactionEmitter
}
}

extension DirectlyManipulablePerformer: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
/// Allow the performer's gesture recognizers to recognizer simultaneously
return gestureRecognizers.contains(otherGestureRecognizer)
}
}
36 changes: 36 additions & 0 deletions tests/unit/ComposedGestureTests.swift
Original file line number Diff line number Diff line change
@@ -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 XCTest
import MaterialMotionRuntime
import MaterialMotionDirectManipulationFamily

class ComposedGestureTests: XCTestCase {
func testCopyingAComposedPlan() {
let originalPlan = DirectlyManipulable()
let copiedPlan = originalPlan.copy()

guard let castedCopy = copiedPlan as? DirectlyManipulable else {
XCTFail("Copied plan should be of type DirectlyManipulable")
return
}

XCTAssertNotEqual(originalPlan, castedCopy)
XCTAssertEqual(originalPlan.panGestureRecognizer, castedCopy.panGestureRecognizer)
XCTAssertEqual(originalPlan.pinchGestureRecognizer, castedCopy.pinchGestureRecognizer)
XCTAssertEqual(originalPlan.rotationGestureRecognizer, castedCopy.rotationGestureRecognizer)
}
}
Loading

0 comments on commit 3167a04

Please sign in to comment.