Detect page change in UICollectionView - ios

I tried finding this question for a while but could not find this problem's answer.
My problem is that i have a UICollectionView and the Scroll Direction is Horizontal with Paging Enabled. My problem is that i want to keep the tack of the current page number on which the user is, so i created an int variable and now want to add or subtract it by 1 each time the user swipes right or left. I tried using scrollView's delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
but when the user swipes right or left, it is called as many number of the times as the number of columns on a page in the UICollectionView plus it wont let me know that whether the user went to the next page or the previous one.

Swift 3 Xcode 8.2
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let x = scrollView.contentOffset.x
let w = scrollView.bounds.size.width
let currentPage = Int(ceil(x/w))
// Do whatever with currentPage.
}

Use :
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat pageWidth = collectionView.frame.size.width;
float currentPage = collectionView.contentOffset.x / pageWidth;
if (0.0f != fmodf(currentPage, 1.0f))
{
pageControl.currentPage = currentPage + 1;
}
else
{
pageControl.currentPage = currentPage;
}
NSLog(#"Page Number : %ld", (long)pageControl.currentPage);
}
And if you are not using any pageControl, then ceil(currentPage) will be your current page number.

Based on #Shankar BS 's answer I've implemented it like this in Swit. Keep in mind that CollectionViewDelegate conforms to ScrollViewDelegate:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = scrollView.frame.size.width
let page = Int(floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1)
print("page = \(page)")
}

You can get the current page like below, index will be from 0 to (total page - 1)
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
NSLog(#"Current page -> %d",page);
}

SWIFT 5
For example, put this method in your ViewController with extensions UICollectionViewDelegate,UICollectionViewDataSource
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = scrollView.frame.size.width
let page = Int(floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1)
print("page = \(page)")
}

Swift 2.2
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let x = collectionView.contentOffset.x
let w = collectionView.bounds.size.width
let currentPage = Int(ceil(x/w))
print("Current Page: \(currentPage)")
}

You can get the currentPage this way when the user end the dragging:
Swift 5.6 Tested and works!
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let itemWidth = collectionView.frame.width // Get the itemWidth. In my case every cell was the same width as the collectionView itself
let contentOffset = targetContentOffset.pointee.x
let targetItem = lround(Double(contentOffset/itemWidth))
let targetIndex = targetItem % YOUR_ARRAY.count // numberOfItems which shows in the collection view
print(targetIndex)
}

Xamarin.iOS:
You can override DecelerationEnded method of UICollectionViewDelegate or subscribe to DecelerationEnded event of UICollectionView (custom delegate will be used under the hood).
public override void DecelerationEnded(UIScrollView scrollView)
{
var x = scrollView.ContentOffset.X;
var w = scrollView.Bounds.Size.Width;
var currentPage = Math.Ceiling(x / w);
// use currentPage here
}

Related

Change ScrollView Page When Scrolled Horizontally but not Vertically

