按单元格而非屏幕分页 UICollectionView

我有 UICollectionView与水平滚动,总是有2个单元格并排每整个屏幕。我需要在细胞开始时停止滚动。启用分页后,集合视图将同时滚动整个页面(即两个单元格) ,然后停止。

我需要启用单个单元格的滚动,或者在单元格边缘停止的多个单元格的滚动。

我尝试子类 UICollectionViewFlowLayout并实现方法 targetContentOffsetForProposedContentOffset,但到目前为止,我只能中断我的集合视图,它停止了滚动。有没有更简单的方法来实现这一点,或者我真的需要实现 UICollectionViewFlowLayout子类的所有方法?谢谢。

130608 次浏览

好的,我在这里找到了解决方案: targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout

I should have searched for targetContentOffsetForProposedContentOffset in the begining.

just override the method:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
*targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0
float pageWidth = (float)self.articlesCollectionView.bounds.size.width;
int minSpace = 10;


int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines
if (cellToSwipe < 0) {
cellToSwipe = 0;
} else if (cellToSwipe >= self.articles.count) {
cellToSwipe = self.articles.count - 1;
}
[self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}

Approach 1: Collection View

flowLayoutUICollectionViewFlowLayout属性

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


if let collectionView = collectionView {


targetContentOffset.memory = scrollView.contentOffset
let pageWidth = CGRectGetWidth(scrollView.frame) + flowLayout.minimumInteritemSpacing


var assistanceOffset : CGFloat = pageWidth / 3.0


if velocity.x < 0 {
assistanceOffset = -assistanceOffset
}


let assistedScrollPosition = (scrollView.contentOffset.x + assistanceOffset) / pageWidth


var targetIndex = Int(round(assistedScrollPosition))




if targetIndex < 0 {
targetIndex = 0
}
else if targetIndex >= collectionView.numberOfItemsInSection(0) {
targetIndex = collectionView.numberOfItemsInSection(0) - 1
}


print("targetIndex = \(targetIndex)")


let indexPath = NSIndexPath(forItem: targetIndex, inSection: 0)


collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: true)
}
}

Approach 2: Page View Controller

您可以使用 UIPageViewController,如果它满足您的要求,每个页面将有一个单独的视图控制器。

有点像 Evya 的答案,但是更平滑一些,因为它没有将 targetContentOffset 设置为零。

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if ([scrollView isKindOfClass:[UICollectionView class]]) {
UICollectionView* collectionView = (UICollectionView*)scrollView;
if ([collectionView.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) {
UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout;


CGFloat pageWidth = layout.itemSize.width + layout.minimumInteritemSpacing;
CGFloat usualSideOverhang = (scrollView.bounds.size.width - pageWidth)/2.0;
// k*pageWidth - usualSideOverhang = contentOffset for page at index k if k >= 1, 0 if k = 0
// -> (contentOffset + usualSideOverhang)/pageWidth = k at page stops


NSInteger targetPage = 0;
CGFloat currentOffsetInPages = (scrollView.contentOffset.x + usualSideOverhang)/pageWidth;
targetPage = velocity.x < 0 ? floor(currentOffsetInPages) : ceil(currentOffsetInPages);
targetPage = MAX(0,MIN(self.projects.count - 1,targetPage));


*targetContentOffset = CGPointMake(MAX(targetPage*pageWidth - usualSideOverhang,0), 0);
}
}
}

这是我在 Swift 3中的版本。计算滚动结束后的偏移量,并用动画调整偏移量。

collectionLayoutUICollectionViewFlowLayout()

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let index = scrollView.contentOffset.x / collectionLayout.itemSize.width
let fracPart = index.truncatingRemainder(dividingBy: 1)
let item= Int(fracPart >= 0.5 ? ceil(index) : floor(index))


let indexPath = IndexPath(item: item, section: 0)
collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
}

