Parallax effect is slightly choppy when first loaded in UICollectionView in Swift - ios

I am working on implementing a parallax effect for my UICollectionView. The effect thankfully is working. However, when I scroll the collection vertically for the very first time, there is a choppy effect that occurs. This ONLY happens on the first scroll through of the list. After that, the parallax effect is smooth while scrolling.
Below is my relevant code:
MyViewController.swift:
extension FeedViewController: UIScrollViewDelegate{
func scrollViewDidScroll(scrollView: UIScrollView) {
// Scroll navigation bar animation
let scrolledDown = scrollView.contentOffset.y > -self.topLayoutGuide.length
let newAlpha:CGFloat = scrolledDown ? 0.65 : 0.00
UIView.animateWithDuration(0.25) {
self.navBarBackground.alpha = newAlpha
}
// Parallax effect
for cell in collectionView.visibleCells() {
updateParallaxCell(cell)
}
}
func updateParallaxCell(cell: UICollectionViewCell) {
if let myCell = cell as? MyCustomCell {
myCell.updateParallaxEffect(collectionView, mainView: view)
}
}
}
MyCustomCell.swift
override func awakeFromNib() {
super.awakeFromNib()
backgroundImageOriginalOrigin = backgroundImage.frame.origin
}
func updateParallaxEffect(collectionView: UICollectionView, mainView view: UIView) {
let convertedRect = collectionView.convertRect(self.frame, toView: view) //frame of the cell within the coordinate system of the main view
let distanceFromCenter = CGRectGetHeight(self.frame) / 2 - CGRectGetMinY(convertedRect) //y coordinate distance of the cell from the center of the main view
let difference : CGFloat = 50.0 //maximum relative offset of the image within the cell
let move = -difference / 4 + (distanceFromCenter / self.frame.size.height) * difference // calculated relative offset of the image
let y = self.backgroundImageOriginalOrigin.y + move // newly calculated Y coordinate of the image
self.backgroundImage.frame.origin = CGPointMake(self.backgroundImageOriginalOrigin.x, y) // adjusting the image view frame
}
override func layoutSubviews() {
updateParallaxEffect(collectionView, mainView: mainView)
super.layoutSubviews()
}
Can anyone see what it is I'm doing wrong?

Try adding this to your viewDidLoad():
UICollectionView().setContentOffset(CGPointMake(0, 1), animated: true)
UICollectionView().setContentOffset(CGPointMake(0, 0), animated: false)

Related

Manually scrolling UIScrollView which is animated by UIViewPropertyAnimator

I have a UIScrollView which scrolls automatically by setting its content offset within via a UIViewPropertyAnimator. The auto-scrolling is working as expected, however I also want to be able to interrupt the animation to scroll manually.
This seems to be one of the selling points of UIViewPropertyAnimator:
...dynamically modify your animations before they finish
However it doesn't seem to play nicely with scroll views (unless I'm doing something wrong here).
For the most part, it is working. When I scroll during animation, it pauses, then resumes once deceleration has ended. However, as I scroll towards the bottom, it rubber bands as if it is already at the end of the content (even if it is nowhere near). This is not an issue while scrolling towards the top.
Having noticed this, I checked the value of scrollView.contentOffset and it seems that it is stuck at the maximum value + the rubber banding offset. I found this question/answer which seems to be indicate this could be a bug with UIViewPropertyAnimator.
My code is as follows:
private var maxYOffset: CGFloat = .zero
private var interruptedFraction: CGFloat = .zero
override func layoutSubviews() {
super.layoutSubviews()
self.maxYOffset = self.scrollView.contentSize.height - self.scrollView.frame.height
}
private func scrollToEnd() {
let maxOffset = CGPoint(x: .zero, y: self.maxYOffset)
let duration = (Double(self.script.wordCount) / Double(self.viewModel.wordsPerMinute)) * 60.0
let animator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
self.scrollView.contentOffset = maxOffset
}
animator.startAnimation()
self.scrollAnimator = animator
}
extension UIAutoScrollView: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// A user initiated pan gesture will begin scrolling.
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
self.interruptedFraction = scrollAnimator.fractionComplete
scrollAnimator.pauseAnimation()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
scrollAnimator.startAnimation()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
scrollAnimator.startAnimation()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
switch scrollView.panGestureRecognizer.state {
case .changed:
// A user initiated pan gesture triggered scrolling.
if let scrollAnimator = self.scrollAnimator {
let fraction = (scrollView.contentOffset.y - self.maxYOffset) / self.maxYOffset
let boundedFraction = min(max(.zero, fraction), 1)
scrollAnimator.fractionComplete = boundedFraction + self.interruptedFraction
}
default:
break
}
}
}
Is there anywhere obvious I'm going wrong here? Or any workarounds I can employ to make the scroll view stop rubber banding on scroll downwards?
You can add tap Gesture Recognizer and call this function,
extension UIScrollView {
func stopDecelerating() {
let contentOffset = self.contentOffset
self.setContentOffset(contentOffset, animated: false)
}
}