I have a scroll view that contains 2 UITableViews. In other words, page 0 contains UITableView 0 and page 1 contains UITableView 1. At the top of the view, I have a segmented control. When a user swipes between pages, the highlighted segmented control changes. I.e. when the user is on Page 0 and they change to page 1, the segmented control that is highlighted also changes from page 0 to page 1.
My problem is that when the the current page is "Page 1" and I scroll vertically through my UITableView, my function changes the segmented control index to 0. If the user is on page 1 and they scroll vertically on the UITable, I do not want the index to be changed. I have provided my function to change the segmented control index below.
I have tried including && yOffset == 0 to my if statement, however it does not work because I believe Y == 0 for a brief moment in time.
Any help is much appreciated.
Thanks.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var previousPage : NSInteger = 0
let pageWidth : CGFloat = scrollView.frame.size.width
let fractionalPage = Float(scrollView.contentOffset.x / pageWidth)
let page = lroundf(fractionalPage)
let yOffset = Float(scrollView.contentOffset.y / pageWidth)
if previousPage != page {
previousPage = page
}
segments.selectedSegmentIndex = previousPage
segments(nil)
}
EDIT - SOLUTION:
Solution provided by Dhaval D. works perfectly. Prior to performing the page change, check to see whether or not the scroll was performed by the UITableView. Thank you for your help!
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Check whether scrolling is performed by UITableView then ignore page counter related stuff
if scrollView != tableView0 && scrollView != tableView1 {
var previousPage : NSInteger = 0
let pageWidth : CGFloat = scrollView.frame.size.width
let fractionalPage = Float(scrollView.contentOffset.x / pageWidth)
let page = lroundf(fractionalPage)
let yOffset = Float(scrollView.contentOffset.y / pageWidth)
if previousPage != page {
previousPage = page
}
segments.selectedSegmentIndex = previousPage
segments(nil)
}
}
You can do this by either matching your UITableView objects in your scrollViewDidScroll like below or you can compare your UIScrollView object for which you want to calculate PageNo or you want to change segment control for showing current selection.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Check whether scrolling is performed by UITableView then ignore page counter related stuff
if scrollView != tableView0 && scrollView != tableView1 {
var previousPage : NSInteger = 0
let pageWidth : CGFloat = scrollView.frame.size.width
let fractionalPage = Float(scrollView.contentOffset.x / pageWidth)
let page = lroundf(fractionalPage)
let yOffset = Float(scrollView.contentOffset.y / pageWidth)
if previousPage != page {
previousPage = page
}
segments.selectedSegmentIndex = previousPage
segments(nil)
}
}

Scroll view when collection view scrolls

I want to scroll the UIView when collection view scrolls as shown in below video
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset.x)
let contentSize = self.propertyImageCollectionView.contentSize
let xOffSet = (((scrollView.contentOffset.x * self.propertyImageCollectionView.frame.width) / (contentSize.width - self.progressViewWidthConstratint.constant)))
let xValue = xOffSet + ((self.progressViewWidthConstratint.constant * self.progressViewLeadingConstraint.constant) / self.propertyImageCollectionView.frame.width)
print( self.progressViewWidthConstratint.constant)
print( xOffSet)
print(xValue)
if xValue > 0 && xValue < self.propertyImageCollectionView.frame.width - self.progressViewWidthConstratint.constant{
self.progressViewLeadingConstraint.constant = xValue
}
}
The view is not scrolling upto end of screen. It stops scrolling before some points. Where am I lacking in my code. I also tried to use KVO but not succeed. Do you have any alternative approach?

Disabling scrollViewDidScroll method when searching in UITableView in Swift

I implemented scrollViewDidScroll in order to update UITableView when tableView did scroll to bottom. and adding 10 new elements to it.
There is a UISearchbar in UITableView too. when I search something, scrolling might be needed. but I don't want scrollViewDidScroll to be executed in this situation.
override func scrollViewDidScroll(scrollView: UIScrollView) {
let height = scrollView.frame.size.height
let contentYoffset = scrollView.contentOffset.y
let distanceFromBottom = scrollView.contentSize.height - contentYoffset
if distanceFromBottom < height {
print(" you reached end of the table")
self.fetchOffset += 10
// changing fillteredPosts. updating tableView.
}
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
filteredPosts.removeAll()
let searchText = searchController.searchBar.text
if let searchString = searchText, let searchInt = Int(searchString) {
for post in originalAllPosts {
if post.userId == searchInt {
filteredPosts.append(post)
}
}
}
tableView.reloadData()
}
Assuming you are performing search for every single character that is entered
override func scrollViewDidScroll(scrollView: UIScrollView) {
if searchController.searchBar.text.isEmpty {
let height = scrollView.frame.size.height
let contentYoffset = scrollView.contentOffset.y
let distanceFromBottom = scrollView.contentSize.height - contentYoffset
if distanceFromBottom < height {
print(" you reached end of the table")
self.fetchOffset += 10
// changing fillteredPosts. updating tableView.
}
}
}
Here the statements inside scrollViewDidScroll will be executed only when searchBar is empty that means we are not performing a search action

Which page of UICollectionView is visible?

