How to detect that multiple UIGestureRecognizers end? - ios

in my view I have multiple gesture recognizers (UIPanGestureRecognizer, UIPinchGestureRecognizer and UIRotationGestureRecognizer) and I allowed them to detect touches simultaneously:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.view != otherGestureRecognizer.view {
return false
}
return true
}
The problem with this approach is that I am no longer able to detect when all the 3 gestures end. I cannot use
if gesture.state == .ended || gesture.state == .cancelled {
}
bacause this is valid only for a gesture recognizer and not for the all 3.
Any idea if there an api to detect when all active recognizers end?
Thanks
Solution
This works but it is very ugly: basically I keep track when all three gestures recognizers end and prevent to detect the end multiple times as the recognizers callback can be called in any order:
class SCCanvasViewController: UIViewController {
var gesturesAlreadyEnded = false
lazy var panGestureRecognizer: UIPanGestureRecognizer = {
let gr = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
gr.delegate = self
return gr
}()
lazy var pinchGestureRecognizer: UIPinchGestureRecognizer = {
let gr = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
gr.delegate = self
return gr
}()
lazy var rotateGestureRecognizer: UIRotationGestureRecognizer = {
let gr = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:)))
gr.delegate = self
return gr
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(panGestureRecognizer)
view.addGestureRecognizer(pinchGestureRecognizer)
view.addGestureRecognizer(rotateGestureRecognizer)
}
func isGestureEnded(gesture: UIGestureRecognizer) -> Bool {
return gesture.state == .ended || gesture.state == .cancelled || gesture.state == .failed
}
func allGesturesEnded() -> Bool {
let panEnded = isGestureEnded(gesture: panGestureRecognizer)
let pinchEnded = isGestureEnded(gesture: pinchGestureRecognizer)
let rotationEnded = isGestureEnded(gesture: rotateGestureRecognizer)
return panEnded && pinchEnded && rotationEnded
}
#objc func handlePan(_ gesture: UIPanGestureRecognizer) {
if gesture.state == .began {
gesturesAlreadyEnded = false
}
if !gesturesAlreadyEnded && isGestureEnded(gesture: gesture) {
canvasView.showHorizontalSnapIndicators(areVisible: false)
canvasView.showVerticalSnapIndicators(areVisible: false)
if (allGesturesEnded()) {
gesturesAlreadyEnded = true
print("Can create transformation command")
}
return
}
}
#objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
if gesture.state == .began {
gesturesAlreadyEnded = false
}
if !gesturesAlreadyEnded && isGestureEnded(gesture: gesture) {
if (allGesturesEnded()) {
gesturesAlreadyEnded = true
print("Can create transformation command")
}
return
}
}
#objc func handleRotation(_ gesture: UIRotationGestureRecognizer) {
if gesture.state == .began {
gesturesAlreadyEnded = false
}
if !gesturesAlreadyEnded && isGestureEnded(gesture: gesture) {
if (allGesturesEnded()) {
gesturesAlreadyEnded = true
print("Can create transformation command")
}
return
}
}
}
extension SCCanvasViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.view != otherGestureRecognizer.view {
return false
}
return true
}
}

