Search code examples
swiftuiscrollviewvisual-glitchpage-jump

Using UIScrollView with a minimum content top anchor causes visual glitch


I have a scroll view in which I have a content view. I set the scroll view's top anchor to be just above the bottom of an image. I set the content view's top anchor to actually be at the bottom of the image. That way you can pull down on the content and reveal up to the bottom of the image without being able to pull the content view down any further. However, this is causing the content to jump.

Here is my code:

class HomeParallaxScrollViewController: UIViewController {

    private let topImageView = UIImageView(image: UIImage(named: "cat"))
    private let contentView = UIView()
    private let scrollView = UIScrollView()
    private let label = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .gray

        topImageView.contentMode = .scaleAspectFill
        contentView.backgroundColor = .white
        label.text = "SOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT"
        label.textColor = .black
        label.numberOfLines = 0

        [contentView, label, topImageView, scrollView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }

        scrollView.addSubview(contentView)
        contentView.addSubview(label)
        view.addSubview(topImageView)
        view.addSubview(scrollView)

        NSLayoutConstraint.activate([
            topImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
            topImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            topImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            topImageView.heightAnchor.constraint(equalToConstant: 200),

            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
            scrollView.topAnchor.constraint(equalTo: topImageView.bottomAnchor, constant: -30),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

            contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
            contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            contentView.topAnchor.constraint(lessThanOrEqualTo: topImageView.bottomAnchor), //This is what's causing the glitch

            label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            label.topAnchor.constraint(equalTo: contentView.topAnchor),
            label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
    }
}

And here is that is happening: enter image description here


Solution

  • Trying to add another top constraint -- particularly to an element outside the scroll view -- is a bad idea, and, as you see, won't work. I'm sure you noticed auto-layout conflict messages being generated.

    One approach is to implement scrollViewDidScroll delegate func:

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // limit drag-down in the scroll view to the overlap size
        scrollView.contentOffset.y = max(scrollView.contentOffset.y, -30)
    }
    

    As the user drags-down to scroll, it will stop at 30-points.

    Here is your example, with slight modifications -- I don't have your .plBackgroundLightGray or .PLSemiboldFont and I added an image load for the top image view -- but this should run as-is:

    // conform to UIScrollViewDelegate
    class HomeParallaxScrollViewController: UIViewController, UIScrollViewDelegate {
    
        private let topImageView = UIImageView(image: UIImage(named: "cat"))
        private let contentView = UIView()
        private let scrollView = UIScrollView()
        private let label = UILabel()
    
        // this will be the "overlap" of the scroll view and top image view
        private var scrollOverlap: CGFloat = 30.0
    
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            // limit drag-down in the scroll view to scrollOverlap points
            scrollView.contentOffset.y = max(scrollView.contentOffset.y, -scrollOverlap)
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .lightGray // .plBackgroundLightGray
    
            topImageView.contentMode = .scaleAspectFill
            if let img = UIImage(named: "background") {
                topImageView.image = img
            }
            contentView.backgroundColor = .white
            label.text = "SOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT"
            label.font = UIFont.boldSystemFont(ofSize: 16) // .PLSemiboldFont(size: 16)
            label.textColor = .black
            label.numberOfLines = 0
    
            [contentView, label, topImageView, scrollView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
    
            scrollView.addSubview(contentView)
            contentView.addSubview(label)
            view.addSubview(topImageView)
            view.addSubview(scrollView)
    
            NSLayoutConstraint.activate([
                topImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
                topImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                topImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                topImageView.heightAnchor.constraint(equalToConstant: 200),
    
                scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
                scrollView.topAnchor.constraint(equalTo: topImageView.bottomAnchor, constant: scrollOverlap),
                scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
    
                contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
                contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
                contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
                contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
    
                // nope, not a good idea -- will cause constraint conflicts
                //contentView.topAnchor.constraint(lessThanOrEqualTo: topImageView.bottomAnchor), //This is what's causing the glitch
    
                label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
                label.topAnchor.constraint(equalTo: contentView.topAnchor),
                label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
            ])
    
            // set delegate to self
            scrollView.delegate = self
        }
    
    }