迅捷3版本的 Evya 回答:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
let pageWidth:Float = Float(self.view.bounds.width)
let minSpace:Float = 10.0
var cellToSwipe:Double = Double(Float((scrollView.contentOffset.x))/Float((pageWidth+minSpace))) + Double(0.5)
if cellToSwipe < 0 {
cellToSwipe = 0
} else if cellToSwipe >= Double(self.articles.count) {
cellToSwipe = Double(self.articles.count) - Double(1)
}
let indexPath:IndexPath = IndexPath(row: Int(cellToSwipe), section:0)
self.collectionView.scrollToItem(at:indexPath, at: UICollectionViewScrollPosition.left, animated: true)




}

部分是基于史蒂文 · 奥乔的回答。 我已经使用水平滚动和没有弹出 UICollectionView 来测试了这一点。CellSize 是 CollectionViewCell 大小。您可以调整因子来修改滚动灵敏度。

override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
var factor: CGFloat = 0.5
if velocity.x < 0 {
factor = -factor
}
let indexPath = IndexPath(row: (scrollView.contentOffset.x/cellSize.width + factor).int, section: 0)
collectionView?.scrollToItem(at: indexPath, at: .left, animated: true)
}

Here's my implementation in Swift 5 for 垂直的 cell-based paging:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {


guard let collectionView = self.collectionView else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}


// Page height used for estimating and calculating paging.
let pageHeight = self.itemSize.height + self.minimumLineSpacing


// Make an estimation of the current page position.
let approximatePage = collectionView.contentOffset.y/pageHeight


// Determine the current page based on velocity.
let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage))


// Create custom flickVelocity.
let flickVelocity = velocity.y * 0.3


// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)


let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top


return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}

一些注意事项:

  • Doesn't glitch
  • 将分页设置为 FALSE (否则无法工作)
  • 允许您轻松设置 your own flickvelocity
  • 如果在尝试这种方法之后仍然无法工作,检查 itemSize是否真的匹配项的大小,因为这经常是一个问题,特别是在使用 collectionView(_:layout:sizeForItemAt:)时,使用 itemSize 自定义变量代替。
  • 这在设置 self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast时效果最好。

这里有一个 水平版本(还没有彻底测试过,所以请原谅任何错误) :

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {


guard let collectionView = self.collectionView else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}


// Page width used for estimating and calculating paging.
let pageWidth = self.itemSize.width + self.minimumInteritemSpacing


// Make an estimation of the current page position.
let approximatePage = collectionView.contentOffset.x/pageWidth


// Determine the current page based on velocity.
let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage))


// Create custom flickVelocity.
let flickVelocity = velocity.x * 0.3


// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)


// Calculate newHorizontalOffset.
let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - collectionView.contentInset.left


return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}

此代码是基于我在个人项目中使用的代码,您可以通过下载并运行示例目标来检出 here

这是个直接的方法。

这种情况很简单,但最终相当常见(典型的缩略图滚动器具有固定的单元格大小和单元格之间的固定间隔)

var itemCellSize: CGSize = <your cell size>
var itemCellsGap: CGFloat = <gap in between>


override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageWidth = (itemCellSize.width + itemCellsGap)
let itemIndex = (targetContentOffset.pointee.x) / pageWidth
targetContentOffset.pointee.x = round(itemIndex) * pageWidth - (itemCellsGap / 2)
}


// CollectionViewFlowLayoutDelegate


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


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

Note that there is no reason to call a scrollToOffset or dive into layouts. The native scrolling behaviour already does everything.

干杯

还可以创建假滚动视图来处理滚动。

水平或垂直

// === Defaults ===
let bannerSize = CGSize(width: 280, height: 170)
let pageWidth: CGFloat = 290 // ^ + paging
let insetLeft: CGFloat = 20
let insetRight: CGFloat = 20
// ================


var pageScrollView: UIScrollView!