Set it up to get multi-touch notifications, then examine the set of touches impacted by the event.
class TouchableView: UIView {
var touchViews = [UITouch:TouchSpotView]()
override init(frame: CGRect) {
super.init(frame: frame)
isMultipleTouchEnabled = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isMultipleTouchEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
createViewForTouch(touch: touch)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let view = viewForTouch(touch: touch)
// Move the view to the new location.
let newLocation = touch.location(in: self)
view?.center = newLocation
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
// Other methods. . .
}

Related

Programmatically made buttons conflict with viewController’s gesture recognizers

Referring to my last question:
Spritekit: passing from UIButtons to buttons as SKSpriteNode
I’m working on a SpriteKit game: the user can tap and swipe on the screen to move sprites inside the scene and I added gesture recognizers in my ViewController for that.
Then I create a HUD to keep 4 buttons programmatically made with which the user could add other sprites to the scene.
I want my buttons fade and scale a little when pressed and then turn back to the original state, but it seems that they conflict with viewController’s gesture recognizers: buttons fade and scale down, but they stay in that state, don’t go back to normal state.
What can I do?
This is the Button class:
import SpriteKit
protocol ButtonDelegate: NSObjectProtocol {
func buttonClicked(sender: Button)
}
class Button: SKSpriteNode {
weak var delegate: ButtonDelegate!
var buttonTexture = SKTexture()
init(name: String) {
buttonTexture = SKTexture(imageNamed: name)
super.init(texture: buttonTexture, color: .clear, size: buttonTexture.size())
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var touchEndedCallback: (() -> Void)?
weak var currentTouch: UITouch?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if isUserInteractionEnabled {
setScale(0.9)
self.alpha = 0.5
if let currentTouch = touches.first {
let touchLocation = currentTouch.location(in: self)
for node in self.nodes(at: touchLocation) {
delegate?.buttonClicked(sender: self)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
setScale(1.0)
self.alpha = 1.0
touchEndedCallback?()
print("tapped!")
}
}
This is the code I used in View Controller:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let skView = view as! SKView
skView.isMultipleTouchEnabled = false
skView.presentScene(scene)
#IBAction func didTap(_ sender: UITapGestureRecognizer) {
game.myCode
}
#IBAction func didPan(_ sender: UIPanGestureRecognizer) {
let currentPoint = sender.translation(in: self.view)
if let originalPoint = panPointReference {
if abs(currentPoint.x - originalPoint.x) > (SquareSize * 0.9) {
if sender.velocity(in: self.view).x > CGFloat(0) {
//game.myCode
panPointReference = currentPoint
} else {
//game.myCode
panPointReference = currentPoint
}
}
} else if sender.state == .began {
panPointReference = currentPoint
}
}
#IBAction func didSwipe(_ sender: UISwipeGestureRecognizer) {
//game.myCode
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive shouldReceiveTouch: UITouch) -> Bool {
if UITouch .isKind(of: Button.self) {
return false
}
return true
}
func buttonClicked(sender: Button) {
//myCode
}
}
SOLVED IT!
Ok, it was simpler than I think.
(I took a break).
- I deleted UIgestures and added them programmatically;
- in my GameScene’s touchesEnded method I created a conditional to check which one of my node has been touched.
- in View Controller I add the gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) method, to avoid conflicts with gesture recognizers attached to different view (my nodes are in two different views):
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.view != otherGestureRecognizer.view {
return false
}
return true
}
I hope this will help…
I don't think they are interfering with the inbuilt gesture recognizers of the View Controller as you are not adding any gestures of your own, you are just overriding touchesBegin and touchesEnded.
Does "tapped" get printed out?
It is possible that touchesEnd() is not getting called, try implementing touchesCancelled() as well and see if that gets called.

Instance member cannot be used

