Search code examples
swiftanimationuikitscrollviewparallax

Problem with parallax effect animation for imageViews in Swift


I am trying to create a parallax effect animation for pictures in a scroll view. The main idea is to turn constraints on and off to achieve this.

Generally everything works, but after scrolling, sometimes you can see a thin line is part of the next image or or the animation becomes very sharp, and I really do not understand why this happens.

Maybe someone will tell me a better way to achieve parallax effect animation? I will be very grateful for your help.

I left the code on GitHub, you can run it and read my comments: https://github.com/swiloper/ConstraintsProblem

And also watch a small demonstration of what I got:

enter image description here

And this thin line on the side when scrolling:

enter image description here


Solution

  • Downloaded your GitHub project...

    You are getting the "thin line is part of the next image" because this part of your code in scrollViewDidScroll:

    if -(scrollView.currentVerticalOffset + UIApplication.shared.topSafeAreaInset) > 0 {
        // change some framing / constraint values...
    } else {
        // if user do not drag to dawn I disable constraints
        disableUniversalImageViewConstraint(imageView: placeImageScrollView.currentImageView)
    }
    

    Doesn't reset the values in the else block. So, your original 0 values "get stuck" at > 0 (usually ends up being 0.5).


    Not sure what you mean by "the animation becomes very sharp" ... although, the imageViews can get misplaced due to the way you're adding / removing constraints.

    I would suggest not mixing / matching explicit frames and constraints. Take a look at this approach:

    class ViewController: UIViewController {
    
        // MARK: Properties
        
        lazy var place: Place = Place(imageNames: ["firstLakeLemuriaImage", "secondLakeLemuriaImage", "thirdLakeLemuriaImage"])
    
        // scrollView to display all content
        private lazy var scrollView: UIScrollView = {
            let scrollView = UIScrollView(frame: screenBounds)
            scrollView.contentSize = CGSize(width: screenWidth, height: placeDescriptionContentView.frame.height + screenWidth)
            scrollView.frame.size.height -= UIApplication.shared.bottomSafeAreaInset
            scrollView.backgroundColor = .white
            scrollView.delegate = self
            scrollView.tag = 1
            return scrollView
        }()
        
        // scrollView to display place images
        private lazy var placeImageScrollView: ImageScrollView! = {
            let scrollView = ImageScrollView(frame: CGRect(x: 0, y: -UIApplication.shared.topSafeAreaInset, width: screenWidth, height: screenWidth), place: place)
            scrollView.delegate = self
            scrollView.tag = 2
            // allow subviews to show beyond scroll view's frame
            //  so we can "stretch" them for the parallax effect
            scrollView.clipsToBounds = false
            return scrollView
        }()
        
        private lazy var placeImagePageControl: UIPageControl! = {
            let pageControlWidth = 160.0
            let pageControlHeight = 36.0
            let leftPageControlSpacing = 58.0
            let pageControl = UIPageControl(frame: CGRect(x: screenWidth - pageControlWidth - sideSpacing + leftPageControlSpacing, y: sideSpacing, width: pageControlWidth, height: pageControlHeight))
            pageControl.addTarget(self, action: #selector(pageDidChange(_:)), for: .valueChanged)
            pageControl.numberOfPages = 3
            return pageControl
        }()
        
        private lazy var placeTitleView: UIView = {
            let titleLabel = UILabel(frame: CGRect(x: sideSpacing, y: 0.0, width: screenWidth - sideSpacing * 2, height: 60.0))
            titleLabel.font = UIFont.systemFont(ofSize: 31.0, weight: .bold)
            titleLabel.text = "Place Title"
            titleLabel.textColor = .white
            titleLabel.adjustsFontSizeToFitWidth = true
            
            let shadowView = UIView(frame: CGRect(x: 0.0, y: screenWidth - 60.0 - UIApplication.shared.topSafeAreaInset, width: screenWidth, height: 60.0))
            shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 10.0).cgPath
            shadowView.layer.shadowColor = UIColor.black.cgColor
            shadowView.layer.shadowOffset = CGSize.zero
            shadowView.layer.shadowOpacity = 0.3
            shadowView.layer.shadowRadius = 35.0
            shadowView.clipsToBounds = false
            shadowView.addSubview(titleLabel)
            
            return shadowView
        }()
        
        // contentView with description about place
        private lazy var placeDescriptionContentView: UIView = {
            let view = UIView(frame: CGRect(x: 0.0, y: placeImageScrollView.frame.maxY, width: screenWidth, height: 1000.0))
            view.backgroundColor = .white
            return view
        }()
        
        // indicates that image constraints is enable
        private var isConstraintEnable = false
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .white
            addSubviews()
            setupConstraints()
        }
        
        override var preferredStatusBarStyle: UIStatusBarStyle {
            return .lightContent
        }
        
        // MARK: Methods
        
        private func addSubviews() {
            view.addSubview(scrollView)
            scrollView.addSubview(placeImageScrollView)
            view.addSubview(placeImagePageControl)
            scrollView.addSubview(placeTitleView)
            scrollView.addSubview(placeDescriptionContentView)
        }
        
        private func setupConstraints() {
    
            placeImageScrollView.translatesAutoresizingMaskIntoConstraints = false
            placeImageScrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
            placeImageScrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
            placeImageScrollView.heightAnchor.constraint(equalToConstant: screenWidth).isActive = true
            placeImageScrollView.widthAnchor.constraint(equalTo: placeImageScrollView.heightAnchor).isActive = true
            
        }
        
        // MARK: Objc methods
        
        @objc private func pageDidChange(_ sender: UIPageControl) {
            placeImageScrollView.setContentOffset(CGPoint(x: CGFloat(sender.currentPage) * screenWidth, y: 0), animated: true)
            placeImagePageControl.currentPage = sender.currentPage
            // change current imageView
            placeImageScrollView.currentImageView = placeImageScrollView.imageViewsArray[placeImagePageControl.currentPage]
        }
    }
    
    // MARK: UIScrollViewDelegate
    
    extension ViewController: UIScrollViewDelegate {
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if scrollView.tag == 1 {
                // if user the user is dragging the page down
                if -(scrollView.currentVerticalOffset + UIApplication.shared.topSafeAreaInset) > 0 {
                    
                    // calculate the *original* imageView frame
                    let n = CGFloat(placeImagePageControl.currentPage)
                    var r = CGRect(x: n * screenWidth, y: 0.0, width: screenWidth, height: screenWidth)
    
                    // we want the bottom of the imageView to "stick" to the top of the placeDescriptionContentView
                    //  convert placeDescriptionContentView.frame to view coordinate space
                    let ff = scrollView.convert(placeDescriptionContentView.frame, to: view)
                    
                    // we want to change the *original* imageView size by 1/2 of the difference
                    let v = (screenWidth - ff.origin.y) * 0.5
                    r = r.insetBy(dx: v, dy: v)
                    // move it back to the top
                    r.origin.y = 0
                    // set the new frame
                    placeImageScrollView.currentImageView.frame = r
                    
                } else {
                    
                    // reset the imageView's frame
                    let n = CGFloat(placeImagePageControl.currentPage)
                    let r = CGRect(x: n * screenWidth, y: 0.0, width: screenWidth, height: screenWidth)
                    placeImageScrollView.currentImageView.frame = r
                }
            } else {
                
                // user is scrolling the images left/right
                let currentPage = Int(round(scrollView.contentOffset.x / scrollView.frame.width))
                placeImagePageControl.currentPage = currentPage > 2 ? 2 : currentPage
                placeImageScrollView.currentImageView = placeImageScrollView.imageViewsArray[placeImagePageControl.currentPage]
                
            }
        }
    }
    
    final class ImageScrollView: UIScrollView {
        var currentImageView: UIImageView!
        // array for added imageViews
        var imageViewsArray: [UIImageView] = []
        
        init(frame: CGRect, place: Place) {
            super.init(frame: frame)
            setupImageScrollView(place: place)
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
        }
        
        private func setupImageScrollView(place: Place) {
            contentSize = CGSize(width: screenWidth * 3, height: frame.height)
            showsHorizontalScrollIndicator = false
            backgroundColor = .white
            isPagingEnabled = true
            
            // add imageViews for scrollView
            for index in 0...2 {
                let image = UIImage.getImageFromBundle(fileName: place.imageNames[index], fileType: "jpg")
                let imageView = UIImageView(frame: CGRect(x: screenWidth * CGFloat(index), y: 0.0, width: screenWidth, height: screenWidth))
                imageView.contentMode = .scaleAspectFill
                imageView.clipsToBounds = true
                imageView.image = image
                addSubview(imageView)
                imageViewsArray.append(imageView)
                index == 0 ? currentImageView = imageView : nil
            }
        }
    }