Determine Percentage of Visibility of Horizontal CollectionView Cells on Screen

I have a horizontal collectionView. There will always be 2 cells side by side, they both have the same width and height. The cells plays videos and when scrolling I want to load the video for the next cell that is 70% on screen and stop the video for the cell that is less than that. I found several similar questions but everything seems to reference 1 cell.
1- Cells 59 and 60 are both 100% on screen
2- Scrolling left, 59 is still more than 70%, 60 is 100%, and 61 is less than 30% on screen
3- Still scrolling left, 59 is less than 30% on screen, 60 is 100%, and 61 is more than 70%
Anything that is is already visible as in cell 59 in the first two photos and 60 in all three photos, I have logic to prevent the cell from loading again even though cell.loadNewVideo() will run for them.
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = UIScreen.main.bounds.width / 2
return CGSize(width: width, height: 150)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.visibleCells.forEach { cell in
guard let cell = cell as? VideoCell else { continue }
guard let indexPath = collectionView.indexPath(for: cell) else { return }
guard let layoutAttributes = collectionView.layoutAttributesForItem(at: indexPath) else { return }
let cellFrameInSuperview = collectionView.convert(layoutAttributes.frame, to: collectionView.superview)
guard let superView = collectionView.superview else { return }
let convertedRect = collectionView.convert(cellFrameInSuperview, to: superView)
let intersect = collectionView.frame.intersection(convertedRect)
if intersect.width > (UIScreen.main.bounds.width / 2) * 0.7 {
cell.loadNewVideo()
} else {
cell.destroyOldVideo()
}
}
}
I also tried the same code scrollViewWillEndDragging in but it also didn't work:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
}
You are way overthinking this. Forget about layoutAttributes and all that fancy-pants stuff and just do what you said you wanted to do: find out how much of the cell is onscreen.
The cell is a view. The screen (represented, let's say, by the window) is a view. Intersect them! Look at the intersection of the cell's frame with the window (or whatever), in appropriately converted coordinates, to get the part of the cell that is onscreen, and compare its size with the cell's frame's size. That is your percentage. Now do anything you like that percentage.
In this example, "anything you like" is just to display the percentage.
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let cells = self.collectionView.visibleCells
for cell in cells {
let f = cell.frame
let w = self.view.window!
let rect = w.convert(f, from: cell.superview!)
let inter = rect.intersection(w.bounds)
let ratio = (inter.width * inter.height) / (f.width * f.height)
let rep = (String(Int(ratio * 100)) + "%")
let label = cell.contentView.subviews.first as! UILabel
label.text = rep
}
}

Swift: change tableview height on scroll