I have this splash view and I'm having problems with use3dtouch because I'm having an error telling me that splashview has no instance member 'use3dtouch'. Here is the code.
Here is an image of the error
The error
import Foundation
import UIKit
class VPSplashView : UIView {
var vp = VPSplashView()
private lazy var __once: () = {
if VPSplashView.traitCollection.forceTouchCapability == UIForceTouchCapability.unavailable
{
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(VPSplashView.longPressed(_:)))
self.addGestureRecognizer(longPressRecognizer)
VPSplashView.use3DTouch = false
} else {
VPSplashView.use3DTouch = true
}
}()
static func addSplashTo(_ view : UIView, menuDelegate: MenuDelegate) -> VPSplashView{
let splashView = VPSplashView(view: view)
splashView.backgroundColor = UIColor.clear
splashView.isExclusiveTouch = true
if (view.isKind(of: UIScrollView.classForCoder())){
(view as! UIScrollView).canCancelContentTouches = false
}
splashView.menu?.delegate = menuDelegate
return splashView
}
// MARK: Initialization
var menu : VPSplashMenu?
fileprivate var use3DTouch : Bool = true
var onceToken: Int = 0
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
init(view: UIView){
super.init(frame:view.bounds)
view.addSubview(self)
self.menu = VPSplashMenu.init(center: self.center)
}
func setDataSource(_ source: MenuDataSource!){
self.menu?.dataSource = source
}
override func layoutSubviews() {
super.layoutSubviews()
self.superview?.bringSubview(toFront: self)
if (self.superview != nil){
self.setup()
}
}
fileprivate func setup(){
_ = self.__once;
}
// MARK: Long Press Handling
func longPressed(_ sender: UILongPressGestureRecognizer)
{
switch sender.state {
case .began:
let centerPoint = sender.location(in: self)
menu?.movedTo(centerPoint)
menu?.showAt(self)
menu?.squash()
case .ended:
menu?.cancelTap()
menu?.removeFromSuperview()
case .changed:
let centerPoint = sender.location(in: self)
menu?.handleTap((menu?.convert(centerPoint, from: self))!)
default:
menu?.removeFromSuperview()
}
}
// MARK: Touch Handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if (use3DTouch == true){
var centerPoint : CGPoint = CGPoint.zero
for touch in touches {
centerPoint = touch.location(in: self)
menu?.movedTo(centerPoint)
menu?.showAt(self)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if (use3DTouch == true){
for touch in touches {
let centerPoint = touch.location(in: self)
if (menu?.shown == false){
menu?.movedTo(centerPoint)
if (touch.force > minimalForceToSquash){
menu?.squash()
}
} else {
menu?.handleTap((menu?.convert(centerPoint, from: self))!)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if (use3DTouch == true){
menu?.hide()
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if (use3DTouch){
menu?.hide()
}
}
}
Your added code has clarified some important things to write an answer...
traitCollection is an instance property which is declared in UITraitEnvironment, where UIView conforms to
use3DTouch is an instance property which is declared in VPSplashView explicitly
longPressed(_:) is an instance method which is declared in VPSplashView explicitly
The third is not critical in this case, but the first two are clearly related to the error message you have gotten: Instance member cannot be used.
When you access to instance members (in this case, instance properties), you need to prefix an instance reference before .memberName, not a class name.
In your case, self seems to be an appropriate instance:
private lazy var __once: () = {
//###
if self.traitCollection.forceTouchCapability == UIForceTouchCapability.unavailable
{
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressed(_:)))
self.addGestureRecognizer(longPressRecognizer)
//###
self.use3DTouch = false
} else {
//###
self.use3DTouch = true
}
}()
But you are doing things in far more complex way than needed, so you may need some more fixes.
One critical thing is this line:
var vp = VPSplashView()
This may cause some runtime issue (even if you have solved some compile-time issue) and vp is not used. Better remove it.

How to detect the location of my finger through in a scrollview or webview in Swift? (using touchesMoved if possible)

I want to print the location of the user's finger on a webview.
Using this (below) doesn't work on a webview
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self.view) // even when i replace this part with UIWebview
print(position.x)
print(position.y)
}
}
I think that creating a UIPanGestureRecognizer should solve it, add the following code into viewDidLoad() method:
override func viewDidLoad() {
// ...
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(webViewtouchMoved(panGesture:)))
panGesture.delegate = self
webView.addGestureRecognizer(panGesture)
// ...
}
webViewtouchMoved(panGesture:) method:
func webViewtouchMoved(panGesture: UIPanGestureRecognizer) {
if panGesture.state == .began || panGesture.state == .changed {
let position = panGesture.location(in: view)
print(position.x)
print(position.y)
}
}
also, you should add this extension for the desired ViewController:
// change 'ViewController' to your class name:
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
hope this helped.

Touch up and Touch down action for UIImageView