override func viewDidLoad() {
super.viewDidLoad()


// Create fake scrollview to properly handle paging
pageScrollView = UIScrollView(frame: CGRect(origin: .zero, size: CGSize(width: pageWidth, height: 100)))
pageScrollView.isPagingEnabled = true
pageScrollView.alwaysBounceHorizontal = true
pageScrollView.showsVerticalScrollIndicator = false
pageScrollView.showsHorizontalScrollIndicator = false
pageScrollView.delegate = self
pageScrollView.isHidden = true
view.insertSubview(pageScrollView, belowSubview: collectionView)


// Set desired gesture recognizers to the collection view
for gr in pageScrollView.gestureRecognizers! {
collectionView.addGestureRecognizer(gr)
}
}


func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == pageScrollView {
// Return scrolling back to the collection view
collectionView.contentOffset.x = pageScrollView.contentOffset.x
}
}


func refreshData() {
...


refreshScroll()
}


override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()


refreshScroll()
}


/// Refresh fake scrolling view content size if content changes
func refreshScroll() {
let w = collectionView.width - bannerSize.width - insetLeft - insetRight
pageScrollView.contentSize = CGSize(width: pageWidth * CGFloat(banners.count) - w, height: 100)
}

具有自定义页宽的水平分页(Swift 4 & 5)

这里提供的许多解决方案导致了一些奇怪的行为,感觉不像是正确实现了分页。


然而,本教程中提供的解决方案似乎没有任何问题。感觉就像一个完美的分页算法。你可以通过5个简单的步骤来实现它:

  1. 将以下属性添加到类型中: private var indexOfCellBeforeDragging = 0
  2. 像这样设置 collectionView delegate: collectionView.delegate = self
  3. 通过扩展向 UICollectionViewDelegate添加一致性: extension YourType: UICollectionViewDelegate { }
  4. 将以下方法添加到实现 UICollectionViewDelegate一致性的扩展中,并为 pageWidth设置一个值:

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    let pageWidth = // The width your page should have (plus a possible margin)
    let proportionalOffset = collectionView.contentOffset.x / pageWidth
    indexOfCellBeforeDragging = Int(round(proportionalOffset))
    }
    
  5. Add the following method to the extension implementing the UICollectionViewDelegate conformance, set the same value for pageWidth (you may also store this value at a central place) and set a value for collectionViewItemCount:

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    // Stop scrolling
    targetContentOffset.pointee = scrollView.contentOffset
    
    
    // Calculate conditions
    let pageWidth = // The width your page should have (plus a possible margin)
    let collectionViewItemCount = // The number of items in this section
    let proportionalOffset = collectionView.contentOffset.x / pageWidth
    let indexOfMajorCell = Int(round(proportionalOffset))
    let swipeVelocityThreshold: CGFloat = 0.5
    let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold
    let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold
    let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
    let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)
    
    
    if didUseSwipeToSkipCell {
    // Animate so that swipe is just continued
    let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
    let toValue = pageWidth * CGFloat(snapToIndex)
    UIView.animate(
    withDuration: 0.3,
    delay: 0,
    usingSpringWithDamping: 1,
    initialSpringVelocity: velocity.x,
    options: .allowUserInteraction,
    animations: {
    scrollView.contentOffset = CGPoint(x: toValue, y: 0)
    scrollView.layoutIfNeeded()
    },
    completion: nil
    )
    } else {
    // Pop back (against velocity)
    let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
    collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
    }
    }
    

下面是我使用 UICollectionViewFlowLayout覆盖 targetContentOffset的方法:

(尽管最终我没有使用它,而是使用了 UIPageViewController。)

/**
A UICollectionViewFlowLayout with...
- paged horizontal scrolling
- itemSize is the same as the collectionView bounds.size
*/
class PagedFlowLayout: UICollectionViewFlowLayout {


override init() {
super.init()
self.scrollDirection = .horizontal
self.minimumLineSpacing = 8 // line spacing is the horizontal spacing in horizontal scrollDirection
self.minimumInteritemSpacing = 0
if #available(iOS 11.0, *) {
self.sectionInsetReference = .fromSafeArea // for iPhone X
}
}


required init?(coder aDecoder: NSCoder) {
fatalError("not implemented")
}