I have a VC as shown in the image
It has a UICollectionView on top, and a UITableView at the bottom.
CollectionView has 1:3 of the screen and TableView has 2:3 of the screen(set using equalHeight constraint).
I want to change the height of the UICollectionView when the tableView is scrolled.
When the tableView is scrolled up,I want to change the multiplier of equalHeights constraint to like 1:5 and 4:5 of collectionView and tableView respectively.This will ensure that height of tableView increases and collectionView decreases
When the tableView is scrolled down, the multiplier of equalHeights constraint should reset to default.
I've tried adding swipe and pan gestures to tableView, but they are unrecognised.
How to achieve this functionality?
P.S: Would love to achieve this functionality by using a pan gesture, so that dragging up and down changes the heights progressively.
This is the view hierarchy
EDIT/UPDATE
This is the code that I'm using.
class MyConstant {
var height:CGFloat = 10
}
let myConstant = MyConstant()
MainScreenVC
override func viewWillAppear(_ animated: Bool) {
myConstant.height = self.view.frame.size.height
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (self.lastContentOffset < scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.decreaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = -self.view.frame.height / 6
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
} else if (self.lastContentOffset > scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.increaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = 0
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
}
self.lastContentOffset = scrollView.contentOffset.y
}
Cell.Swift
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(decreaseHeight), name: Notification.Name("decreaseHeightNotification"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(increaseHeight), name: Notification.Name("increaseHeightNotification"), object: nil)
self.contentView.translatesAutoresizingMaskIntoConstraints = false
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = heightConstraint.constant * 1.5
}
#objc func decreaseHeight() {
heightConstraint.constant = (myConstant.height / 6) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
#objc func increaseHeight() {
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
Now when I scroll both simultaneously, the screen freezes. Also is there a better way of resizing the collectionViewCell size?
I haven't tested it but you can do this. Use Autolayout in this view. it will work better with that.
Set the tableview constraint as Top, Left, Bottom, Right => 0, 0, 0, 0 with the main view and put collectionView under the tableview with constraint as Top,Left,Right, height => 0, 0, 0, x with the main view.
Note: Tableview is on top of the collectionView.
Connect your height constraint outlet of CollectionView and also define your defaultOffset variable
#IBOutlet weak var defaultHeightConstraint: NSLayoutConstraint
var defaultOffSet: CGPoint?
In viewDidLoad,
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset = UIEdgeInsetsMake(collectionView.size.height, 0, 0, 0)
}
In viewDidAppear, write
override func viewDidAppear(_ animated: Bool) {
defaultOffSet = tableView.contentOffset
}
In ViewDidScroll
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = tableView.contentOffset
if let startOffset = self.defaultOffSet {
if offset.y < startOffset.y {
// Scrolling down
// check if your collection view height is less than normal height, do your logic.
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant - deltaY
} else {
// Scrolling up
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant + deltaY
}
self.view.layoutIfNeeded()
}
}
Hope it helps.
Since UITableView is a subclass of UIScrollView, your table view's delegate can receive UIScrollViewDelegate methods.
You don't need to use PanGesture
First implement UITableviewDelegate and in that
var oldContentOffset = CGPoint.zero
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y - oldContentOffset.y
if (self.oldContentOffset < scrollView.contentOffset.y) {
// moved to top
} else if (self.oldContentOffset > scrollView.contentOffset.y) {
// moved to bottom
} else {
// didn't move
}
oldContentOffset = scrollView.contentOffset
}
Here you can write you logic to change constant value of your constraint
UPDATE/EDIT
Another thing is don't use multiplier for tabelview height.with collection view it is fine you can give height constraint with self.view in ratio of 1/3 . for tableview just give leading ,trailing top (UICollectionView) and bottom.
So when you change height constant of UICollectionView Tableview height automatically decrease and vice-versa for decrease.
You can not change the multiplier from the outlet as it is read only. so you have to constant value to self.view.frame.height / 3 will work for you
Hope it is helpful to you
For a much cleaner approach, why don't you use a UIScrollView as the container view and add the UICollectionView and UITableView inside it and give the collectionView a height constraint while disabling tableView's scroll till the height constraint of your collectionView becomes 0.
Here's the snippet:
Extend your VC with UIScrollViewDelegate and use:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= 0 && scrollView.contentOffset.y <= yourLimit {
heightConstraint.constant = collectionViewHeight - scrollView.contentOffset.y
tableView.isScrollEnabled = false
} else if scrollView.contentOffset.y > yourLimit {
heightConstraint.constant = collectionViewHeight
tableView.isScrollEnabled = true
}
}
Try this snippet after customizing small things in it. It should work mostly, if it doesn't there is another approach which I'd share once this doesn't work.
Why don't you just add Custom Table Cell with your collection view inside, and when indexPath.row == 0 , return your custom table cell.

UITableViewCell Scroll View