what i want to achieve is when user touch on UIImageView set Image1, when user lifts his finger set Image2.
i can only get UIGestureRecognizerState.Ended with this code
var tap = UITapGestureRecognizer(target: self, action: Selector("tappedMe:"))
imageView.addGestureRecognizer(tap)
imageView.userInteractionEnabled = true
func tappedMe(gesture: UITapGestureRecognizer)
{
if gesture.state == UIGestureRecognizerState.Began{
imageView.image=originalImage
dbgView.text="Began"
}else if gesture.state == UIGestureRecognizerState.Ended{
imageView.image=filteredImage
dbgView.text="Ended"
}else if gesture.state == UIGestureRecognizerState.Cancelled{
imageView.image=filteredImage
dbgView.text="Cancelled"
}else if gesture.state == UIGestureRecognizerState.Changed{
imageView.image=filteredImage
dbgView.text="Changed"
}
}
The UITapGestureRecognizer doesn't change it's state to .Began, but the UILongPressGestureRecognizer does. If you for some reason font want to override the touch callbacks directly you could use a UILongPressGestureRecognizer with a very short minimumPressDuration of like 0.1 to achieve the effect.
Example by #Daij-Djan:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var tap = UILongPressGestureRecognizer(target: self, action: Selector("pressedMe:"))
tap.minimumPressDuration = 0
self.view.addGestureRecognizer(tap)
self.view.userInteractionEnabled = true
}
func pressedMe(gesture: UITapGestureRecognizer) {
if gesture.state == .Began{
self.view.backgroundColor = UIColor.blackColor()
} else if gesture.state == .Ended {
self.view.backgroundColor = UIColor.whiteColor()
}
}
}
Here is the solution:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if touch.view == self {
//began
}
}
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if touch.view == self {
//end
}
}
super.touchesEnded(touches, with: event)
}
Note: Put this inside a UIView SubClass and add: userInteractionEnabled = true inside the init block
The best and most practical solution would be to embed your UIImageView within a UIControl subclass.
By default UIImageView has user interaction enabled. If you create a simple UIControl subclass you can easily add your image view into it and then use the following methods to achieve what you want:
let control = CustomImageControl()
control.addTarget(self, action: "imageTouchedDown:", forControlEvents: .TouchDown)
control.addTarget(self, action: "imageTouchedUp:", forControlEvents: [ .TouchUpInside, .TouchUpOutside ])
The advantages of doing it this way is that you get access to all of the different touch events saving you time from having to detect them yourself.
Depending on what you want to do, you could also override var highlighted: Bool or var selected: Bool to detect when the user is interacting with the image.
It's better to do it this way so that your user has a consistent user experience with all the controls in their app.
A simple implementation would look something like this:
final class CustomImageControl: UIControl {
let imageView: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
// setup our image view
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
imageView.leadingAnchor.constraintEqualToAnchor(leadingAnchor).active = true
imageView.trailingAnchor.constraintEqualToAnchor(trailingAnchor).active = true
imageView.topAnchor.constraintEqualToAnchor(topAnchor).active = true
imageView.bottomAnchor.constraintEqualToAnchor(bottomAnchor).active = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let control = CustomImageControl()
control.translatesAutoresizingMaskIntoConstraints = false
control.imageView.image = ... // set your image here.
control.addTarget(self, action: "imageTouchedDown:", forControlEvents: .TouchDown)
control.addTarget(self, action: "imageTouchedUp:", forControlEvents: [ .TouchUpInside, .TouchUpOutside ])
view.addSubview(control)
control.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor).active = true
control.centerYAnchor.constraintEqualToAnchor(view.centerYAnchor).active = true
}
#objc func imageTouchedDown(control: CustomImageControl) {
// pressed down on the image
}
#objc func imageTouchedUp(control: CustomImageControl) {
// released their finger off the image
}
}

Swift : Pull down to dismiss `UITableViewController`

