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:
And this thin line on the side when scrolling:
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
}
}
}