I have a table view cell with a scroll view for scrolling through images. There are a couple of issues I'm having with it.
When the table view is shown, the cell with the scroll view and images shows like the first image and part of the second image and the paging doesn't work and it lets you scroll in any direction.
Once I scroll past this cell and then scroll back to it, it is displayed correctly and paging works like it should.
In cellForRowAtIndexPath()
if indexPath.section == 0 {
let imageCell = tableView.dequeueReusableCellWithIdentifier("imageCell", forIndexPath: indexPath) as! ImageCell
imageCell.selectionStyle = .None
if imageFiles.count > 0 {
if imageFiles.count == 1 {
imageCell.pageControl.hidden = true
}
imageCell.pageControl.numberOfPages = imageFiles.count
imageCell.pageControl.currentPage = 0
var index = 0
for photo in imageFiles {
var frame = CGRect()
frame.origin.x = (imageCell.imageScrollView.frame.size.width * CGFloat(index))
frame.origin.y = 0
frame.size = CGSizeMake(imageCell.imageScrollView.frame.size.width, imageCell.imageScrollView.frame.size.height)
let imageView = UIImageView(frame: frame)
imageView.image = photo
imageCell.imageScrollView.addSubview(imageView)
imageCell.imageScrollView.contentSize = CGSizeMake(imageCell.imageScrollView.frame.size.width * CGFloat(imageFiles.count), imageCell.imageScrollView.frame.size.height)
index += 1
}
} else {
imageCell.pageControl.hidden = true
let imageView = UIImageView(frame: imageCell.imageScrollView.frame)
imageView.image = UIImage(named: "placeholder")
imageCell.imageScrollView.addSubview(imageView)
}
My custom cell:
import UIKit
class ImageCell: UITableViewCell, UIScrollViewDelegate {
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var imageScrollView: UIScrollView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageWidth = scrollView.frame.size.width
pageControl.currentPage = Int(scrollView.contentOffset.x / pageWidth)
}
}
I was using SDWebImageDownloader. viewDidLoad() to download images to use in the scroll view but I wasn't reloading the table view data. Once I added that, this works perfectly.
func downloadImages() {
let downloader = SDWebImageDownloader.sharedDownloader()
for imageURL in images {
let url = NSURL(string: imageURL)
downloader.downloadImageWithURL(url, options: SDWebImageDownloaderOptions.UseNSURLCache, progress: nil,
completed: { (image, data, error, bool) -> Void in
self.imageFiles.append(image)
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
})
}
}
You are doing this:
frame.size = CGSizeMake(imageCell.imageScrollView.frame.size.width, imageCell.imageScrollView.frame.size.height)
I would guess that when your view initially loads, imageScrollView has not been fully laid out, so it doesn't have a valid size. Scrolling away from this cell & back to it after the view is fully loaded would result in the scroll view having a valid size.
You should validate that imageScrollView has the width you think it does, and if it doesn't, maybe use the width of the screen instead.
I have implemented a similar thing in one of my apps, so as another possible option, instead of putting a scroll view in your first cell, you could place a CollectionView in there instead and populate your collection view with cells of images. This would A) allow your layout to dynamically change with the view, and B) rather than loading up your scroll view with all the images, they can be dynamically loaded as needed.

Swift: UITableView increments layoutSubviews() when scrolling

I have a table view with custom cell in Swift that contains a horizontal layout UIView. I have noticed that when scrolling the correctly positioned subview in the horizontal view starts to multiply. I guess this has something to do with layoutSubviews() being called when table scrolls and the fact tableview recycles its cells when hidden and shows them when needed, but ignores currently positioned subviews..
It looks something like this
There's already a similar question from before, but it has no good answer.
UIScrollview calling superviews layoutSubviews when scrolling?
Here's the code I'm using inside my custom cell for horizontal positioning:
class HorizontalLayout: UIView {
var xOffsets: [CGFloat] = []
override func layoutSubviews() {
var width: CGFloat = 0
for i in 0..<subviews.count {
var view = subviews[i] as UIView
view.layoutSubviews()
width += xOffsets[i]
view.frame.origin.x = width
width += view.frame.width
}
self.frame.size.width = width
}
override func addSubview(view: UIView) {
xOffsets.append(view.frame.origin.x)
super.addSubview(view)
}
func removeAll() {
for view in subviews {
view.removeFromSuperview()
}
xOffsets.removeAll(keepCapacity: false)
}
}
Taken from here: https://medium.com/swift-programming/dynamic-layouts-in-swift-b56cf8049b08
Using inside custom cell like so:
func loadStops(stops:[String]) {
for stop in stops {
// just testing purposes only
let view = UIView(frame: CGRectMake(10, 0, 40, 40))
view.backgroundColor = UIColor.redColor()
stopsView.addSubview(view)
}
}
Is there a way to fix this problem and prevent the subview of being multiplied when scrolling and perhaps a better way to position subviews horizontally in a tableview cell?

Resources