// Note: Setting `minimumInteritemSpacing` here will be too late. Don't do it here.
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
collectionView.decelerationRate = UIScrollViewDecelerationRateFast // mostly you want it fast!


let insetedBounds = UIEdgeInsetsInsetRect(collectionView.bounds, self.sectionInset)
self.itemSize = insetedBounds.size
}


// Table: Possible cases of targetContentOffset calculation
// -------------------------
// start |          |
// near  | velocity | end
// page  |          | page
// -------------------------
//   0   | forward  |  1
//   0   | still    |  0
//   0   | backward |  0
//   1   | forward  |  1
//   1   | still    |  1
//   1   | backward |  0
// -------------------------
override func targetContentOffset( //swiftlint:disable:this cyclomatic_complexity
forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {


guard let collectionView = collectionView else { return proposedContentOffset }


let pageWidth = itemSize.width + minimumLineSpacing
let currentPage: CGFloat = collectionView.contentOffset.x / pageWidth
let nearestPage: CGFloat = round(currentPage)
let isNearPreviousPage = nearestPage < currentPage


var pageDiff: CGFloat = 0
let velocityThreshold: CGFloat = 0.5 // can customize this threshold
if isNearPreviousPage {
if velocity.x > velocityThreshold {
pageDiff = 1
}
} else {
if velocity.x < -velocityThreshold {
pageDiff = -1
}
}


let x = (nearestPage + pageDiff) * pageWidth
let cappedX = max(0, x) // cap to avoid targeting beyond content
//print("x:", x, "velocity:", velocity)
return CGPoint(x: cappedX, y: proposedContentOffset.y)
}


}

Ok so the proposed answers did'nt worked for me because I wanted to scroll by sections instead, and thus, have variable width page sizes

I did this (vertical only):

   var pagesSizes = [CGSize]()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
defer {
lastOffsetY = scrollView.contentOffset.y
}
if collectionView.isDecelerating {
var currentPage = 0
var currentPageBottom = CGFloat(0)
for pagesSize in pagesSizes {
currentPageBottom += pagesSize.height
if currentPageBottom > collectionView!.contentOffset.y {
break
}
currentPage += 1
}
if collectionView.contentOffset.y > currentPageBottom - pagesSizes[currentPage].height, collectionView.contentOffset.y + collectionView.frame.height < currentPageBottom {
return // 100% of view within bounds
}
if lastOffsetY < collectionView.contentOffset.y {
if currentPage + 1 != pagesSizes.count {
collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom), animated: true)
}
} else {
collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom - pagesSizes[currentPage].height), animated: true)
}
}
}

在这种情况下,我预先使用部分高度 + 页眉 + 页脚计算每个页面的大小,并将其存储在数组中。那是 pagesSizes成员

下面是我在 Swift 4.2中找到的用于 水平滚动的最简单的方法:

我使用 visibleCells上的第一个单元格,然后滚动到,如果第一个可见单元格显示的宽度小于它的一半,我就滚动到下一个单元格。

如果您的集合滚动 vertically,只需将 x改为 y,将 width改为 height

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
var indexes = self.collectionView.indexPathsForVisibleItems
indexes.sort()
var index = indexes.first!
let cell = self.collectionView.cellForItem(at: index)!
let position = self.collectionView.contentOffset.x - cell.frame.origin.x
if position > cell.frame.size.width/2{
index.row = index.row+1
}
self.collectionView.scrollToItem(at: index, at: .left, animated: true )
}

可以使用以下库: https://github.com/ink-spot/UPCarouselFlowLayout

它非常简单,你不需要像其他答案一样考虑细节。

这是我的解决方案,在 Swift 4.2中,我希望它能帮到你。

class SomeViewController: UIViewController {


private lazy var flowLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: /* width */, height: /* height */)
layout.minimumLineSpacing = // margin
layout.minimumInteritemSpacing = 0.0
layout.sectionInset = UIEdgeInsets(top: 0.0, left: /* margin */, bottom: 0.0, right: /* margin */)
layout.scrollDirection = .horizontal
return layout
}()