I want to pull down to dismiss UITableViewController so I used scrollViewDidScroll method but it didn't works!
class CommentViewController: PFQueryTableViewController {
private let tableHeaderHeight: CGFloat = 350.0
extension CommentViewController
{
override func scrollViewDidScroll(scrollView: UIScrollView)
{
// Pull down to dismiss TVC
let offsetY = scrollView.contentOffset.y
let adjustment: CGFloat = 130.0
// for later use
if (-offsetY) > (tableHeaderHeight+adjustment) {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
}
Swift 4
var panGestureRecognizer : UIPanGestureRecognizer!
override func viewDidLoad() {
mainTableView.bounces = true
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panRecognized))
panGestureRecognizer.delegate = self
mainTableView.addGestureRecognizer(panGestureRecognizer)
}
#objc func panRecognized(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began && mainTableView.contentOffset.y == 0 {
} else if recognizer.state != .ended && recognizer.state != .cancelled && recognizer.state != .failed {
let panOffset = recognizer.translation(in: mainTableView)
let eligiblePanOffset = panOffset.y > 300
if eligiblePanOffset {
recognizer.isEnabled = false
recognizer.isEnabled = true
self.dismiss(animated: true, completion: nil)
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
You have to implement additional pan gesture recognizer which will recognize simultaneously with scrollView's pan gesture recognizer. Then you can determine whether user is panning by his finger when table view is already scrolled to the top.
e.g.
var isTrackingPanLocation = false
var panGestureRecognizer: UIPanGestureRecognizer!
public override func viewDidLoad() {
super.viewDidLoad()
tableView.bounces = false
panGestureRecognizer = UIPanGestureRecognizer(target: self,
action: #selector(panRecognized(gestureRecognizer:)))
panGestureRecognizer.delegate = self
tableView.addGestureRecognizer(panGestureRecognizer)
}
public func panRecognized(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began && tableView.contentOffset.y == 0 {
recognizer.setTranslation(CGPoint.zero, inView : tableView)
isTrackingPanLocation = true
} else if recognizer.state != .ended &&
recognizer.state != .cancelled &&
recognizer.state != .failed &&
isTrackingPanLocation {
let panOffset = recognizer.translationInView(tableView)
// determine offset of the pan from the start here.
// When offset is far enough from table view top edge -
// dismiss your view controller. Additionally you can
// determine if pan goes in the wrong direction and
// then reset flag isTrackingPanLocation to false
let eligiblePanOffset = panOffset.y > 200
if eligiblePanOffset {
recognizer.enabled = false
recognizer.enabled = true
dismissViewControllerAnimated(true, completion: nil)
}
if panOffset.y < 0 {
isTrackingPanLocation = false
}
} else {
isTrackingPanLocation = false
}
}
public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWithGestureRecognizer
otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Why don't you place print(offsetY) in scrollViewDidScroll. I suspect that (-offsetY) > (tableHeaderHeight+adjustment) will never be satisfied because of the rubber banding will cause the tableview to rebound before it can dismiss the view controller
For people looking at this in 2019 -- A more modern approach would use the UIGestureRecognizerDelegate methods, instead of keeping extra state in your view controller. For example:
private weak var panFromTop: UIPanGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
// Add pan gesture recognizer
let panFromTop = UIPanGestureRecognizer(target: self, action: #selector(handlePanFromTop(_:)))
panFromTop.delegate = self
tableView.addGestureRecognizer(panFromTop)
self.panFromTop = panFromTop
}
#objc func handlePanFromTop(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
// TODO: BEGIN YOUR ANIMATION HERE
case .changed:
// TODO: UPDATE YOUR ANIMATION HERE
default:
let translation = recognizer.translation(in: view)
let velocity = recognizer.velocity(in: view)
if ((translation.y + velocity.y) / view.bounds.height) > 0.5 {
// TODO: FINISH YOUR ANIMATION HERE
} else {
// TODO: CANCEL YOUR ANIMATION HERE
}
}
}
Disable bounce at the top of the table view only:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
scrollView.setContentOffset(.zero, animated: false)
}
}
Then implement the gesture recognizer delegate methods:
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
return true
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let recognizer = gestureRecognizer as? UIPanGestureRecognizer,
recognizer === panFromTop else {
// Only require special conditions for the panFromTop gesture recognizer
return true
}
// Require the scroll view to be at the top,
// and require the pan to start by dragging downward
return (
tableView.contentOffset.y <= 0 &&
recognizer.velocity(in: view).y > 0
)
}

Resources