Skip to content
Snippets Groups Projects
Commit c2d825db authored by Zhiwei Zhang's avatar Zhiwei Zhang
Browse files

test

parents
Branches master
No related tags found
No related merge requests found
Showing
with 1626 additions and 0 deletions
.DS_Store 0 → 100644
File added
File added
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'WhatToEat' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for WhatToEat
pod "Koloda", '~> 4.3.1'
pod "KSSwipeStack"
target 'WhatToEatTests' do
inherit! :search_paths
# Pods for testing
end
target 'WhatToEatUITests' do
inherit! :search_paths
# Pods for testing
end
end
PODS:
- Koloda (4.3.1):
- pop (~> 1.0)
- KSSwipeStack (0.4.3):
- RxSwift
- pop (1.0.10)
- RxSwift (4.1.2)
DEPENDENCIES:
- Koloda (~> 4.3.1)
- KSSwipeStack
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- Koloda
- KSSwipeStack
- pop
- RxSwift
SPEC CHECKSUMS:
Koloda: 0d39b2aa188ccd1fac48d752acaf9802e683c625
KSSwipeStack: e378571f132e24bdaabb2208a71ac8f9a44cb5b2
pop: 82ca6b068ce9278fd350fd9dd09482a0ce9492e6
RxSwift: e49536837d9901277638493ea537394d4b55f570
PODFILE CHECKSUM: 26d9aedb58daf9277ea640294290fbc9b657995b
COCOAPODS: 1.5.0
//
// KSSwipeStack.h
// KSSwipeStack
//
// Created by Simon Arneson on 2017-03-24.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for KSSwipeStack.
FOUNDATION_EXPORT double KSSwipeStackVersionNumber;
//! Project version string for KSSwipeStack.
FOUNDATION_EXPORT const unsigned char KSSwipeStackVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <KSSwipeStack/PublicHeader.h>
//
// PanDirectionGestureRecognizer.swift
// KSSwipeStack
//
// Created by Gustav Sundin on 26/09/16.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
// See http://stackoverflow.com/a/30607392/948942
//
import UIKit
import UIKit.UIGestureRecognizerSubclass
public enum PanDirection {
case Vertical
case Horizontal
}
public class PanDirectionGestureRecognizer: UIPanGestureRecognizer {
let direction: PanDirection
init(direction: PanDirection, target: AnyObject, action: Selector) {
self.direction = direction
super.init(target: target, action: action)
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if state == .began {
guard let view = self.view else {
return
}
let v = velocity(in: view)
switch direction {
case .Horizontal where fabs(v.y) > fabs(v.x):
state = .cancelled
case .Vertical where fabs(v.x) > fabs(v.y):
state = .cancelled
default:
break
}
}
}
}
//
// SwipableData.swift
// KSSwipeStack
//
// Created by Simon Arneson on 2017-03-24.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
import Foundation
/**
Protocol which must be implemented by all data models meant to be swiped in the card stack.
*/
public protocol SwipableData {
/**
- returns: The view to be rendered in the card stack representing this piece of data.
*/
func getView(with frame: CGRect) -> SwipableView
}
//
// SwipableView.swift
// KSSwipeStack
//
// Created by Simon Arneson on 2017-03-24.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
import UIKit
/**
Class which all view means to be presented in the card stack must inherit from.
*/
open class SwipableView: UIView {
private var data: SwipableData?
public override init(frame: CGRect) {
super.init(frame: frame)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
/**
Is fired when the view is being moved within the stack.
## You can here change the visuals of the view based on:
- Whether or not the card is being swiped to the right (liked) or (disliked)
- The requested opacity of any overlays the view might have, based on the x-position of the view.
- parameter like: true for like (right) and false for dislike (left)
- parameter opacity: the requested opacity of overlays. Based on x-position
*/
open func respondToSwipe(like: Bool, opacity: Float) {
}
/**
Should contain logic to reset the view to its initial, visual state.
This is for example called when a card is released, and snaps back to the center of the view.
*/
open func resetView() {
}
/**
This is being fired when a view is 'dimissed' from the stack.
For example when it has been swiped away and left the view.
*/
open func respondToDismiss() {
}
/**
Set the data this SwipableView is a representation of.
- parameter data: The data it is meant to represent.
*/
open func setData(_ data: SwipableData) {
self.data = data
}
/**
Get the data this SwipableView is a representation of.
- returns: The data it is meant to represent.
*/
open func getData() -> SwipableData? {
return data
}
/**
Should return whether or not the user is to be allowed to undo swiping this card.
*/
open func isUndoable() -> Bool {
return true
}
}
//
// Swipe.swift
// KSSwipeStack
//
// Created by Simon Arneson on 2017-03-24.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
import Foundation
/**
Representation of the swiping of a card in the stack
*/
public struct Swipe {
public var direction: SwipeDirection
public var data: SwipableData?
}
// SwipeDelegate.swift
// KSSwipeStack
//
// Created by Simon Arneson on 2017-03-24.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
import Foundation
/**
Implement this protocol to observe swipes in the card stack.
*/
public protocol SwipeDelegate {
/**
Fires on every swipe in the stack
- parameter swipe: The swipe which just occured.
*/
func onNext(_ swipe: Swipe)
}
//
// SwipeDirection.swift
// KSSwipeStack
//
// Created by Simon Arneson on 2017-03-24.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
import Foundation
public enum SwipeDirection {
case left
case right
case up
case down
func getSwipeEndpoint() -> CGPoint {
switch self {
case .left:
return CGPoint(x: -UIScreen.main.bounds.size.width * 2, y: 0)
case .right:
return CGPoint(x: UIScreen.main.bounds.size.width * 2, y: 0)
case .up:
return CGPoint(x: 0, y: -UIScreen.main.bounds.size.width * 2)
case .down:
return CGPoint(x: 0, y: UIScreen.main.bounds.size.width * 2)
}
}
}
//
// SwipeHelper.swift
// KSSwipeStack
//
// Created by Simon Arneson on 2016-09-27.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
import Foundation
import UIKit
class SwipeHelper {
private let swipeViewSize: CGSize
private let swipeViewCenter: CGPoint
open var options = SwipeOptions()
init(with frame: CGRect) {
swipeViewSize = frame.size
swipeViewCenter = CGPoint(x: frame.width / 2, y: frame.height / 2)
}
/// Move and animate a view to a desired position
///
/// - Parameters:
/// - card: The view to be move
/// - duration: The duration of the animation
/// - toPoint: Destination of the move action
func move(_ card: UIView, duration: Double = 0.25, toPoint: CGPoint) {
move(card, duration: duration, toPoint: toPoint, completion: {})
}
/// Move and animate a view to a desired position
///
/// - Parameters:
/// - card: The view to be move
/// - duration: The duration of the animation
/// - toPoint: Destination of the move action
/// - completion: Callback fireing when the animation is done and the view is moved.
func move(_ card: UIView, duration: Double = 0.25, toPoint: CGPoint, completion: @escaping () -> Void) {
UIView.animate(withDuration: duration, delay: 0, options: UIViewAnimationOptions.curveLinear, animations: {
card.frame = CGRect(origin: toPoint, size: card.frame.size)
card.layoutIfNeeded()
}, completion: { _ in
completion()
})
}
/// Move and animate a view to a desired position,
/// transforming the view according to the current SwipeOptions
/// Uses dismissAnimationDuration set in SwipeOptions
/// - Parameters:
/// - card: The view to be move
/// - toPoint: Destination of the move action
/// - completion: Callback fireing when the animation is done and the view is moved.
func moveFastAndTransform(_ card: SwipableView, toPoint: CGPoint, completion: @escaping () -> Void) {
addBorderToCard(card)
let rotation = calculateRotationAnimation(cardCenter: toPoint)
let scale = calculateScaleAnimation(cardCenter: toPoint)
card.respondToSwipe(like: toPoint.x > 0, opacity: toPoint.equalTo(swipeViewCenter) ? 0.0 : 1.0)
UIView.animate(withDuration: options.dismissAnimationDuration, delay: 0, options: UIViewAnimationOptions.curveLinear, animations: {
card.center = toPoint
card.layer.transform = CATransform3DConcat(rotation, scale)
card.layoutIfNeeded()
}, completion: { _ in
completion()
})
}
/// Calculate the magnitude if a throw based on the velocity vector
/// Used when determining if a card has been thrown 'hard enough' to be dismissed.
/// - Parameter velocity: velocity vector of the gesture
/// - Returns: magnitude of the throw
func calculateThrowMagnitude(for velocity: CGPoint) -> Float {
let velXSq = Float(velocity.x) * Float(velocity.x)
let velYSq = Float(velocity.y) * Float(velocity.y)
return sqrtf(velXSq + velYSq)
}
/// Returns a view to its original visual state in regards to border, rotation and scale.
/// Animates the reset of the view
/// - Parameter card: View to reset
func resetCard(_ card: UIView) {
let rotation = CATransform3DMakeRotation(0, 0, 0, 1)
let scale = CATransform3DMakeScale(1.0, 1.0, 1.0)
let borderAnim = CABasicAnimation(keyPath: "borderWidth")
borderAnim.fromValue = card.layer.borderWidth
borderAnim.toValue = 0
borderAnim.duration = 0.1
card.layer.borderWidth = 0
let cornerAnim = CABasicAnimation(keyPath: "cornerRadius")
cornerAnim.fromValue = card.layer.cornerRadius
cornerAnim.toValue = 0
cornerAnim.duration = 0.1
card.layer.cornerRadius = 0
let both = CAAnimationGroup()
both.duration = 0.1
both.animations = [cornerAnim, borderAnim]
both.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
card.layer.add(both, forKey: "both")
UIView.animate(withDuration: 0.1) {
card.layer.transform = CATransform3DConcat(rotation, scale)
}
}
/// Tranforms a view based on the preferences set in SwipeOptions
///
/// - Parameter card: View to tranform
func transformCard(_ card: SwipableView) {
let rotation = calculateRotationAnimation(cardCenter: card.center)
let scale = calculateScaleAnimation(cardCenter: card.center)
card.layer.transform = CATransform3DConcat(rotation, scale)
addBorderToCard(card)
}
func addBorderToCard(_ card: UIView) {
card.layer.borderWidth = 10
card.layer.borderColor = UIColor.white.cgColor
card.layer.cornerRadius = 10
card.layer.masksToBounds = true
}
private func calculateScaleAnimation(cardCenter: CGPoint) -> CATransform3D {
let horizontalDistance = calculateHorizontalDistanceFromCenter(cardCenter)
let verticalDistance = calculateVerticalDistanceFromCenter(cardCenter)
var scaleFactor = CGFloat(0)
if horizontalDistance >= verticalDistance {
scaleFactor = CGFloat(1 - horizontalDistance / 800.0)
} else {
scaleFactor = CGFloat(1 - verticalDistance / 1600.0)
}
return CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0)
}
private func calculateRotationAnimation(cardCenter: CGPoint) -> CATransform3D {
let xFromCenter = Double(cardCenter.x - swipeViewSize.width / 2)
var rads = CGFloat(xFromCenter / 10.0 * .pi / 180.0)
if abs(rads) > 1.4 {
rads = -rads
}
return CATransform3DMakeRotation(rads, 0, 0, 1)
}
/// Calculates the distance in the horizontal plane from the position of a view to the center of the screen
///
/// - Parameter cardCenter: A positonal coordinate, preferably the center of a view.
/// - Returns: The horizontal distance from the center of the screen
private func calculateHorizontalDistanceFromCenter(_ cardCenter: CGPoint) -> Double {
return Double(abs(cardCenter.x - swipeViewSize.width / 2))
}
/// Calculates the distance in the vertical plane from the position of a view to the center of the screen
///
/// - Parameter cardCenter: A positonal coordinate, preferably the center of a view.
/// - Returns: The vertical distance from the center of the screen
private func calculateVerticalDistanceFromCenter(_ cardCenter: CGPoint) -> Double {
return Double(abs(cardCenter.y - swipeViewSize.height / 2))
}
/// Calculate a proper destination for a dismissal of a view based on its position
/// Places the view far to the left if the view is to the left the the center of the screen and vice versa.
/// - Parameter card: View which endpoint to calculate
/// - Returns: Proper destination for the view
func calculateEndpoint(_ card: UIView) -> CGPoint {
let deltaX = card.center.x - swipeViewSize.width / 2
let deltaY = card.center.y - swipeViewSize.height / 2
let k = deltaY / deltaX
let toX = deltaX < 0 ? -swipeViewSize.height / 2 : swipeViewSize.width + swipeViewSize.height / 2
return CGPoint(x: toX, y: toX * k)
}
/// Calculate a proper destination for a dismissal of a view based on current velocity
/// Places the view far to the left if the view is currently moving to the left and vice versa.
/// The angle from the center to the proposed destination of the view is based on the angle of the velocity vector
/// - Parameter card: View which endpoint to calculate
/// - Returns: Proper destination for the view
func calculateEndpoint(_ card: UIView, basedOn velocity: CGPoint) -> CGPoint {
let k = velocity.y / velocity.x
let toX = velocity.x < 0 ? -swipeViewSize.height / 2 : swipeViewSize.width + swipeViewSize.height / 2
return CGPoint(x: toX, y: toX * k)
}
/// Converts a position with coordinates with the origin of the screen as origo to one using the center of the screen as origo.
/// Can be used to convert a origin value to a center value refering to the same positioning of a full screen view.
/// - Parameter center: Position using origin as origo
/// - Returns: Position with coordinates using center as origo
func convertToCenter(origin: CGPoint) -> CGPoint {
return CGPoint(x: origin.x + swipeViewSize.width / 2, y: origin.y + swipeViewSize.height / 2)
}
}
//
// SwipeOptions.swift
// KSSwipeStack
//
// Created by Simon Arneson on 2017-03-24.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
import UIKit
public struct SwipeOptions {
public var throwingThreshold = Float(800)
public var snapDuration = 0.1
public var allowHorizontalSwipes = true
public var allowVerticalSwipes = false
public var horizontalPanThreshold = CGFloat(0.5)
public var verticalPanThreshold = CGFloat(0.5)
public var visibleImageOrigin = CGPoint(x: 0, y: 0)
public var allowUndo = true
public var maxRenderedCards = 5
public var refillThreshold = 10
public var dismissAnimationDuration = 0.25
public var freezeWhenDismissing = false
public init() {}
}
//
// SwipeView.swift
// KSSwipeStack
//
// Created by Simon Arneson on 2017-03-24.
// Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//
import RxSwift
import UIKit
struct SwipeHistoryItem {
let data: SwipableData
let origin: CGPoint
}
/// Represents a swipable view to be rendered in the swipe stack.
/// The visual representation of a SwipeData object.
public class SwipeView: UIView {
private lazy var swipeHelper = SwipeHelper(with: frame)
private lazy var horizontalPan = PanDirectionGestureRecognizer(direction: .Horizontal, target: self, action: #selector(respondToHorizontalPan))
private lazy var verticalPan = PanDirectionGestureRecognizer(direction: .Vertical, target: self, action: #selector(respondToVerticalPan))
fileprivate var dataset: [SwipableData] = []
fileprivate var renderedCards: [SwipableView] = []
fileprivate var swipeHistory: [SwipeHistoryItem] = []
fileprivate var options = SwipeOptions()
fileprivate var swipeDelegate: SwipeDelegate?
fileprivate var swipeSubject: PublishSubject<Swipe>?
fileprivate var refillSubject: PublishSubject<Swipe>?
public func setup() {
setup(options: self.options, swipeDelegate: nil)
}
public func setup(options: SwipeOptions) {
setup(options: options, swipeDelegate: nil)
}
public func setup(swipeDelegate: SwipeDelegate?) {
setup(options: self.options, swipeDelegate: swipeDelegate)
}
public func setup(options: SwipeOptions, swipeDelegate: SwipeDelegate?) {
self.options = options
swipeHelper.options = options
if let swipeDelegate = swipeDelegate {
self.swipeDelegate = swipeDelegate
}
setSwipeDirections(horizontal: options.allowHorizontalSwipes, vertical: options.allowVerticalSwipes)
}
/**
Sets whether it should be possible to swipe cards horizontally and/or vertically.
- parameter horizontal: Set to true if the SwipeView should respond to horizontal swipes.
- parameter vertical: Set to true if the SwipeView should respond to vertical swipes.
*/
public func setSwipeDirections(horizontal: Bool, vertical: Bool) {
if horizontal {
addGestureRecognizer(horizontalPan)
} else {
removeGestureRecognizer(horizontalPan)
}
if vertical {
addGestureRecognizer(verticalPan)
} else {
removeGestureRecognizer(verticalPan)
}
}
/**
Adds a card to the stack and calls notifyDatasetUpdated to make sure it is rendered if needed.
- parameter data: The data the new card represents.
*/
public func addCard(_ data: SwipableData) {
dataset.append(data)
notifyDatasetUpdated()
}
/**
Adds a card to the top of the stack and calls notifyDatasetUpdated to make sure it is rendered if needed.
- parameter data: The data the new card represents.
*/
public func addCardToTop(_ data: SwipableData) {
let cardFrame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
let renderedCard = renderCard(data.getView(with: cardFrame))
renderedCards.insert(renderedCard, at: 0)
addSubview(renderedCard)
bringSubview(toFront: renderedCard)
}
/**
Adds a card to the top of the stack and calls notifyDatasetUpdated to make sure it is rendered if needed.
- parameter data: The data the new card represents.
*/
public func addCardToTop(_ data: SwipableData, from origin: CGPoint) {
let cardFrame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
let renderedCard = renderCard(data.getView(with: cardFrame))
renderedCard.frame.origin = origin
renderedCards.insert(renderedCard, at: 0)
addSubview(renderedCard)
swipeHelper.transformCard(renderedCard)
bringSubview(toFront: renderedCard)
snapBack()
}
/**
Get swipe events generated by the SwipeView
- returns: RxObservable firing once for each swipe
*/
public func getSwipes() -> Observable<Swipe> {
if let swipeSubject = swipeSubject {
return swipeSubject.asObserver()
}
swipeSubject = PublishSubject<Swipe>()
return getSwipes()
}
/**
Get notifications when the card stack has reached the refillThreshold defined in SwipeOptions
- returns: RxObservable firing with the swipe which put the stack below the refillThreshold
*/
public func needsRefill() -> Observable<Swipe> {
if let refillSubject = refillSubject {
return refillSubject.asObserver()
}
refillSubject = PublishSubject<Swipe>()
return needsRefill()
}
// Undoes last swipe
public func undoSwipe() {
guard options.allowUndo else {
return
}
if let cardToUndo = swipeHistory.popLast() {
addCardToTop(cardToUndo.data, from: cardToUndo.origin)
}
}
fileprivate func setupSwipeCards() {
}
/**
Fetch the card currently visible at the top of the stack.
- returns: The top card (the currently visible) in the stack.
*/
fileprivate func getCurrentCard() -> SwipableView? {
return renderedCards.first
}
/**
Notify the swipe view that the dataset has changed.
*/
public func notifyDatasetUpdated() {
if self.renderedCards.count < options.maxRenderedCards, !dataset.isEmpty {
fillStack()
}
}
/**
Fills the card stack by rendering new cards from the dataset if needed.
*/
private func fillStack() {
let cardFrame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
let card = renderCard(dataset.removeFirst().getView(with: cardFrame))
self.renderedCards.append(card)
if self.renderedCards.count < options.maxRenderedCards, !dataset.isEmpty {
fillStack()
}
}
/**
Renders a Swipeble View onto the screen and puts it in the correct postion in the stack.
- parameter view: The SwipeableView to render.
*/
func renderCard(_ view: SwipableView) -> SwipableView {
if !renderedCards.isEmpty, let lastCard = renderedCards.last {
insertSubview(view, belowSubview: lastCard)
} else {
addSubview(view)
sendSubview(toBack: view)
}
return view
}
/// Renders the next card from the stack
func showNextCard() {
if !renderedCards.isEmpty {
let swipedCard = renderedCards.removeFirst()
self.isUserInteractionEnabled = true
swipedCard.removeFromSuperview()
swipedCard.respondToDismiss()
}
if self.renderedCards.count < options.maxRenderedCards, !dataset.isEmpty {
fillStack()
}
isUserInteractionEnabled = true
}
/// Handles horizontal pan gestures (drags) in the view
///
/// - Parameter gesture: the gesture itself
@objc func respondToHorizontalPan(gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: self)
let velocity = gesture.velocity(in: self)
let magnitude = swipeHelper.calculateThrowMagnitude(for: velocity)
if let card = getCurrentCard() {
let previousOrigin = card.frame.origin
let nextOrigin = CGPoint(x: self.frame.origin.x + translation.x, y: self.frame.origin.y + translation.y)
card.center = CGPoint(x: frame.width / 2 + translation.x, y: frame.height / 2 + translation.y)
swipeHelper.transformCard(card)
let opacity = abs(Float(card.center.x.distance(to: self.center.x) / (frame.width / 4)))
card.respondToSwipe(like: translation.x > 0, opacity: opacity)
if gesture.state == .ended {
let throwingThresholdExceeded = magnitude > options.throwingThreshold
let panThresholdExceeded = abs(nextOrigin.x) > frame.width * options.horizontalPanThreshold
if throwingThresholdExceeded {
if velocity.x > 0 {
respondToSwipe(.right, gesture: gesture)
} else {
respondToSwipe(.left, gesture: gesture)
}
} else if panThresholdExceeded {
if previousOrigin.x < options.visibleImageOrigin.x {
respondToSwipe(.left, gesture: gesture)
} else {
respondToSwipe(.right, gesture: gesture)
}
} else {
snapBack()
}
} else if gesture.state == .cancelled {
snapBack()
}
}
}
/// Handles vertical pan gestures (drags) in the view
///
/// - Parameter gesture: the gesture itself
@objc func respondToVerticalPan(gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: self)
let velocity = gesture.velocity(in: self)
let magnitude = swipeHelper.calculateThrowMagnitude(for: velocity)
if let card = getCurrentCard() {
let previousOrigin = card.frame.origin
let nextOrigin = CGPoint(x: self.frame.origin.x + translation.x, y: self.frame.origin.y + translation.y)
card.center = CGPoint(x: frame.width / 2 + translation.x, y: frame.height / 2 + translation.y)
swipeHelper.transformCard(card)
let opacity = abs(Float(card.center.y.distance(to: self.center.y) / (frame.height / 4)))
card.respondToSwipe(like: translation.y > 0, opacity: opacity)
if gesture.state == .ended {
let throwingThresholdExceeded = magnitude > options.throwingThreshold
let panThresholdExceeded = abs(nextOrigin.y) > frame.height * options.verticalPanThreshold
if throwingThresholdExceeded {
if velocity.y > 0 {
respondToSwipe(.down, gesture: gesture)
} else {
respondToSwipe(.up, gesture: gesture)
}
} else if panThresholdExceeded {
if previousOrigin.y < options.visibleImageOrigin.y {
respondToSwipe(.up, gesture: gesture)
} else {
respondToSwipe(.down, gesture: gesture)
}
} else {
snapBack()
}
} else if gesture.state == .cancelled {
snapBack()
}
}
}
/// Handles when a view is swiped in the view
///
/// - Parameters:
/// - direction: The direction in which the view was swiped
/// - gesture: The gesture generating the swipe
open func respondToSwipe(_ direction: SwipeDirection, gesture: UIGestureRecognizer? = nil) {
guard let card = getCurrentCard() else {
// TODO:
return
}
if card.isUndoable(), let data = card.getData() {
swipeHistory.append(SwipeHistoryItem(data: data, origin: card.frame.origin))
}
dismissCard(direction: direction, gesture: gesture, completion: { [weak self] in
let swipe = Swipe(direction: direction, data: card.getData())
if let swipeHandler = self?.swipeDelegate {
swipeHandler.onNext(swipe)
}
if let swipeSubject = self?.swipeSubject {
swipeSubject.onNext(swipe)
}
if self?.needsRefill() ?? false, let refillSubject = self?.refillSubject {
refillSubject.onNext(swipe)
}
})
}
/// Resets the currently visible view to the original position with a 'snap' effect.
func snapBack() {
if let currentCard = getCurrentCard() {
swipeHelper.resetCard(currentCard)
swipeHelper.move(currentCard, duration: options.snapDuration, toPoint: options.visibleImageOrigin)
currentCard.resetView()
}
}
/// Get the number of items in the swipe view, both rendered and unrendered.
///
/// - Returns: The number of items in the dataset
func getDataCount() -> Int {
return self.renderedCards.count + self.dataset.count
}
/// Checks if the refill threshold is surpassed
///
/// - Returns: true if the data count is below the refill threshold
func needsRefill() -> Bool {
return getDataCount() <= options.refillThreshold
}
/// Used to dismiss a swipable view through an end position.
/// Animates a movement to specified position and then dismisses the swipable view.
/// - Parameters:
/// - toPoint: destination of dismissal animation
/// - gesture: the gesture generating the dismissal
/// - completion: callback firing when the animation is completed and the view is dismissed.
fileprivate func dismissCard(direction: SwipeDirection, gesture: UIGestureRecognizer?, completion: @escaping () -> Void) {
guard let card = getCurrentCard() else {
return
}
isUserInteractionEnabled = !options.freezeWhenDismissing
var toPoint = swipeHelper.convertToCenter(origin: direction.getSwipeEndpoint())
if options.allowHorizontalSwipes && !options.allowVerticalSwipes {
// Special case to better handle rapid flicks
if !(card.frame.origin.x == 0 && card.frame.origin.y == 0) {
if card.center.x > frame.width / 2 {
toPoint = swipeHelper.calculateEndpoint(card)
} else if let gesture = gesture as? UIPanGestureRecognizer {
let velocity = gesture.velocity(in: self)
if !(velocity.x == 0 && velocity.y == 0) {
toPoint = swipeHelper.calculateEndpoint(card, basedOn: velocity)
}
}
}
}
swipeHelper.moveFastAndTransform(card, toPoint: toPoint, completion: {
completion()
self.showNextCard()
})
}
}
MIT License
Copyright (c) 2017 Kicksort
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<p align="center">
<img src="http://kicksort.se/img/logo.png" alt="Kicksort" width="300"/>
</p>
# KSSwipeStack
[![CI Status](http://img.shields.io/travis/scanniza/KSSwipeStack.svg?style=flat)](https://travis-ci.org/scanniza/KSSwipeStack)
[![Version](https://img.shields.io/cocoapods/v/KSSwipeStack.svg?style=flat)](http://cocoapods.org/pods/KSSwipeStack)
[![License](https://img.shields.io/cocoapods/l/KSSwipeStack.svg?style=flat)](http://cocoapods.org/pods/KSSwipeStack)
[![Platform](https://img.shields.io/cocoapods/p/KSSwipeStack.svg?style=flat)](http://cocoapods.org/pods/KSSwipeStack)
KSSwipeStack is a lightweight card swiping library for iOS written in Swift.
KSSwipeStack handles any data model and the design/layout of the swipe cards are completely customizable.
Using the options provided you can customize the behavior and animations used in the swipe stack.
<p align="center">
<img src="https://media.giphy.com/media/w8BnmsjcJyFKU/giphy.gif" alt="Example GIF" width="270" height="480"/>
</p>
## Features
Built-in support for:
- [Custom Views](https://github.com/Kicksort/KSSwiftStack#Create-a-custom-class-extending-SwipableView)
- [Custom data model](https://github.com/Kicksort/KSSwiftStack#Create-a-simple-data-model-implementing-the-protocol-SwipableData)
- [Delegate pattern](https://github.com/Kicksort/KSSwiftStack#Using-SwipeDelegate)
- [RxSwift observables](https://github.com/Kicksort/KSSwiftStack#Using-RxSwift)
## Example
To run the [example project](https://github.com/Kicksort/KSSwiftStack/Example/), clone the repo, and run `pod install` from the Example directory first.
## Installation
KSSwipeStack is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod "KSSwipeStack"
```
## Getting started
### Create a [SwipeView](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipeView.swift)
[SwipeView](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipeView.swift) is the container of the swipe stack.
1. Add a [SwipeView](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipeView.swift) to your Storyboard/nib and create an outlet for it.
2. Run setup on said [SwipeView](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipeView.swift), using one of the provided `setup` methods.
- The simplest form takes no arguments:
```swift
swipeView.setup()
```
_____
- You can also use the setup method which takes a [SwipeOptions](#options) as argument in order to modify the behavior of the stack. See the [Options](#options) section for further details on how to use SwipeOptions.
```swift
var swipeOptions = SwipeOptions()
swipeOptions.allowVerticalSwipes = true
swipeView.setup(options: swipeOptions)
```
_____
- Finally you can pass along a [SwipeDelegate](#using-swipedelegate) reference (if you don't want to use RxSwift), either by itself:
```swift
swipeView.setup(swipeDelegate: self)
```
_____
- ...or together with some custom SwipeOptions:
```swift
swipeView.setup(swipeOptions: swipeOptions, swipeDelegate: self)
```
### Create a custom class extending [SwipableView](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipableView.swift)
Styled to properly represent on item of your data.
```swift
class ExampleCard: SwipableView {
override func setData(_ data: SwipableData) {
super.setData(data)
backgroundColor = .kicksortGray
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.width - 100, height: 200))
imageView.contentMode = .scaleAspectFit
imageView.image = #imageLiteral(resourceName: "kicksortLogoInverted")
imageView.center = center
addSubview(imageView)
}
}
```
### Create a simple data model implementing the protocol [SwipableData](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipableData.swift).
The protocol contains only one method, getView, in which you need to return a [SwipableView](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipableView.swift).
```swift
func getView(with frame: CGRect) -> SwipableView {
let view = ExampleCard(frame: frame)
view.setData(self)
return view
}
```
### Add cards to the stack
You can add any number of cards of any number of different types to the same stack.
You add a card by simply calling addCard with a parameter implementing [SwipableData](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipableData.swift).
```swift
swipeView.addCard(ExampleData())
```
### Handle swipes
#### Using [RxSwift](https://github.com/ReactiveX/RxSwift)
You can observe all swipe events coming from the stack using RxSwift by simply setting up an observer.
```swift
swipeView.getSwipes().subscribe(onNext: { (swipe) in
print("RX SWIPE EVENT")
}, onError: nil, onCompleted: nil, onDisposed: nil).addDisposableTo(disposableBag)
```
You can also observe if the stack needs a refill based on the refill threshold provided in [SwipeOptions](#options).
```swift
swipeView.needsRefill().subscribe(onNext: { (swipe) in
print("RX REFILL EVENT")
}, onError: nil, onCompleted: nil, onDisposed: nil).addDisposableTo(disposableBag)
```
#### Using [SwipeDelegate](https://github.com/Kicksort/KSSwipeStack/KSSwipeStack/Classes/SwipeDelegate.swift)
When setting up the SwipeView you can provide a Class implementing SwipeDelegate to handle the swipes received from the stack.
```swift
swipeView.setup(options: SwipeOptions(), swipeDelegate: self)
extension ViewController: SwipeDelegate {
func onNext(_ swipe: Swipe) {
dump("DELEGATE SWIPE EVENT")
}
}
```
## Extras
### Undo swipe
Call the `undoSwipe()` method in order to move the latest swiped card back to the stack. You can call the method any number of times in order to go back additional steps through the swipe history. Note that this feature can be disabled by setting `allowUndo` in SwipeOptions to false.
```swift
swipeView.undoSwipe()
```
If you want to prevent a specific card from being added to the swipe history (and therefore skipped when calling `undoSwipe()`), you should override `isUndoable()` for that SwipableView and return false.
## Options
Using the SwipeOptions struct you can modify the behavior ot the swipe stack.
```swift
public struct SwipeOptions {
public var throwingThreshold = Float(800)
public var snapDuration = 0.1
public var allowHorizontalSwipes = true
public var allowVerticalSwipes = false
public var horizontalPanThreshold = CGFloat(0.5)
public var verticalPanThreshold = CGFloat(0.5)
public var visibleImageOrigin = CGPoint(x: 0, y: 0)
public var allowUndo = true
public var maxRenderedCards = 5
public var refillThreshold = 10
public var dismissAnimationDuration = 0.25
public var freezeWhenDismissing = false
public init(){}
}
```
Specifying how 'hard' you have to throw a card for it to be dismissed.
```swift
public var throwingThreshold = Float(800)
```
Duration of the snap-back animation
```swift
public var snapDuration = 0.1
```
Make the swipe stack respond to horizontal swipes.
```swift
public var allowHorizontalSwipe = true
```
Make the swipe stack respond to vertical swipes.
```swift
public var allowVerticalSwipe = false
```
X-axis threshold for if a card is dismissed upon release.
```swift
public var horizontalPanThreshold = CGFloat(0.5)
```
Origin of a card in the 'original' state.
```swift
public var visibleImageOrigin = CGPoint(x: 0, y: 0)
```
Allow undoing of swipes.
```swift
public var allowUndo = true
```
How many cards should be rendered in the SwipeView at the same time.
```swift
public var maxRenderedCards = 5
```
Threshold of when a refill event is sent to refill subscribers.
```swift
public var refillThreshold = 10
```
Duration of the dismiss animation.
```swift
public var dismissAnimationDuration = 0.25
```
You can optionally choose to freeze the stack as a card is being dismissed to prevent the user from swiping
```swift
public var freezeWhenDismissing = false
```
## Authors
[arneson](https://github.com/arneson), Simon Arneson, arneson@kicksort.se
[Sundin](https://github.com/Sundin), Gustav Sundin, gustav@kicksort.se
## License
KSSwipeStack is available under the MIT license. See the LICENSE file for more info.
The MIT License (MIT)
Copyright (c) 2017 Yalantis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
//
// DraggableCardView.swift
// Koloda
//
// Created by Eugene Andreyev on 4/23/15.
// Copyright (c) 2015 Yalantis. All rights reserved.
//
import UIKit
import pop
public enum DragSpeed: TimeInterval {
case slow = 2.0
case moderate = 1.5
case `default` = 0.8
case fast = 0.4
}
protocol DraggableCardDelegate: class {
func card(_ card: DraggableCardView, wasDraggedWithFinishPercentage percentage: CGFloat, inDirection direction: SwipeResultDirection)
func card(_ card: DraggableCardView, wasSwipedIn direction: SwipeResultDirection)
func card(_ card: DraggableCardView, shouldSwipeIn direction: SwipeResultDirection) -> Bool
func card(cardWasReset card: DraggableCardView)
func card(cardWasTapped card: DraggableCardView)
func card(cardSwipeThresholdRatioMargin card: DraggableCardView) -> CGFloat?
func card(cardAllowedDirections card: DraggableCardView) -> [SwipeResultDirection]
func card(cardShouldDrag card: DraggableCardView) -> Bool
func card(cardSwipeSpeed card: DraggableCardView) -> DragSpeed
}
//Drag animation constants
private let defaultRotationMax: CGFloat = 1.0
private let defaultRotationAngle = CGFloat(Double.pi) / 10.0
private let defaultScaleMin: CGFloat = 0.8
private let screenSize = UIScreen.main.bounds.size
//Reset animation constants
private let cardResetAnimationSpringBounciness: CGFloat = 10.0
private let cardResetAnimationSpringSpeed: CGFloat = 20.0
private let cardResetAnimationKey = "resetPositionAnimation"
private let cardResetAnimationDuration: TimeInterval = 0.2
internal var cardSwipeActionAnimationDuration: TimeInterval = DragSpeed.default.rawValue
public class DraggableCardView: UIView, UIGestureRecognizerDelegate {
//Drag animation constants
public var rotationMax = defaultRotationMax
public var rotationAngle = defaultRotationAngle
public var scaleMin = defaultScaleMin
weak var delegate: DraggableCardDelegate? {
didSet {
configureSwipeSpeed()
}
}
private var overlayView: OverlayView?
private(set) var contentView: UIView?
private var panGestureRecognizer: UIPanGestureRecognizer!
private var tapGestureRecognizer: UITapGestureRecognizer!
private var animationDirectionY: CGFloat = 1.0
private var dragBegin = false
private var dragDistance = CGPoint.zero
private var swipePercentageMargin: CGFloat = 0.0
//MARK: Lifecycle
init() {
super.init(frame: CGRect.zero)
setup()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override public var frame: CGRect {
didSet {
if let ratio = delegate?.card(cardSwipeThresholdRatioMargin: self) , ratio != 0 {
swipePercentageMargin = ratio
} else {
swipePercentageMargin = 1.0
}
}
}
deinit {
removeGestureRecognizer(panGestureRecognizer)
removeGestureRecognizer(tapGestureRecognizer)
}
private func setup() {
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(DraggableCardView.panGestureRecognized(_:)))
addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.delegate = self
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(DraggableCardView.tapRecognized(_:)))
tapGestureRecognizer.delegate = self
tapGestureRecognizer.cancelsTouchesInView = false
addGestureRecognizer(tapGestureRecognizer)
if let delegate = delegate {
cardSwipeActionAnimationDuration = delegate.card(cardSwipeSpeed: self).rawValue
}
}
//MARK: Configurations
func configure(_ view: UIView, overlayView: OverlayView?) {
self.overlayView?.removeFromSuperview()
self.contentView?.removeFromSuperview()
if let overlay = overlayView {
self.overlayView = overlay
overlay.alpha = 0;
self.addSubview(overlay)
configureOverlayView()
self.insertSubview(view, belowSubview: overlay)
} else {
self.addSubview(view)
}
self.contentView = view
configureContentView()
}
private func configureOverlayView() {
if let overlay = self.overlayView {
overlay.translatesAutoresizingMaskIntoConstraints = false
let width = NSLayoutConstraint(
item: overlay,
attribute: NSLayoutAttribute.width,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.width,
multiplier: 1.0,
constant: 0)
let height = NSLayoutConstraint(
item: overlay,
attribute: NSLayoutAttribute.height,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.height,
multiplier: 1.0,
constant: 0)
let top = NSLayoutConstraint (
item: overlay,
attribute: NSLayoutAttribute.top,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.top,
multiplier: 1.0,
constant: 0)
let leading = NSLayoutConstraint (
item: overlay,
attribute: NSLayoutAttribute.leading,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.leading,
multiplier: 1.0,
constant: 0)
addConstraints([width,height,top,leading])
}
}
private func configureContentView() {
if let contentView = self.contentView {
contentView.translatesAutoresizingMaskIntoConstraints = false
let width = NSLayoutConstraint(
item: contentView,
attribute: NSLayoutAttribute.width,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.width,
multiplier: 1.0,
constant: 0)
let height = NSLayoutConstraint(
item: contentView,
attribute: NSLayoutAttribute.height,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.height,
multiplier: 1.0,
constant: 0)
let top = NSLayoutConstraint (
item: contentView,
attribute: NSLayoutAttribute.top,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.top,
multiplier: 1.0,
constant: 0)
let leading = NSLayoutConstraint (
item: contentView,
attribute: NSLayoutAttribute.leading,
relatedBy: NSLayoutRelation.equal,
toItem: self,
attribute: NSLayoutAttribute.leading,
multiplier: 1.0,
constant: 0)
addConstraints([width,height,top,leading])
}
}
func configureSwipeSpeed() {
if let delegate = delegate {
cardSwipeActionAnimationDuration = delegate.card(cardSwipeSpeed: self).rawValue
}
}
//MARK: GestureRecognizers
@objc func panGestureRecognized(_ gestureRecognizer: UIPanGestureRecognizer) {
dragDistance = gestureRecognizer.translation(in: self)
let touchLocation = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
let firstTouchPoint = gestureRecognizer.location(in: self)
let newAnchorPoint = CGPoint(x: firstTouchPoint.x / bounds.width, y: firstTouchPoint.y / bounds.height)
let oldPosition = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y)
let newPosition = CGPoint(x: bounds.size.width * newAnchorPoint.x, y: bounds.size.height * newAnchorPoint.y)
layer.anchorPoint = newAnchorPoint
layer.position = CGPoint(x: layer.position.x - oldPosition.x + newPosition.x, y: layer.position.y - oldPosition.y + newPosition.y)
removeAnimations()
dragBegin = true
animationDirectionY = touchLocation.y >= frame.size.height / 2 ? -1.0 : 1.0
layer.rasterizationScale = UIScreen.main.scale
layer.shouldRasterize = true
case .changed:
let rotationStrength = min(dragDistance.x / frame.width, rotationMax)
let rotationAngle = animationDirectionY * self.rotationAngle * rotationStrength
let scaleStrength = 1 - ((1 - scaleMin) * fabs(rotationStrength))
let scale = max(scaleStrength, scaleMin)
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, scale, scale, 1)
transform = CATransform3DRotate(transform, rotationAngle, 0, 0, 1)
transform = CATransform3DTranslate(transform, dragDistance.x, dragDistance.y, 0)
layer.transform = transform
let percentage = dragPercentage
updateOverlayWithFinishPercent(percentage, direction:dragDirection)
if let dragDirection = dragDirection {
//100% - for proportion
delegate?.card(self, wasDraggedWithFinishPercentage: min(fabs(100 * percentage), 100), inDirection: dragDirection)
}
case .ended:
swipeMadeAction()
layer.shouldRasterize = false
default:
layer.shouldRasterize = false
resetViewPositionAndTransformations()
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if let touchView = touch.view, let _ = touchView as? UIControl {
return false
}
return delegate?.card(cardShouldDrag: self) ?? true
}
@objc func tapRecognized(_ recogznier: UITapGestureRecognizer) {
delegate?.card(cardWasTapped: self)
}
//MARK: Private
private var directions: [SwipeResultDirection] {
return delegate?.card(cardAllowedDirections: self) ?? [.left, .right]
}
private var dragDirection: SwipeResultDirection? {
//find closest direction
let normalizedDragPoint = dragDistance.normalizedDistanceForSize(bounds.size)
return directions.reduce((distance:CGFloat.infinity, direction:nil)) { closest, direction in
let distance = direction.point.distanceTo(normalizedDragPoint)
if distance < closest.distance {
return (distance, direction)
}
return closest
}.direction
}
private var dragPercentage: CGFloat {
guard let dragDirection = dragDirection else { return 0 }
// normalize dragDistance then convert project closesest direction vector
let normalizedDragPoint = dragDistance.normalizedDistanceForSize(frame.size)
let swipePoint = normalizedDragPoint.scalarProjectionPointWith(dragDirection.point)
// rect to represent bounds of card in normalized coordinate system
let rect = SwipeResultDirection.boundsRect
// if point is outside rect, percentage of swipe in direction is over 100%
if !rect.contains(swipePoint) {
return 1.0
} else {
let centerDistance = swipePoint.distanceTo(.zero)
let targetLine = (swipePoint, CGPoint.zero)
// check 4 borders for intersection with line between touchpoint and center of card
// return smallest percentage of distance to edge point or 0
return rect.perimeterLines
.flatMap { CGPoint.intersectionBetweenLines(targetLine, line2: $0) }
.map { centerDistance / $0.distanceTo(.zero) }
.min() ?? 0
}
}
private func updateOverlayWithFinishPercent(_ percent: CGFloat, direction: SwipeResultDirection?) {
overlayView?.overlayState = direction
let progress = max(min(percent/swipePercentageMargin, 1.0), 0)
overlayView?.update(progress: progress)
}
private func swipeMadeAction() {
let shouldSwipe = { direction in
return self.delegate?.card(self, shouldSwipeIn: direction) ?? true
}
if let dragDirection = dragDirection , shouldSwipe(dragDirection) && dragPercentage >= swipePercentageMargin && directions.contains(dragDirection) {
swipeAction(dragDirection)
} else {
resetViewPositionAndTransformations()
}
}
private func animationPointForDirection(_ direction: SwipeResultDirection) -> CGPoint {
let point = direction.point
let animatePoint = CGPoint(x: point.x * 4, y: point.y * 4) //should be 2
let retPoint = animatePoint.screenPointForSize(screenSize)
return retPoint
}
private func animationRotationForDirection(_ direction: SwipeResultDirection) -> CGFloat {
return CGFloat(direction.bearing / 2.0 - Double.pi / 4)
}
private func swipeAction(_ direction: SwipeResultDirection) {
overlayView?.overlayState = direction
overlayView?.alpha = 1.0
delegate?.card(self, wasSwipedIn: direction)
let translationAnimation = POPBasicAnimation(propertyNamed: kPOPLayerTranslationXY)
translationAnimation?.duration = cardSwipeActionAnimationDuration
translationAnimation?.fromValue = NSValue(cgPoint: POPLayerGetTranslationXY(layer))
translationAnimation?.toValue = NSValue(cgPoint: animationPointForDirection(direction))
translationAnimation?.completionBlock = { _, _ in
self.removeFromSuperview()
}
layer.pop_add(translationAnimation, forKey: "swipeTranslationAnimation")
}
private func resetViewPositionAndTransformations() {
delegate?.card(cardWasReset: self)
removeAnimations()
let resetPositionAnimation = POPSpringAnimation(propertyNamed: kPOPLayerTranslationXY)
resetPositionAnimation?.fromValue = NSValue(cgPoint:POPLayerGetTranslationXY(layer))
resetPositionAnimation?.toValue = NSValue(cgPoint: CGPoint.zero)
resetPositionAnimation?.springBounciness = cardResetAnimationSpringBounciness
resetPositionAnimation?.springSpeed = cardResetAnimationSpringSpeed
resetPositionAnimation?.completionBlock = {
(_, _) in
self.layer.transform = CATransform3DIdentity
self.dragBegin = false
}
layer.pop_add(resetPositionAnimation, forKey: "resetPositionAnimation")
let resetRotationAnimation = POPBasicAnimation(propertyNamed: kPOPLayerRotation)
resetRotationAnimation?.fromValue = POPLayerGetRotationZ(layer)
resetRotationAnimation?.toValue = CGFloat(0.0)
resetRotationAnimation?.duration = cardResetAnimationDuration
layer.pop_add(resetRotationAnimation, forKey: "resetRotationAnimation")
let overlayAlphaAnimation = POPBasicAnimation(propertyNamed: kPOPViewAlpha)
overlayAlphaAnimation?.toValue = 0.0
overlayAlphaAnimation?.duration = cardResetAnimationDuration
overlayAlphaAnimation?.completionBlock = { _, _ in
self.overlayView?.alpha = 0
}
overlayView?.pop_add(overlayAlphaAnimation, forKey: "resetOverlayAnimation")
let resetScaleAnimation = POPBasicAnimation(propertyNamed: kPOPLayerScaleXY)
resetScaleAnimation?.toValue = NSValue(cgPoint: CGPoint(x: 1.0, y: 1.0))
resetScaleAnimation?.duration = cardResetAnimationDuration
layer.pop_add(resetScaleAnimation, forKey: "resetScaleAnimation")
}
//MARK: Public
func removeAnimations() {
pop_removeAllAnimations()
layer.pop_removeAllAnimations()
}
func swipe(_ direction: SwipeResultDirection) {
if !dragBegin {
delegate?.card(self, wasSwipedIn: direction)
let swipePositionAnimation = POPBasicAnimation(propertyNamed: kPOPLayerTranslationXY)
swipePositionAnimation?.fromValue = NSValue(cgPoint:POPLayerGetTranslationXY(layer))
swipePositionAnimation?.toValue = NSValue(cgPoint:animationPointForDirection(direction))
swipePositionAnimation?.duration = cardSwipeActionAnimationDuration
swipePositionAnimation?.completionBlock = {
(_, _) in
self.removeFromSuperview()
}
layer.pop_add(swipePositionAnimation, forKey: "swipePositionAnimation")
let swipeRotationAnimation = POPBasicAnimation(propertyNamed: kPOPLayerRotation)
swipeRotationAnimation?.fromValue = POPLayerGetRotationZ(layer)
swipeRotationAnimation?.toValue = CGFloat(animationRotationForDirection(direction))
swipeRotationAnimation?.duration = cardSwipeActionAnimationDuration
layer.pop_add(swipeRotationAnimation, forKey: "swipeRotationAnimation")
overlayView?.overlayState = direction
let overlayAlphaAnimation = POPBasicAnimation(propertyNamed: kPOPViewAlpha)
overlayAlphaAnimation?.toValue = 1.0
overlayAlphaAnimation?.duration = cardSwipeActionAnimationDuration
overlayView?.pop_add(overlayAlphaAnimation, forKey: "swipeOverlayAnimation")
}
}
}
//
// KolodaCardStorage.swift
// Pods
//
// Created by Eugene Andreyev on 3/30/16.
//
//
import Foundation
import UIKit
extension KolodaView {
func createCard(at index: Int, frame: CGRect? = nil) -> DraggableCardView {
let cardView = generateCard(frame ?? frameForTopCard())
configureCard(cardView, at: index)
return cardView
}
func generateCard(_ frame: CGRect) -> DraggableCardView {
let cardView = DraggableCardView(frame: frame)
cardView.delegate = self
return cardView
}
func configureCard(_ card: DraggableCardView, at index: Int) {
let contentView = dataSource!.koloda(self, viewForCardAt: index)
card.configure(contentView, overlayView: dataSource?.koloda(self, viewForCardOverlayAt: index))
//Reconfigure drag animation constants from Koloda instance.
if let rotationMax = self.rotationMax {
card.rotationMax = rotationMax
}
if let rotationAngle = self.rotationAngle {
card.rotationAngle = rotationAngle
}
if let scaleMin = self.scaleMin {
card.scaleMin = scaleMin
}
}
}
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment