ChatGPT解决这个技术问题 Extra ChatGPT

按单元格而不是屏幕分页 UICollectionView

我有 UICollectionView 与水平滚动,并且在整个屏幕上总是有 2 个并排的单元格。我需要滚动停止在单元格的开头。启用分页后,集合视图会滚动整个页面,一次是 2 个单元格,然后停止。

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

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

您的 collectionviewcell 宽度必须等于 screnn 宽度并且启用了 collectionView 分页
但我需要一次显示 2 个单元格。我在 iPad 上,所以 2 个单元格各共享一半屏幕。
使用 targetContentOffsetForProposedContentOffset:withScrollingVelocity: 并关闭分页
这就是我正在尝试的。有什么例子吗?
@MohammadBashirSidani 不,它没有

C
Community

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

我应该在一开始就搜索 targetContentOffsetForProposedContentOffset


对于在 swift 5 中询问此问题的任何人:collectionView.isPagingEnabled = true 这样做!
@Mohammad Bashir Sidani 不,我没有。阅读问题。
e
evya

只需覆盖该方法:

- (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];
}

那段代码对我帮助很大,但我不得不添加对当前滚动方向的检查,并相应地调整了 +/- 0.5 值。
您可以设置 collectionView.pagingEnabled = true
@evya哇,你是对的。 isPagingEnabled 为我工作。
@evya 好东西!!
pagingEnabled 如何为你们工作?在以原始分页偏移结束之前,我的会出现超级故障
J
JoniVR

这是我在 Swift 5 中针对基于单元的垂直分页的实现:

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)
}

一些注意事项:

不会出错

设置分页为假! (否则这将不起作用)

允许您轻松设置自己的轻弹速度。

如果尝试此操作后仍然无法正常工作,请检查您的 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


对于 Swift 5:使用 .fast 而不是 UIScollViewDecelerationRateFast
感谢您指出了这一点!忘了更新这个答案,只是做了!
嗨,@JoniVR 非常好的解释示例,展示了滑动如何垂直工作。非常好心地建议您需要对整体代码进行哪些更改才能使这项工作在水平方向上完美无缺。除了您建议的目标内容偏移功能中的水平滑动代码之外。我认为要横向复制确切的场景需要进行很多更改。如果我错了,请纠正我。
f
fredpi

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

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

但是,this tutorial 中提出的解决方案似乎没有任何问题。感觉就像一个完美的分页算法。您可以通过 5 个简单的步骤来实现它:

将以下属性添加到您的类型: private var indexOfCellBeforeDragging = 0 像这样设置 collectionView 委托: collectionView.delegate = self 通过扩展添加对 UICollectionViewDelegate 的一致性: extension YourType: UICollectionViewDelegate { } 将以下方法添加到实现 UICollectionViewDelegate 一致性的扩展并为 pageWidth 设置一个值: func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { let pageWidth = // 你的页面应该有的宽度(加上可能的边距) let ratioOffset = collectionView.contentOffset.x / pageWidth indexOfCellBeforeDragging = Int(round(proportionalOffset )) } 将以下方法添加到实现 UICollectionViewDelegate 一致性的扩展中,为 pageWidth 设置相同的值(您也可以将此值存储在中心位置)并为 collectionViewItemCount 设置一个值: func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity :CGPoint,目标内容偏移: UnsafeMutablePointer) { // 停止滚动 targetContentOffset.pointee = scrollView.contentOffset // 计算条件 let pageWidth = // 你的页面应该有的宽度(加上可能的边距) let collectionViewItemCount = // 本节中的项目数让proportionalOffset = collectionView.contentOffset.x / pageWidth 让indexOfMajorCell = Int(round(proportionalOffset)) 让swipeVelocityThreshold: CGFloat = 0.5 让hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 swipeVelocityThreshold 让hasEnoughVelocityToSlideToThePreviousCell = indexOfCell - &&BeforeDragging1=0 velocity.x < -swipeVelocityThreshold let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell) if didUseSwipeToSkipCell { // 设置动画以便继续滑动 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 { // 弹回(反对速度) 让 indexPath = IndexPath(row: indexOfMajorCell, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, 动画: true) } }


对于使用它的任何人,您需要将 Pop back (against velocity) 部分更改为:collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)。注意 .centeredHorizontally
@matthew.kempson 取决于您希望布局的行为方式。对于我使用的布局,.left 很好
我发现 .left 没有按预期工作。它似乎把细胞推得太远了@fredpi
R
Romulo BM

这是我在 Swift 4.2 中发现的用于水平滚动的最简单方法:

我正在使用 visibleCells 上的第一个单元格并滚动到该单元格,如果第一个可见单元格显示的宽度小于其宽度的一半,我将滚动到下一个单元格。

如果您的收藏垂直滚动,只需将 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 )
}

能否请您将链接添加到源文章。顺便说一句,答案很好。
@Md.IbrahimHassan 没有文章,我是来源。谢谢
它有效,但不幸的是体验并不顺畅
不流畅是什么意思?对我来说,结果动画非常流畅。请看我的结果here
S
StevenOjo

Evya 回答的 Swift 3 版本:

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)


}

当您单击单元格的一侧时,会有一个奇怪的偏移量
嘿@Maor,我不知道你是否还需要它,但在我的情况下,这是修复了在集合视图上禁用分页。
我喜欢这个,但快速的小滑动感觉有点迟钝,所以我添加了一些考虑速度并使其更流畅的内容:if(velocity.x > 1) { mod = 0.5; } else if(velocity.x < -1) { mod = -0.5; } 然后在 + Double(0.5) 之后添加 + mod
J
John Cido

部分基于 StevenOjo 的回答。我已经使用水平滚动和没有 Bounce 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)
}

u
user1046037

方法一:集合视图

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)
    }
}

方法 2:页面视图控制器

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


为此,我必须禁用分页并仅在 collectionview 中启用滚动?
这不适用于最新的 swift4/Xcode9.3,targetContentOffset 没有内存字段。我实现了滚动,但是当您“轻弹”时它没有调整单元格位置。
适用于几个单元格,但是当我到达单元格 13 时,它开始跳回上一个单元格,您无法继续。
О
Олень Безрогий

修改 Romulo BM 答案以进行速度监听

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 )
}

M
Michael Lin Liu

这是 Swift5 中的优化解决方案,包括处理错误的 indexPath。 - 迈克尔林刘

步骤1。获取当前单元格的 indexPath。

第2步。检测滚动时的速度。

第三步。当速度增加时增加 indexPath 的行。

第4步。告诉集合视图滚动到下一个项目

    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 )
 }

M
Moose

这是执行此操作的直接方法。

案例很简单,但最后很常见(典型的缩略图滚动器,具有固定的单元格大小和固定的单元格之间的间隙)

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
}

请注意,没有理由调用 scrollToOffset 或深入布局。本机滚动行为已经完成了一切。

祝大家好运:)


您可以选择将 collectionView.decelerationRate = .fast 设置为更接近模仿的默认分页。
这真是太好了。 @elfanek 我发现该设置可以正常工作,除非您进行小而轻柔的滑动,否则它似乎会快速闪烁。
s
skensell

有点像 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);
        }
    }
}

M
Martin Koles

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

collectionLayout 是一个 UICollectionViewFlowLayout()

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)
}

c
carlosobedgomez

斯威夫特 5

我找到了一种无需子类化 UICollectionView 的方法,只需计算水平方向的 contentOffset。显然没有 isPagingEnabled 设置为 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
}

i
icompot

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

水平或垂直

// === 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)
}

L
Leo

这是我的解决方案,在 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)
  }
}

A
Arthur Avagyan

Олень Безрогий 的原始答案有问题,所以在最后一个单元格集合视图上滚动到开头

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 )
}

A
Antzi

好的,所以建议的答案对我不起作用,因为我想按部分滚动,因此页面大小可变

我这样做了(仅限垂直):

   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 成员


a
ak.

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

一次分页一个单元格

根据滑动速度一次分页 2+ 个单元格

水平或垂直方向

这很简单:

let layout = PagingCollectionViewLayout()

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

您只需将 PagingCollectionViewLayout.swift 添加到您的项目中

或者

pod 'PagingCollectionViewLayout' 添加到您的 podfile


很好,但不确定为什么它在垂直滚动集合视图中仅适用于单元格高度 600。尝试了其他高度值,但它会比项目滚动更多或更少。
M
Mecid
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)
    }
}

您不应该复制/粘贴答案。如果合适,将其标记为重复。
佚名
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 )
}

请不要只发布代码作为答案,还要解释您的代码的作用以及它如何解决问题的问题。带有解释的答案通常更有帮助,质量更好,并且更有可能吸引赞成票。
B
Booharin

这是我见过的最好的解决方案。只需将它与 .linear 类型一起使用。 https://github.com/nicklockwood/iCarousel 上帝保佑作者!:)


H
Hlung

这是我使用 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)
  }

}

l
levan

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

这非常简单,您不需要考虑其他答案所包含的细节。