Right now I have a UICollectionView set up with horizontal paging. I have sized my cells to fit the screen so that there is only ever one visible(unless you are scrolling).
What I wish to do is get the index path of the cell that is in view once you have stopped scrolling. This is because other parts of the view are dependent on the information that is currently visible.
I have set up a function in the scrollViewWillBeginDecelerating function and I am close but I still don't seem to understand how it is grabbing the cells.
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
if( lastContentOffset > scrollView.contentOffset.x && lastPointVisited > 0)
{
lastPointVisited -= 1
print("Scrolling Right")
}
else if(lastContentOffset < scrollView.contentOffset.x && lastPointVisited < myPoints.count - 1)
{
lastPointVisited += 1
print("Scrolling Left")
}
lastContentOffset = scrollView.contentOffset.x
}
This gets me accurate results if I am very careful about scrolling. Going fast or only half scrolling a page so that it snaps back seems to throw everything off.
You can use following code to get page number
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = scrollView.frame.size.width
let page = Int(floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1)
print("Page Number : \(page)")
}
It works for me If I put the same code in these two delegates at same time
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
//code here
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
//code here
}
instead of writing it in
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView)
It gives me the exact value of index whether it is scrolling fast or half scrolled.
Hope it helps!
try this,
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
let cellObj = collectionViewDemo.visibleCells[0]
print(collectionViewDemo.indexPath(for: cellObj))
}
You can use the below code it will be always effective to find the visible collectionView page
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageWidth = scrollview.frame.width
let pageIndex = scrollview.frame.width
let indexTemp = Int((scrollview.contentOffset.x +
pageWidth / 2) / pageWidth)
print(pageIndex)
}

I want to make effect similar to resizing top view on contacts app?

Native contacts app has interesting effect - when user tries to scroll, the scroll view pushes the top view with avatar and only when top view is in "small mode" it is scrolled up.
I was able to resize view on scrolling, via didScroll method. But problem is, content offset is also changing while i push the top vied. In native contacts, content offset changes only when top view is in "small mode"
Any suggestions, how did they made this?
Here's a sample project of how you could do it:
https://github.com/lukaswuerzburger/resizeable-tableview-header
You simply have a view controller with a table view and a custom view (resizable header) in it. On scrollViewDidScroll you change the height constraint.
PS: The transitions between the master and detail view controller are not prefect, but maybe someone can help me with that. The change of navigationBar's backgroundImage and shadowImage doesn't really work animated when popping back to the master view controller.
The bounty answer is good, but it still gives somewhat choppy solution to this problem. If you want exacly like native this is your solution. I have been playing a lot lately with collection views, and have gained much more experience. One of the things that I found is that this problem can easily be solved with custom layout:
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
let minHeightForHeader: CGFloat = 50
let offsetFromTop: CGFloat = 20
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// get current attributes
let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)
// get first section
if let attributes = attributes, elementKind == UICollectionElementKindSectionHeader && indexPath.section == 0 {
if let collectionView = collectionView {
// now check for content offset
var frame = attributes.frame
let yOffset = collectionView.contentOffset.y
if yOffset >= -offsetFromTop {
let calculatedHeight = frame.size.height - (yOffset + offsetFromTop)
let maxValue = minHeightForHeader > calculatedHeight ? minHeightForHeader : calculatedHeight
frame.size.height = maxValue
attributes.frame = frame
}
else {
frame.origin.y = offsetFromTop + yOffset
attributes.frame = frame
}
}
return attributes
}
// default return
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Just replace your default layout with this custom class and add this line somewhere in your setup method
flowLayout.sectionHeadersPinToVisibleBounds = true
Voila!
EDIT: Just one more optional method for clipping part, when you scroll to certain part, scroll might continue or return:
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let collectionView = collectionView {
let yOffset = collectionView.contentOffset.y
if yOffset + offsetFromTop >= maxHeightForHeader / 2 && yOffset + offsetFromTop < maxHeightForHeader && !(velocity.y < 0) || velocity.y > 0{
return CGPoint.init(x: 0, y: maxHeightForHeader - minHeightForHeader - offsetFromTop)
}
if yOffset + offsetFromTop < maxHeightForHeader / 2 && yOffset + offsetFromTop > 0 || velocity.y < 0 {
return CGPoint.init(x: 0, y: -offsetFromTop)
}
}
return proposedContentOffset
}

Resources