Swift : Pull down to dismiss `UITableViewController`

25,234

Solution 1

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
}

Solution 2

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
}

Solution 3

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

Solution 4

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
    )
}
Share:
25,234
marrioa
Author by

marrioa

Updated on July 09, 2022

Comments

  • marrioa
    marrioa almost 2 years

    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)
                    }
     }
        }
    
  • marrioa
    marrioa over 8 years
    So I should delete it ? what do you suggest ?
  • beyowulf
    beyowulf over 8 years
    place print(-offsetY) in the scrollViewDidScroll before the if statement. build and run your app, pull down on your tableview, look at the values in the console. Do they ever become greater than 480.0?
  • marrioa
    marrioa over 8 years
    What is eligiblePanOffset ?
  • Eugene Dudnyk
    Eugene Dudnyk over 8 years
    That is a flag which is true when user pulled enough to the top. You have to compute it basing on panOffset and 0 - point of table view bounds. E.g. let eligiblePanOffset = (panOffset.y <= -300)
  • marrioa
    marrioa over 8 years
    346.666666666667 this is the result
  • marrioa
    marrioa over 8 years
    I don't know how can you an example
  • beyowulf
    beyowulf over 8 years
    So you need to rethink (-offsetY) > (tableHeaderHeight+adjustment) try (-offsetY)> 340.0
  • marrioa
    marrioa over 8 years
    Crashed After scrolling panRecognized:]: unrecognized selector sent to instance 0x7fa29b02a770
  • Eugene Dudnyk
    Eugene Dudnyk over 8 years
    maybe because it's private. Set to public. Let's move to chat conversation.
  • Eugene Dudnyk
    Eugene Dudnyk over 8 years
  • Tim Fuqua
    Tim Fuqua over 5 years
    Pretty much interchangeable with UIScrollView as well. This solution is much simpler than others I've seen.
  • Thein
    Thein almost 5 years
    yea, can use with UIScrollView at the same time :) Thanks
  • Muhammad Shauket
    Muhammad Shauket over 4 years
    After doing this tableview doesn’t scroll anymore
  • Eugene Dudnyk
    Eugene Dudnyk over 4 years
    Check if the method gestureRecognizer (shouldRecognizeSimultaneouslyWithGestureRecognizer: otherGestureRecognizer:) is being called for you. I put it there to ensure that the effect that you described doesn't happen.
  • Muhammad Shauket
    Muhammad Shauket over 4 years
    it disable tableview scrolling
  • latenitecoder
    latenitecoder over 3 years
    Cleanest solution on here. Worked out of the box.
  • DawnSong
    DawnSong over 3 years
    If you want to scroll up, you need to drag down a little then drag up.
  • goodliving
    goodliving over 2 years
    Great answer, thank you for it! To improve it, don't implement scrollViewDidScroll but add scrollView.isScrollEnabled = false to case .began and true to default in handlePanFromTop.
  • Leon Jakonda
    Leon Jakonda almost 2 years
    best solution, thx