private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.showsHorizontalScrollIndicator = false
collectionView.dataSource = self
collectionView.delegate = self
// collectionView.register(SomeCell.self)
return collectionView
}()


private var currentIndex: Int = 0
}


// MARK: - UIScrollViewDelegate


extension SomeViewController {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
guard scrollView == collectionView else { return }


let pageWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
currentIndex = Int(scrollView.contentOffset.x / pageWidth)
}


func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard scrollView == collectionView else { return }


let pageWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
var targetIndex = Int(roundf(Float(targetContentOffset.pointee.x / pageWidth)))
if targetIndex > currentIndex {
targetIndex = currentIndex + 1
} else if targetIndex < currentIndex {
targetIndex = currentIndex - 1
}
let count = collectionView.numberOfItems(inSection: 0)
targetIndex = max(min(targetIndex, count - 1), 0)
print("targetIndex: \(targetIndex)")


targetContentOffset.pointee = scrollView.contentOffset
var offsetX: CGFloat = 0.0
if targetIndex < count - 1 {
offsetX = pageWidth * CGFloat(targetIndex)
} else {
offsetX = scrollView.contentSize.width - scrollView.width
}
collectionView.setContentOffset(CGPoint(x: offsetX, y: 0.0), animated: true)
}
}

我创建了一个自定义集合视图布局 给你,它支持:

  • 一次呼叫一个细胞
  • 根据滑动速度,每次分页2 + 个单元格
  • 水平或垂直方向

这很简单:

let layout = PagingCollectionViewLayout()


layout.itemSize =
layout.minimumLineSpacing =
layout.scrollDirection =

您可以将 PagingCollectionViewLayout.swift添加到您的项目中

或者

pod 'PagingCollectionViewLayout'添加到您的 podfile

final class PagingFlowLayout: UICollectionViewFlowLayout {
private var currentIndex = 0


override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let count = collectionView!.numberOfItems(inSection: 0)
let currentAttribute = layoutAttributesForItem(
at: IndexPath(item: currentIndex, section: 0)
) ?? UICollectionViewLayoutAttributes()


let direction = proposedContentOffset.x > currentAttribute.frame.minX
if collectionView!.contentOffset.x + collectionView!.bounds.width < collectionView!.contentSize.width || currentIndex < count - 1 {
currentIndex += direction ? 1 : -1
currentIndex = max(min(currentIndex, count - 1), 0)
}


let indexPath = IndexPath(item: currentIndex, section: 0)
let closestAttribute = layoutAttributesForItem(at: indexPath) ?? UICollectionViewLayoutAttributes()


let centerOffset = collectionView!.bounds.size.width / 2
return CGPoint(x: closestAttribute.center.x - centerOffset, y: 0)
}
}

Swift 5

我已经找到了一种不用子类化 UICollectionView 的方法,只需在水平方向上计算 contentOffset。显然没有 isPagingEnable 设置为 true。 密码如下:

var offsetScroll1 : CGFloat = 0
var offsetScroll2 : CGFloat = 0
let flowLayout = UICollectionViewFlowLayout()
let screenSize : CGSize = UIScreen.main.bounds.size
var items = ["1", "2", "3", "4", "5"]


override func viewDidLoad() {
super.viewDidLoad()
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = 7
let collectionView = UICollectionView(frame: CGRect(x: 0, y: 590, width: screenSize.width, height: 200), collectionViewLayout: flowLayout)
collectionView.register(collectionViewCell1.self, forCellWithReuseIdentifier: cellReuseIdentifier)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = UIColor.clear
collectionView.showsHorizontalScrollIndicator = false
self.view.addSubview(collectionView)
}


func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
offsetScroll1 = offsetScroll2
}


func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
offsetScroll1 = offsetScroll2
}


func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>){
let indexOfMajorCell = self.desiredIndex()
let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
flowLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
targetContentOffset.pointee = scrollView.contentOffset
}


