UICollectionView's scrollToItem not calling targetContentOffset - ios

I have a collection view that implements paging that is why I override the targetContentOffset in a custom UICollectionViewFlowLayout to handle it. It gets called when the collection view is scrolled through user interaction and it works. However, it does not get called when using scrollToItem or the scroll to visible rect. What would be the best way to scroll to the collection view programmatically that will surely go through the method targetContentOffset?

In my case i have used bellow method for Maintain paging and scrolling programmatically.
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if scrollView.contentOffset.y > -topHeaderView.frame.size.height && scrollView.contentOffset.y < -20 - 50 {
let aHeaderHeight = topHeaderView.frame.size.height
if velocity.y <= 0 {
targetContentOffset.memory = CGPoint(x: 0, y: -aHeaderHeight)
} else {
targetContentOffset.memory = CGPoint(x: 0, y: 20 + 50)
}
}
}
Hope this will help you.

Related

UICollectionView with pagination. Keeping the second cell in center of the screen

I am new to iOS.
I am having my collection view inside tableview cell.
There are 3 cells in collection view cell.
I need to show the second cell of collection view in center of the screen as shown in the image and also want to add pagination into it.
Any help will be appreciated.
Image
Thank You
I had to do a similar collection view a few months ago, This is the code that I use:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let layout = theNameOfYourCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let cellWidthIncludingSpacing = layout.itemSize.width + layout.minimumLineSpacing // Calculate cell size
let offset = scrollView.contentOffset.x
let index = (offset + scrollView.contentInset.left) / cellWidthIncludingSpacing // Calculate the cell need to be center
if velocity.x > 0 { // Scroll to -->
targetContentOffset.pointee = CGPoint(x: ceil(index) * cellWidthIncludingSpacing - scrollView.contentInset.right, y: -scrollView.contentInset.top)
} else if velocity.x < 0 { // Scroll to <---
targetContentOffset.pointee = CGPoint(x: floor(index) * cellWidthIncludingSpacing - scrollView.contentInset.left, y: -scrollView.contentInset.top)
} else if velocity.x == 0 { // No dragging
targetContentOffset.pointee = CGPoint(x: round(index) * cellWidthIncludingSpacing - scrollView.contentInset.left, y: -scrollView.contentInset.top)
}
}
This code calculates the size of your cell, how many cells have already been shown and once the scroll is finished, adjust it to leave the cell centered.
Make sure you have the pagingEnabled of your collectionView in false if you want to use this code.
Also, implement UIScrollViewDelegatein your ViewController

Change UICollectionView's bounce start/end points

Normally UICollectionView starts to bounce when scrolled past it's contentSize, ie when contentOffset < 0 or contentOffset > contentSize.width for horizontal orientation.
Is it possible to change this behavior so the bounce effect starts when scrolled past let's say 10th item (when contentOffset < itemSize.Width * 10 or contentOffset > contentSize.width - (itemSize.Width * 10))?
UPDATE 1:
#OverD - thanks for pointing me towards the right direction.
I ended up with some work in scrollViewWillEndDragging and adjusting targetContentOffset when necessary.
The problem I'm still facing is that the bounce animation is not smooth like the original bounce when reaching the contenSize end.
Any ideas what's missing? Code snippet below:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let numberOfBouncingCells = 10
let extraSectionWidth = (collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumLineSpacing) * numberOfBouncingCells
let startXOffset = extraSectionWidth
let endXOffset = collectionView.contentSize.width - 2 * extraSectionWidth
let yOffset = collectionView.contentOffset.y
if targetContentOffset.pointee.x < startXOffset {
targetContentOffset.pointee = CGPoint(x: startXOffset, y: yOffset)
} else if targetContentOffset.pointee.x > endXOffset {
targetContentOffset.pointee = CGPoint(x: endXOffset, y: yOffset)
}
}
UPDATE 2 (for answer):
See answer below, I ditched the scrollViewWillEndDragging approach in favor of simply changing collectionView.contentInset [.left, .right]
I've answered my own question eventually.
I only had to set collectionView.contentInset [.left, .right] to achieve expected behavior.
var collectionViewFlowLayout: UICollectionViewFlowLayout {
return collectionView.collectionViewLayout as! UICollectionViewFlowLayout
}
let numberOfBouncingCells = 10
let extraBouncingWidth = (collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumLineSpacing) * numberOfBouncingCells
collectionView.contentInset.left = -extraBouncingWidth
collectionView.contentInset.right = -extraBouncingWidth

UIScrollView behaving weird when updating constraints subview

