-1

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

1 Answers1

1

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

  • constrain it to the top of the scroll view
  • add a pan gesture to the scroll view
  • if the user drags down, move the "refresh view" down
  • when it's fully visible, run your data refresh
  • animate it back away

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.

DonMag
  • 69,424
  • 5
  • 50
  • 86