iosuiscrollviewscrollviewpull-to-refresh

Add Pull to Refresh in Scrollview without bounce enabled


How we can configure pull to refresh without bounce enabled in scroll view.

its simple when we keep bounce enable we just need to assign refresh control to scroll view but I don't want to enable bounce

Any suggestions would be appreciated. thanks in advance

Have tried scroll view did scroll method but it won't call as there might be case when scroll view does not have enough data to scroll the page


Solution

  • One approach is to create your own "refresh view" and:

    Here's a quick example:

    class RefreshVC: UIViewController {
        
        let scrollView: UIScrollView = UIScrollView()
        let contentView: UIView = UIView()
        let contentLabel: UILabel = UILabel()
        
        let myRefreshView: UIView = UIView()
        let activityView: UIActivityIndicatorView = UIActivityIndicatorView()
        
        var cBottom: NSLayoutConstraint!
        
        var myData: [String] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            [contentLabel, contentView, scrollView, myRefreshView, activityView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
            }
            
            myRefreshView.addSubview(activityView)
            contentView.addSubview(contentLabel)
            scrollView.addSubview(contentView)
            scrollView.addSubview(myRefreshView)
            
            view.addSubview(scrollView)
            
            let g = view.safeAreaLayoutGuide
            let cg = scrollView.contentLayoutGuide
            let fg = scrollView.frameLayoutGuide
            
            NSLayoutConstraint.activate([
                
                scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
                scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
                scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
                scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -80.0),
    
                contentView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
                contentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
                contentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
                contentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
                
                contentView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
                
                contentLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8.0),
                contentLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
                contentLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
                contentLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8.0),
                
                activityView.centerXAnchor.constraint(equalTo: myRefreshView.centerXAnchor),
                activityView.centerYAnchor.constraint(equalTo: myRefreshView.centerYAnchor),
                
                myRefreshView.widthAnchor.constraint(equalToConstant: 200.0),
                myRefreshView.heightAnchor.constraint(equalToConstant: 100.0),
                myRefreshView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
    
            ])
    
            cBottom = myRefreshView.bottomAnchor.constraint(equalTo: fg.topAnchor)
            cBottom.isActive = true
    
            myRefreshView.backgroundColor = .white.withAlphaComponent(0.90)
            myRefreshView.layer.cornerRadius = 12
            myRefreshView.layer.borderColor = UIColor.black.cgColor
            myRefreshView.layer.borderWidth = 1
            
            //activityView.style = .large
            activityView.color = .red
            activityView.startAnimating()
            
            scrollView.backgroundColor = .systemBlue
            contentView.backgroundColor = .systemYellow
            contentLabel.backgroundColor = .cyan
            
            contentLabel.numberOfLines = 0
            contentLabel.textAlignment = .center
            contentLabel.font = .systemFont(ofSize: 40.0, weight: .bold)
            
            // let's start with 5 lines of text as our content
            myData = (1...5).compactMap({ "Line \($0)" })
            
            contentLabel.setContentCompressionResistancePriority(.required, for: .vertical)
            contentLabel.text = myData.joined(separator: "\n")
            
            scrollView.bounces = false
            
            let pg = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
            scrollView.addGestureRecognizer(pg)
            pg.delegate = self
        }
        
        var startPT: CGPoint = .zero
        var isRefreshing: Bool = false
        
        @objc func handlePan(_ pan: UIPanGestureRecognizer) {
            
            guard let sv = pan.view as? UIScrollView,
                  isRefreshing == false
            else { return }
            
            let curPT = pan.location(in: view)
            
            switch pan.state {
            case .began:
                // we only want to "pull down" the refresh view if
                //  we start dragging when the scroll view is all the
                //  way at the top
                if sv.contentOffset.y == 0 {
                    startPT = curPT
                } else {
                    startPT.y = .greatestFiniteMagnitude
                }
                
            case .changed:
                let diff = curPT.y - startPT.y
                // if we are dragging down
                if diff > 0 {
                    // if the scroll view content is at the top
                    if sv.contentOffset.y == 0 {
                        scrollView.isScrollEnabled = false
                        // move the refresh view down
                        cBottom.constant = min(diff, myRefreshView.frame.height + 4.0)
                        // if the refresh view is fully down
                        if cBottom.constant == myRefreshView.frame.height + 4.0 {
                            isRefreshing = true
                            refreshContent()
                        }
                    }
                }
                
            default:
                // if the refresh view has not been pulled all the way
                //  when drag ended / was cancelled
                // animate it back up
                print("done", cBottom.constant)
                self.scrollView.isScrollEnabled = true
                if cBottom.constant > 0.0, cBottom.constant < myRefreshView.frame.height + 4.0 {
                    cBottom.constant = 0
                    UIView.animate(withDuration: 0.3, animations: {
                        self.view.layoutIfNeeded()
                    })
                }
    
            }
            
        }
        
        @objc func refreshContent() {
            
            // let's simulate a 1-second refresh task
            //  and add a line to the scroll view content
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
                self.myData.append("Line \(self.myData.count + 1)")
                self.contentLabel.text = self.myData.joined(separator: "\n")
                // animate the refresh view back up
                DispatchQueue.main.async {
                    self.cBottom.constant = 0
                    UIView.animate(withDuration: 0.3, animations: {
                        self.view.layoutIfNeeded()
                    }, completion: {_ in
                        self.isRefreshing = false
                        self.scrollView.isScrollEnabled = true
                    })
                }
            })
            
        }
        
    }
    
    extension RefreshVC: UIGestureRecognizerDelegate {
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    }
    

    and it looks like this when running:

    enter image description here

    Once you've added enough lines, the scroll view will scroll... and the "refresh view" will only get pulled-down if the scroll view is scrolled all the way to the top.

    Note: this is EXAMPLE CODE ONLY!!!

    It is just to give you a start. You would likely want to tweak the distances, interactive capabilities, etc.