I made an UIScrollView in a XIB for my onboarding. The UIScrollView has 3 onboarding views. Long story short:
This works perfect. However I want the top left and right buttons (Overslaan - Volgende) to animate up / off the screen when the third/last page is on screen. My UIScrollView starts behaving weird when I animate the buttons off:
This is the code im using:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageIndex = Int(targetContentOffset.pointee.x / self.frame.width)
pageControl.currentPage = pageIndex
if stepViews[pageIndex] is OnboardingLoginView {
moveControlConstraintsOffScreen()
} else {
moveControlConstraintsOnScreen()
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
}, completion: nil)
}
I debugged the code and it turns out that setting a new constant for the constraints causes the issue, regardless of the animation block. How do I make the buttons move up/off the screen without my scrollView behaving weird?
It looks like triggering a layout pass is interfering with your scroll view positioning. You could implement func scrollViewDidScroll(_ scrollView: UIScrollView) and try to update the button view on a per-frame basis, without changing Auto Layout constraints. I usually use the transform property for frame changes outside of Auto Layout, since any changes to frame or bounds are overwritten during the next layout pass.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard mustBeOnLastPage else {
buttonOne.tranform = .identity
buttonTwo.tranform = .identity
return
}
let offset = scrollView.contentOffset.x
buttonOne.tranform = .init(translationX: 0, y: offset)
buttonTwo.tranform = .init(translationX: 0, y: offset)
}
Old answer
This interpretation is a bit of tangent of what you're asking for.
Since it looks like you're using the scroll view in a paging context, I would approach this problem by using UIPageViewController. Since UIPageViewController uses UIScrollView internally, you can observe the contentOffset of the last view in the scroll view to determine how far along the page has scrolled.
Yes, this involves looking inside the view hierarchy, but Apple hasn't changed it for half a decade so you should be safe. Coincidentally, I actually implemented this approach last week and it works like a charm.
If you're interested, I can further expand on this topic. The post that pointed me in the right direction can be found here.
Here is what i do
I can't post image,you can look at this http://i.imgur.com/U7FHoMu.gif
and this is the code
var centerYConstraint: Constraint!
func setupConstraint() {
fadeView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
centerYConstraint = make.centerY.equalToSuperview().constraint
make.size.equalTo(CGSize(width: 50, height: 50))
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint,targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageIndex = Int(targetContentOffset.pointee.x / self.view.frame.width)
if pageIndex != 1 {
centerYConstraint.update(offset: self.view.frame.height)
} else {
centerYConstraint.update(offset: 0)
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Based on #CloakedEddy's answer I made 2 changes:
1: It seems layoutSubviews is responsible for the weird behaviour. To fix this I prevent the scrollView from calling layoutSubviews all the time:
override func layoutSubviews() {
super.layoutSubviews()
if !didLayoutSubviews {
for index in 0..<stepViews.count {
let page: UIView = stepViews[index]
let xPosition = scrollView.frame.width * CGFloat(index)
page.frame = CGRect(x: xPosition, y: 0, width: scrollView.bounds.width, height: scrollView.frame.height)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(index + 1)
}
didLayoutSubviews = true
}
}
2: If you want to update your views for device orientations:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
onboardingView.didLayoutSubviews = false
onboardingView.setNeedsLayout()
}

Why is UIView moved to right on offsetBy

I have a scrollview with an UIImageView on it. When the user scrolls, I would like to keep the UIImageView at its original place. I am using offsetBy for this:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
self.headerView!.currentLoadLabel.frame = currentLoadLabelFrame.offsetBy(dx: 0, dy: offset)
}
The UIImageView stays at his y position but moves to the farmost x position at the right of the screen. Why?
try following code:
self.headerView!.currentLoadLabel.frame = currentLoadLabelFrame.offsetBy(dx: scrollView.contentOffset.x, dy: scrollView.contentOffset.y)

Add snap-to position in a UITableView or UIScrollView

