Home > database >  Problem with parallax effect animation for imageViews in Swift
Problem with parallax effect animation for imageViews in Swift

Time:08-02

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

CodePudding user response:

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
        }
    }
}
  • Related