private func desiredIndex() -> Int {
var integerIndex = 0
print(flowLayout.collectionView!.contentOffset.x)
offsetScroll2 = flowLayout.collectionView!.contentOffset.x
if offsetScroll2 > offsetScroll1 {
integerIndex += 1
let offset = flowLayout.collectionView!.contentOffset.x / screenSize.width
integerIndex = Int(round(offset))
if integerIndex < (items.count - 1) {
integerIndex += 1
}
}
if offsetScroll2 < offsetScroll1 {
let offset = flowLayout.collectionView!.contentOffset.x / screenSize.width
integerIndex = Int(offset.rounded(.towardZero))
}
let targetIndex = integerIndex
return targetIndex
}

modify Romulo BM answer for velocity listening

func scrollViewWillEndDragging(
_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>
) {
targetContentOffset.pointee = scrollView.contentOffset
var indexes = collection.indexPathsForVisibleItems
indexes.sort()
var index = indexes.first!
if velocity.x > 0 {
index.row += 1
} else if velocity.x == 0 {
let cell = self.collection.cellForItem(at: index)!
let position = self.collection.contentOffset.x - cell.frame.origin.x
if position > cell.frame.size.width / 2 {
index.row += 1
}
}


self.collection.scrollToItem(at: index, at: .centeredHorizontally, animated: true )
}

原来的答案有问题,所以在最后一个单元格集合视图上滚动到开始

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
var indexes = yourCollectionView.indexPathsForVisibleItems
indexes.sort()
var index = indexes.first!
// if velocity.x > 0 && (Get the number of items from your data) > index.row + 1 {
if velocity.x > 0 && yourCollectionView.numberOfItems(inSection: 0) > index.row + 1 {
index.row += 1
} else if velocity.x == 0 {
let cell = yourCollectionView.cellForItem(at: index)!
let position = yourCollectionView.contentOffset.x - cell.frame.origin.x
if position > cell.frame.size.width / 2 {
index.row += 1
}
}
    

yourCollectionView.scrollToItem(at: index, at: .centeredHorizontally, animated: true )
}

以下是 Swift5中的优化解决方案,包括处理错误的 indexPath

  • Step1获取当前单元格的 indexPath。
  • 第二步,在滚动时检测速度。
  • Step3当速度增加时增加索引路径的行。
  • Step4. Tell the collection view to scroll to the next item
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        

targetContentOffset.pointee = scrollView.contentOffset
        

//M: Get the first visiable item's indexPath from visibaleItems.
var indexPaths = *YOURCOLLECTIONVIEW*.indexPathsForVisibleItems
indexPaths.sort()
var indexPath = indexPaths.first!


//M: Use the velocity to detect the paging control movement.
//M: If the movement is forward, then increase the indexPath.
if velocity.x > 0{
indexPath.row += 1
            

//M: If the movement is in the next section, which means the indexPath's row is out range. We set the indexPath to the first row of the next section.
if indexPath.row == *YOURCOLLECTIONVIEW*.numberOfItems(inSection: indexPath.section){
indexPath.row = 0
indexPath.section += 1
}
}
else{
//M: If the movement is backward, the indexPath will be automatically changed to the first visiable item which is indexPath.row - 1. So there is no need to write the logic.
}
        

//M: Tell the collection view to scroll to the next item.
*YOURCOLLECTIONVIEW*.scrollToItem(at: indexPath, at: .left, animated: true )
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity
velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
var indexes = self.collectionHome.indexPathsForVisibleItems
indexes.sort()
var index = indexes.first!
let cell = self.collectionHome.cellForItem(at: index)!
let position = self.collectionHome.contentOffset.x - cell.frame.origin.x
if position > cell.frame.size.width/2{
index.row = index.row+1
}
self.collectionHome.scrollToItem(at: index, at: .left, animated: true )
}

这是我见过的最好的解决方案。只要使用它与 .linear类型。 Https://github.com/nicklockwood/icarousel 上帝保佑作者! :)