Is it possible to add a snap-to position in a UITableView or UIScrollView? What I mean is not auto scroll to a position if I press a button or call some method to do it, I mean is if I scroll my scroll view or tableview around a specific point, say 0, 30, it will auto-snap to it and stay there? So if my scroll view or table view scrolls and then the user lets go inbetween 0, 25 or 0, 35, it will auto "snap" and scroll there? I can imagine maybe putting in an if-statement to test if the position falls in that area in either the scrollViewDidEndDragging:WillDecelerate: or scrollViewWillBeginDecelerating: methods of UIScrollView but I'm unsure how to implement this in the case of a UITableView. Any guidance would be much appreciated.
Like Pavan stated, scrollViewWillEndDragging: withVelocity: targetContentOffset: is the method you should use. It works with table views and scroll views. The code below should work for you if you are using a table view or vertically scrolling scroll view. 44.0 is the height of the table cells in the sample code so you will need to adjust that value to the height of your cells. If used for a horizontally scrolling scroll view, swap the y's for x's and change the 44.0 to the width of the individual divisions in the scroll view.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// Determine which table cell the scrolling will stop on.
CGFloat cellHeight = 44.0f;
NSInteger cellIndex = floor(targetContentOffset->y / cellHeight);
// Round to the next cell if the scrolling will stop over halfway to the next cell.
if ((targetContentOffset->y - (floor(targetContentOffset->y / cellHeight) * cellHeight)) > cellHeight) {
cellIndex++;
}
// Adjust stopping point to exact beginning of cell.
targetContentOffset->y = cellIndex * cellHeight;
}
I urge you to use the
scrollViewWillEndDragging: withVelocity: targetContentOffset: method instead which is meant to be used for exactly your purpose. to set the target content offset to your desired position.
I also suggest you look at the duplicate questions already posted on SO.
Take a look at these posts
If you really must do this manually, here is a Swift3 version. However, it's highly recommended to just turn on paging for the UITableView and this is handled for you already.
let cellHeight = 139
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee.y = round(targetContentOffset.pointee.y / cellHeight) * cellHeight
}
// Or simply
self.tableView.isPagingEnabled = true
In Swift 2.0 when the table has a content inset and simplifying things, ninefifteen's great answer becomes:
func scrollViewWillEndDragging(scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>)
{
let cellHeight = 44
let y = targetContentOffset.memory.y + scrollView.contentInset.top + cellHeight / 2
var cellIndex = floor(y / cellHeight)
targetContentOffset.memory.y = cellIndex * cellHeight - scrollView.contentInset.top;
}
By just adding cellHeight / 2, ninefifteen's if-statement to increment the index is no longer needed.
Here's the Swift 2.0 equivalent
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let cellWidth = CGRectGetWidth(frame) / 7 // 7 days
var index = round(targetContentOffset.memory.x / cellWidth)
targetContentOffset.memory.x = index * cellWidth
}
And this complicated rounding isn't necessary at all as long as you use round instead of floor
This code lets you snap to a cell, even when cells have a variable (or unknown) height, and will snap to the next row if you'll scroll over the bottom half of the row, making it more "natural".
Original Swift 4.2 code: (for convenience, this is the actual code I developed and tested)
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard var scrollingToIP = table.indexPathForRow(at: CGPoint(x: 0, y: targetContentOffset.pointee.y)) else {
return
}
var scrollingToRect = table.rectForRow(at: scrollingToIP)
let roundingRow = Int(((targetContentOffset.pointee.y - scrollingToRect.origin.y) / scrollingToRect.size.height).rounded())
scrollingToIP.row += roundingRow
scrollingToRect = table.rectForRow(at: scrollingToIP)
targetContentOffset.pointee.y = scrollingToRect.origin.y
}
(translated) Objective-C code: (since this question is tagged objective-c)
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
NSIndexPath *scrollingToIP = [self.tableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
if (scrollingToIP == nil)
return;
CGRect scrollingToRect = [table rectForRowAtIndexPath:scrollingToIP];
NSInteger roundingRow = (NSInteger)(round(targetContentOffset->y - scrollingToRect.origin.y) / scrollingToRect.size.height));
scrollingToIP.row += roundingRow;
scrollingToRect = [table rectForRowAtIndexPath:scrollingToIP];
targetContentOffset->y = scrollingToRect.origin.y;
}
I believe that if you use delegate method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(#"%f",scrollView.contentOffset.y);
if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370)
{
NSLog(#"setContentOffset");
[scrollView setContentOffset:CGPointMake(0, 350) animated:YES];
}
}
Play around with it until you get the desired behaviour.
Maybe calculate the next top edge of the next tableViewCell/UIView and stop at the top of the nearest one at the slow down.
The reason for this: if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370) is that the scroll view calls scrollViewDidScroll in jumps at fast speeds so I give a between if.
You can also know the speed is slowing down by:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"%f",scrollView.contentOffset.y);
int scrollSpeed = abs(scrollView.contentOffset.y - previousScrollViewYOffset);
previousTableViewYOffset = scrollView.contentOffset.y;
if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370 && scrollSpeed < minSpeedToStop)
{
NSLog(#"setContentOffset");
[scrollView setContentOffset:CGPointMake(0, 350) animated:YES];
}
}
Updated for Swift 5
As a bonus, it works for whatever the height is of the cell it was going to land on
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let indexPath = self.tableView.indexPathForRow(at: CGPoint(x: self.tableView.frame.midX, y: targetContentOffset.pointee.y)) else {
return
}
var cellHeight = tableView(self.tableView, heightForRowAt: indexPath)
let cellIndex = floor(targetContentOffset.pointee.y / cellHeight)
if targetContentOffset.pointee.y - (floor(targetContentOffset.pointee.y / cellHeight) * cellHeight) > cellHeight {
cellHeight += 1
}
targetContentOffset.pointee.y = cellIndex * cellHeight
}

Resources