Skip to content

Instantly share code, notes, and snippets.

@jverkoey
Created December 29, 2016 19:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jverkoey/3cc52695d63a49f900c8ddb83cb73bbf to your computer and use it in GitHub Desktop.
Save jverkoey/3cc52695d63a49f900c8ddb83cb73bbf to your computer and use it in GitHub Desktop.
/*
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 IndefiniteObservable
import MaterialMotionStreams
enum TossDirection {
case none
case left
case right
}
class CardSwipeInteraction: Interaction {
let view: UIView
let tossDirection = createProperty(withInitialValue: TossDirection.none)
init(containerView: UIView, previousInteraction: CardSwipeInteraction? = nil, rotation: CGFloat) {
self.containerView = containerView
self.previousInteraction = previousInteraction
self.rotation = rotation
self.dragGesture = UIPanGestureRecognizer()
self.view = UIView(frame: .init(x: 16, y: 16 + 64,
width: containerView.bounds.size.width - 32,
height: containerView.bounds.size.height - 32 - 64))
view.layer.borderWidth = 0.5
view.layer.borderColor = UIColor(white: 0, alpha: 0.1).cgColor
view.layer.cornerRadius = 4
view.backgroundColor = UIColor(hue: CGFloat(arc4random_uniform(256)) / 256.0,
saturation: 1,
brightness: 1,
alpha: 1)
position = propertyOf(view).centerX
}
func connect(with runtime: MotionRuntime) {
view.addGestureRecognizer(dragGesture)
var destination = createProperty(withInitialValue: containerView.bounds.midX)
let attachment = AttachWithSpring(property: position,
to: destination,
springSource: popSpringSource)
let dragStream = gestureSource(dragGesture)
let directionStream = dragStream.onRecognitionState(.ended)
.velocity(in: containerView)
.x()
.threshold(min: -500, max: 500,
whenWithin: TossDirection.none,
whenBelow: TossDirection.left,
whenAbove: TossDirection.right)
runtime.write(directionStream, to: tossDirection)
let midpoint = containerView.bounds.midX
let left = -view.bounds.width
let right = containerView.bounds.width + view.bounds.width
let destinationStream = tossDirection._map { direction -> CGFloat in
switch direction {
case .none: return midpoint
case .left: return left
case .right: return right
}
}
runtime.write(destinationStream, to: attachment.destination)
let gestureEnabledStream = tossDirection._map { direction -> Bool in
switch direction {
case .none: return true
case .left: return false
case .right: return false
}
}
runtime.write(gestureEnabledStream,
to: ReactiveProperty(read: { self.dragGesture.isEnabled },
write: { self.dragGesture.isEnabled = $0 } ))
runtime.write(gestureEnabledStream,
to: ReactiveProperty(read: { self.view.isUserInteractionEnabled },
write: { self.view.isUserInteractionEnabled = $0 } ))
let initialVelocityStream = dragStream.onRecognitionState(.ended).velocity(in: containerView).x()
runtime.write(initialVelocityStream, to: attachment.initialVelocity)
let translationStream = dragStream
.translated(from: propertyOf(view).center, in: containerView)
.x()
runtime.write(attachment.valueStream.toggled(with: translationStream), to: position)
let radians = CGFloat(M_PI / 180.0 * 15.0)
let rotationStream = position
.offset(by: -containerView.bounds.width / 2)
.normalized(by: containerView.bounds.width / 2)
.scaled(by: CGFloat(radians))
// Previous card
if let previousInteraction = previousInteraction {
dragGesture.require(toFail: previousInteraction.dragGesture)
let nextRotationStream = previousInteraction.position
.distance(from: containerView.bounds.width / 2)
.normalized(by: containerView.bounds.width / 2)
.max(1)
.inverted()
.scaled(by: CGFloat(rotation))
runtime.write(nextRotationStream.toggled(with: rotationStream), to: propertyOf(view).rotation)
} else {
runtime.write(rotationStream, to: propertyOf(view).rotation)
}
}
private let containerView: UIView
private let dragGesture: UIPanGestureRecognizer
private let previousInteraction: CardSwipeInteraction?
private let position: ReactiveProperty<CGFloat>
private let rotation: CGFloat
}
class Callback<T>: Writable {
let callback: (T) -> Void
init(_ callback: @escaping (T) -> Void) {
self.callback = callback
}
func write(_ value: T) {
callback(value)
}
}
public class SwipeExampleViewController: UIViewController {
let runtime = MotionRuntime()
var queue: [CardSwipeInteraction] = []
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
(0 ..< 10).forEach { _ in
dequeueCard().connect(with: runtime)
}
}
var lastRotation: CGFloat = CGFloat(M_PI / 180.0 * 2)
func dequeueCard() -> CardSwipeInteraction {
let rotation = -lastRotation
let interaction = CardSwipeInteraction(containerView: view, previousInteraction: queue.last, rotation: rotation)
lastRotation = rotation
if let last = queue.last {
view.insertSubview(interaction.view, belowSubview: last.view)
} else {
view.addSubview(interaction.view)
}
queue.append(interaction)
return interaction
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment