mlevi
mlevi

Reputation: 1463

UICollectionView Horizontal Paging not centered

I have a horizontal scrolling collectionView with each cell the size of the view. When I page through the collectionView it doesn't page by cell. The cells aren't in the center of the screen. I've tried a bunch of things to try to fix it and haven't had any luck. Here's a video of the problem: https://www.youtube.com/watch?v=tXsxWelk16w Any ideas?

Upvotes: 68

Views: 73125

Answers (16)

TejAces
TejAces

Reputation: 551

Swift 4 solution to remove line spacing to keep cells centered:

public func collectionView(
  _ collectionView: UICollectionView, 
  layout collectionViewLayout: UICollectionViewLayout, 
  minimumLineSpacingForSectionAt section: Int
) -> CGFloat {
  return 0
}

Upvotes: 16

dimaskpz
dimaskpz

Reputation: 87

if you need cell full of the device screen width. you can use 'UIScreen.main.bounds.width' to get width cell

@IBOutlet weak var imageCollectionView: UICollectionView!

func collectionSetup() {
    let imageFlowLayout = UICollectionViewFlowLayout()
    let imageSize = UIScreen.main.bounds.width
    imageFlowLayout.scrollDirection = .horizontal
    imageFlowLayout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    imageFlowLayout.itemSize = CGSize(width: imageSize, height: imageSize * 2)
    imageFlowLayout.minimumLineSpacing = 0
    imageFlowLayout.minimumInteritemSpacing = 0
    imageFlowLayout.estimatedItemSize = .zero
    imageCollectionView.setCollectionViewLayout(imageFlowLayout, animated: true)
    imageCollectionView.isPagingEnabled = true
}

Upvotes: 0

Roman Aliyev
Roman Aliyev

Reputation: 273

One more implementation with the Compositional Layout.

@IBOutlet var collectionView: UICollectionView!

override func viewDidLoad() {
    super.viewDidLoad()

    // collectionView.dataSource = self
    // collectionView.register(YourCell.self, forCellWithReuseIdentifier: "YourCellReuseIdentifier")
    // collectionView.alwaysBounceVertical = false
    
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalHeight(1.0))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
    let groupSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalHeight(1.0))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .groupPagingCentered
        
    let config = UICollectionViewCompositionalLayoutConfiguration()
    let layout = UICollectionViewCompositionalLayout(section: section, configuration: config)
        
    collectionView.setCollectionViewLayout(layout, animated: false)
}

Implementing Modern Collection Views

UICollectionViewCompositionalLayout

Upvotes: 0

Muhammad Ahmad
Muhammad Ahmad

Reputation: 259

Add Collection View To The Full Screen, Remove Spacing Between Cells and Estimated Size Will be None

enter image description here

Add This Collection View Delegate Method

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
    }

Upvotes: 2

Kavindu Dissanayake
Kavindu Dissanayake

Reputation: 883

jsut add override this delegate

public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 0 }

Upvotes: 2

Daniel Carlos
Daniel Carlos

Reputation: 278

The problem here is that isPagingEnabled doesn't consider the contentSize, but rather the collection bounds:

the scroll view stops on multiples of the scroll view’s bounds when the user scrolls.

To achieve what we want, we need to calculate the next offset ourselves and turn off isPagingEnabled.

enter image description here

As you can see in the image above, the second cell should start at:

One side + Cell - Next cell visible part.

Here's a delegate that calculates just that: https://gist.github.com/danielCarlosCE/7a5f80dc6087773ba147be4dc72da826

Upvotes: 1

Kevin van den Hoek
Kevin van den Hoek

Reputation: 252

If you're after the behaviour of collectionView.isPagingEnabled (so with 'proper inertia feeling' etc) but without wrong offsets when setting a contentInset or spacing, this is what you need:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let itemWidth = cellSize.width + spacing
    let inertialTargetX = targetContentOffset.pointee.x
    let offsetFromPreviousPage = (inertialTargetX + collectionView.contentInset.left).truncatingRemainder(dividingBy: itemWidth)
    
    // snap to the nearest page
    let pagedX: CGFloat
    if offsetFromPreviousPage > itemWidth / 2 {
        pagedX = inertialTargetX + (itemWidth - offsetFromPreviousPage)
    } else {
        pagedX = inertialTargetX - offsetFromPreviousPage
    }
    
    let point = CGPoint(x: pagedX, y: targetContentOffset.pointee.y)
    targetContentOffset.pointee = point
}

Upvotes: 5

lastcc
lastcc

Reputation: 163

The code I just saw from Apple Official Guides and Sample Code:

AssetViewController.swift:

self.collectionView?.isPagingEnabled = true
self.collectionView?.frame = view.frame.insetBy(dx: -20.0, dy: 0.0)

this code enlarges the collection view so that it extends out of the screen, while the content is just within the screen edges

Upvotes: 2

Paolo Musolino
Paolo Musolino

Reputation: 634

Demo here in Swift 3: https://github.com/damienromito/CollectionViewCustom

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let pageWidth = Float(itemWidth + itemSpacing)
    let targetXContentOffset = Float(targetContentOffset.pointee.x)
    let contentWidth = Float(collectionView!.contentSize.width  )
    var newPage = Float(self.pageControl.currentPage)

    if velocity.x == 0 {
        newPage = floor( (targetXContentOffset - Float(pageWidth) / 2) / Float(pageWidth)) + 1.0
    } else {
        newPage = Float(velocity.x > 0 ? self.pageControl.currentPage + 1 : self.pageControl.currentPage - 1)
        if newPage < 0 {
            newPage = 0
        }
        if (newPage > contentWidth / pageWidth) {
            newPage = ceil(contentWidth / pageWidth) - 1.0
        }
    }
    self.pageControl.currentPage = Int(newPage)
    let point = CGPoint (x: CGFloat(newPage * pageWidth), y: targetContentOffset.pointee.y)
    targetContentOffset.pointee = point
}

Swift 4:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let pageWidth = Float(itemWidth + itemSpacing)
    let targetXContentOffset = Float(targetContentOffset.pointee.x)
    let contentWidth = Float(collectionView!.contentSize.width  )
    var newPage = Float(self.pageControl.currentPage)

    if velocity.x == 0 {
        newPage = floor( (targetXContentOffset - Float(pageWidth) / 2) / Float(pageWidth)) + 1.0
    } else {
        newPage = Float(velocity.x > 0 ? self.pageControl.currentPage + 1 : self.pageControl.currentPage - 1)
        if newPage < 0 {
            newPage = 0
        }
        if (newPage > contentWidth / pageWidth) {
            newPage = ceil(contentWidth / pageWidth) - 1.0
        }
    }

    self.pageControl.currentPage = Int(newPage)
    let point = CGPoint (x: CGFloat(newPage * pageWidth), y: targetContentOffset.pointee.y)
    targetContentOffset.pointee = point
}

Upvotes: 43

Aleksey Shevchenko
Aleksey Shevchenko

Reputation: 1241

Swift version of @vlad-che accepted answer:

extension GoodsViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {

        return 10
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        let frameSize = collectionView.frame.size
        return CGSize(width: frameSize.width - 10, height: frameSize.height)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {

        return UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
    }
}

Upvotes: 24

Umesh Verma
Umesh Verma

Reputation: 876

Swift 3

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        if scrollView == self.collectionView {
            var currentCellOffset = self.collectionView.contentOffset
            currentCellOffset.x += self.collectionView.frame.width / 2
            if let indexPath = self.collectionView.indexPathForItem(at: currentCellOffset) {
              self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
            }
        }
    }

Upvotes: 6

Natalia
Natalia

Reputation: 1359

Swift 3 solution based on @Santos's answer, for use if if you have a regular horizontally paging collection view without a page control like Paolo was using in his Swift 3 example.

I used this to solve an issue where a horizontally paging cell full screen cells with a custom UICollectionViewFlowLayout animator didn't finish rotating AND ended up offset so that the the edges of a full screen cell frame were increasingly horizontally off set from the collection view's bounds as you scrolled (like in the video OP shared).

 func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    // Ensure the scrollview is the one on the collectionView we care are working with 
    if (scrollView == self.collectionView) {
        
        // Find cell closest to the frame centre with reference from the targetContentOffset.
        let frameCenter: CGPoint = self.collectionView.center
        var targetOffsetToCenter: CGPoint = CGPoint(x: targetContentOffset.pointee.x + frameCenter.x, y: targetContentOffset.pointee.y + frameCenter.y)
        var indexPath: IndexPath? = self.collectionView.indexPathForItem(at: targetOffsetToCenter)
        
        // Check for "edge case" where the target will land right between cells and then next neighbor to prevent scrolling to index {0,0}.
        while indexPath == nil {
            targetOffsetToCenter.x += 10
            indexPath = self.collectionView.indexPathForItem(at: targetOffsetToCenter)
        }
        // safe unwrap to make sure we found a valid index path
        if let index = indexPath { 
            // Find the centre of the target cell
            if let centerCellPoint: CGPoint = collectionView.layoutAttributesForItem(at: index)?.center {
                
                // Calculate the desired scrollview offset with reference to desired target cell centre.
                let desiredOffset: CGPoint = CGPoint(x: centerCellPoint.x - frameCenter.x, y: centerCellPoint.y - frameCenter.y)
                targetContentOffset.pointee = desiredOffset
            }
        }
    }
}

Upvotes: 1

user6520705
user6520705

Reputation: 705

After having a similar issue, I fixed mine by realizing that when using horizontal scrolling the height is now the width and the width is now the height because the default is set for vertical scrolling. Try switching the values and see if that helps. https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

Upvotes: 1

santos
santos

Reputation: 176

Swift 3.0 set your own UICollectionViewFlowLayout

let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
let width = UIScreen.main.bounds.width
layout.itemSize = CGSize(width: width, height: 154)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.scrollDirection = .horizontal
collectionView?.collectionViewLayout = layout

Upvotes: 5

Dallas Johnson
Dallas Johnson

Reputation: 1536

Being able to have cells that are smaller the collectionView frame with space between the cells allows for hinting to the user that there other cells either side to scroll to which is a big win for UX. But for the centering of the pages doesn't work as expected with each cell progressively becoming more offset as the user scrolls. I've found the following to work well. The centering/snapping animation on each cell is almost invisible to user since it is only tweaking where the collectionView scrolling would end naturally rather than jerking the collectionView to quickly scroll to another indexPath. It's still important to to have the sectionInset property set large enough to allow cell not to stick to the containing frame edges. Also since there are spaces between the cells the target could land on an indexPath of nil which would cause the collectionView to scroll back to the start. I've fixed this offsetting a little and then trying again but different approaches could be taken here.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
                 withVelocity:(CGPoint)velocity
          targetContentOffset:(inout CGPoint *)targetContentOffset
{
    //Ensure the scrollview is the collectionview we care about
    if (scrollView == self.collectionView) {

        // Find cell closest to the frame centre with reference from the targetContentOffset.
        CGPoint frameCentre = self.collectionView.center;
        CGPoint targetOffsetToCentre = CGPointMake((* targetContentOffset).x + frameCentre.x, (* targetContentOffset).y + frameCentre.y);

        NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:targetOffsetToCentre];

            //Check for "edgecase" that the target will land between cells and then find a close neighbour to prevent scrolling to index {0,0}.
        while (!indexPath) {
            targetOffsetToCentre.x += ((UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout).minimumInteritemSpacing;
            indexPath = [self.collectionView indexPathForItemAtPoint:targetOffsetToCentre];
        }

        // Find the centre of the target cell
        CGPoint centreCellPoint = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath].center;

        // Calculate the desired scrollview offset with reference to desired target cell centre.
        CGPoint desiredOffset = CGPointMake(centreCellPoint.x - frameCentre.x, centreCellPoint.y - frameCentre.y);
        *targetContentOffset = desiredOffset;
    }
}

Upvotes: 5

Vlad
Vlad

Reputation: 7260

Remove spaces between items. For horizontal scrolling collection view set minimum line spacing to 0. You can do this with interface builder or with method of UICollectionViewDelegateFlowLayout protocol:

- (CGFloat)collectionView:(UICollectionView *)collectionView 
                   layout:(UICollectionViewLayout *)collectionViewLayout 
        minimumLineSpacingForSectionAtIndex:(NSInteger)section {
    return 0;    
}

enter image description here

Another way is making your cell's width less than collectionView's width for a value of horizontal space between items. Then add section insets with left and right insets that equal a half of horizontal space between items. For example, your minimum line spacing is 10:

- (CGFloat)collectionView:(UICollectionView *)collectionView
                   layout:(UICollectionViewLayout *)collectionViewLayout
        minimumLineSpacingForSectionAtIndex:(NSInteger)section {
    return 10;
}

- (CGSize)collectionView:(UICollectionView *)collectionView 
                  layout:(UICollectionViewLayout *)collectionViewLayout 
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return CGSizeMake(collectionView.frame.size.width - 10, collectionView.frame.size.height);
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView 
                        layout:(UICollectionViewLayout *)collectionViewLayout 
        insetForSectionAtIndex:(NSInteger)section {
    return UIEdgeInsetsMake(0, 5, 0, 5);
}

enter image description here

And third way: manipulate collectionView scroll in scrollViewDidEndDecelerating: method:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    if (scrollView == self.collectionView) {
        CGPoint currentCellOffset = self.collectionView.contentOffset;
        currentCellOffset.x += self.collectionView.frame.size.width / 2;
        NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:currentCellOffset];
        [self.collectionView scrollToItemAtIndexPath:indexPath
                                    atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
                                            animated:YES];
    }
}

enter image description here

Upvotes: 167

Related Questions