UICollectionView: how to detect when scrolling has stopped - ios

I'm using a UICollectionView to scroll through a set of thumbnails quickly. Once scrolling ends, I'd like to display a larger hi-res version of the current thumbnail.
How can I detect when the user has completed scrolling? I do implement didEndDisplayingCell, but that only tells me when a particular cell has scrolled off; it doesn't tell me when the scroll motion actually completes.

NS_CLASS_AVAILABLE_IOS(6_0) #interface UICollectionView : UIScrollView
UICollectionView is a subclass of UIScrollView. So if you have set the delegate and implemented UIScrollViewDelegate, you should be able to detect this the same way as UIScrollView.
For eg:-
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
As per documentation, the above method should tell when the scroll view has ended decelerating the scrolling movement.

Just to cover your bases you should implement both these UIScrollViewDelegate methods.
In some cases there may not be a deceleration (and scrollViewDidEndDecelerating would not be called), for e.g., the page is fully scrolled in place. In those case do your update right there in scrollViewDidEndDragging.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate) {
[self updateStuff];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self updateStuff];
}

An important fact to note here.
This method gets called on User initiated scrolls (i.e a Pan gesture):
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
or in Swift:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
On the other hand, this one gets called on all manually (programatically) initiated scrolls (like scrollRectToVisible or scrollToItemAtIndexPath):
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
or in Swift:
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView)

Swift 3 version of Abey M and D6mi 's answers:
When scroll is caused by user action
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if (!decelerate) {
//cause by user
print("SCROLL scrollViewDidEndDragging")
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
//caused by user
print("SCROLL scrollViewDidEndDecelerating")
}
When scroll is caused by code action (programmatically): (like "scrollRectToVisible" or "scrollToItemAtIndexPath")
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
//caused by code
print("SCROLL scrollViewDidEndScrollingAnimation")
}
Notes:
Put these functions in your UIScrollViewDelegate or UICollectionViewDelegate delegate.
if you don't have a separate delegate, make your current class extend a UIScrollViewDelegate op top of your class file
.
open class MyClass: NSObject , UICollectionViewDelegate
and somewhere in your viewWillAppear make the class its own delegate
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// ...
self.myScrollView.delegate = self
// ...
}

Swift 3 version:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Your code here
}

if you want to use the visible indexpath:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self scrollingFinish];
}
- (void)scrollingFinish {
if([self.collectionView indexPathsForVisibleSupplementaryElementsOfKind:UICollectionElementKindSectionHeader]){
NSIndexPath *firstVisibleIndexPath = [[self.collectionView indexPathsForVisibleSupplementaryElementsOfKind:UICollectionElementKindSectionHeader] firstObject];
[self.collectionView scrollToItemAtIndexPath:firstVisibleIndexPath atScrollPosition:UICollectionViewScrollPositionTop animated:YES];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
}

Get your collection view index and Dont forget to import UISCrollViewDelegate in your class
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let xPoint = scrollView.contentOffset.x + scrollView.frame.width / 2
let yPoint = scrollView.frame.height / 2
let center = CGPoint(x: xPoint, y: yPoint)
if let ip = self.collectionView.indexPathForItem(at: center) {
pageIndex = ip.row
}
print(">>>>>>>>>\(pageIndex)")
}

Related

Get type of UIScrollView inside UIScrollViewDelegate methods

I have a UIViewController that has UITableView and UICollectionView. I want to do certain tasks when UICollectionView is scrolled.
I've extended UIScrollViewDelegate and wrote my code in
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView)
But this method is called when both UITableView and UICollectionView are scrolled. How do I differentiate between the two views here? I tried
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if let cv = scrollView as? UICollectionView {
}
}
But it's not working. I tried po scrollView and the output is <uninitialized>.
// Say tv and cv are outlets to table View and Collection View
Objective c
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.tv) {
self.tv.contentOffset = self.tv.contentOffset;
} else if (scrollView == self.cv) {
self.cv.contentOffset = self.cv.contentOffset;
}
}
Swift
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if let scrollView == tv {
//do whatever you need with tableView
}
if let scrollView == cv {
//do whatever you need with collectionView
}
}
It was a mistake on my end but for others here I'll post the answer.
I needed to execute my code when slider has finished dragging/scrolling. To achieve that I had
func scrollViewDidScroll(_ scrollView: UIScrollView) {
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: nil, afterDelay: 0.3)
}
Problem was in line
perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: nil, afterDelay: 0.3)
Instead of passing nil, I wrote
perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: scrollView afterDelay: 0.3)
and that resolved it. I wasn't getting scrollView <uninitialized> anymore and my
if let cv = scrollView as? UICollectionView {
}
worked fine.

How can I detect when iOS ScrollView is at the very top?

How can I detect when the user scrolled to the very top of my collectionView?
I tried to use this code:
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if collectionView.contentOffset.y <= collectionView.contentSize.height - collectionView.frame.size.height {
print("top")
}
}
But it prints "top" even if I didn't get to the top.
scrollViewDidEndDragging means the user's finger lifted, but because it has physics to simulate inertia, the view may continue to move. There is another delegate method for exactly what you are looking for: scrollViewDidScrollToTop which as the name implies fires when you get to the top of the scrollview.
EDIT:
you can also try:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y <= 0 {
print("top!")
}
}
EDIT third time's the charm? This one only fires once, after the scroll view has stopped moving, if you are at the top.
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == 0 {
print("top!")
}
}
You can try
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == 0 {
// top
}
}

How to scroll two UITableViews symmetrically?

In my app I have two table views. Both are same height and width and same cell count.
I want when tableview A Scroll Simultaneously tableview B also scroll.
You can use scrollview delegate scrollViewDidScroll for this purpose. This will do the exact thing you want.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.tableA{
self.tableB.setContentOffset(self.tableA.contentOffset, animated: false)
}
else if scrollView == self.tableB{
self.tableA.setContentOffset(self.tableA.contentOffset, animated: false)
}
print("scrollViewDidScroll") // to check whether this function is called while scrolling
}
TRY THIS:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
<your tableView Name>.contentOffset = scrollview.contentOffset
}
As Tableview inherits from ScrollView. Try this.
FOR SWIFT 4:
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
<your tableView Name>.contentOffset = scrollview.contentOffset
}
Instead of using tableview you can use UIPickerView with two components.
If you need code let me know.

How to override the handlePan selector in UICollectionView

In my tvOS app, I am trying to listen to changes in scrolling of my UICollectionView. After research, I found that the collection view natively receives a few gesture recognizers among them a UIPanGestureRecognizer with the selector handlePan:
<UIScrollViewPanGestureRecognizer: 0x101a4c1a0; state = Possible; delaysTouchesEnded = NO; view = <UICollectionView 0x1020c5d00>; target= <(action=handlePan:, target=<UICollectionView 0x1020c5d00>)>>
in the log, or in code:
myCollectionView.panGestureRecognizer
I was wondering if there's a way to add my controller as the target of the gesture recognizer, or maybe override the handlePan method.
I tried implementing the UIGestureRecognizerDelegate but it does not give me an access to the handlePan method.
Maybe I should just add a custom UIPanGestureRecognizer of my own on the collection view?
UICollectionView is a subclass of UIScrollView so you can detect scroll changes on collectionview by adding scrollview delegates.
Objective-C
// called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
}
// called when scroll view grinds to a halt
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
}
Swift
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
}

Easiest way to disable back scrolling in scrollview

I want to stop backward scrolling on ScrollView after user scrolls to the next page. How can I do that.
I tried the following two codes, but the first one does not have any effect
self.scrollView.contentOffset = CGPointMake(self.view.frame.width,0)
and the second only disables the forward scrolling.
self.scrollView.contentSize = CGSizeMake( 2 * scrollWidth, scrollHeight);
To disable scrolling in one direction you implement the UIScrollViewDelegate method scrollViewDidScroll and put your logic there. For instance this TableViewController can only ever scroll down, because if the user tries to scroll up, we just overwrite the contentOffset, effectively undoing their scroll before they see it.
class ViewController: UITableViewController {
var lastScrollPosition = CGPoint.zero
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.y > lastScrollPosition.y else {
scrollView.setContentOffset(lastScrollPosition, animated: false)
return
}
lastScrollPosition = scrollView.contentOffset
}
}
If your cell is equal in size to your screen, you can apply the following option, which is very smooth:
var lastScrollPosition = CGPoint.zero
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x == lastScrollPosition.x + UIScreen.main.bounds.width {
lastScrollPosition.x += UIScreen.main.bounds.width
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.x > lastScrollPosition.x else {
scrollView.setContentOffset(lastScrollPosition, animated: false)
return
}
}

Resources