Paginação UICollectionView por células, não por tela

111

Eu tenho UICollectionViewcom rolagem horizontal e sempre há 2 células lado a lado por toda a tela. Preciso que a rolagem pare no início de uma célula. Com a paginação ativada, a visualização da coleção rola a página inteira, que é 2 células de uma vez, e então para.

Preciso habilitar a rolagem por uma única célula ou a rolagem por várias células com parada na borda da célula.

Tentei criar uma subclasse UICollectionViewFlowLayoute implementar o método targetContentOffsetForProposedContentOffset, mas até agora só consegui quebrar a visualização da minha coleção e ela parou de rolar. Existe alguma maneira mais fácil de conseguir isso e como, ou eu realmente preciso implementar todos os métodos da UICollectionViewFlowLayoutsubclasse? Obrigado.

Martin Koles
fonte
1
a largura da sua coleção de visualização da célula deve ser igual a largura do screnn e a página da visualização da coleção ativada
Erhan
Mas preciso mostrar 2 células de uma vez. Estou no iPad, então 2 células compartilham metade da tela cada.
Martin Koles
2
Use targetContentOffsetForProposedContentOffset:withScrollingVelocity:e desligue a paginação
Wain
Isso é o que estou tentando. Algum exemplo em algum lugar?
Martin Koles,

Respostas:

23

apenas substitua o método:

- (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];
}
evya
fonte
1
Esse trecho de código me ajudou muito, mas tive que adicionar a verificação da direção de rolagem atual e adaptei o valor +/- 0,5.
helkarli
1
Você pode definir collectionView.pagingEnabled = true
evya
@evya Uau, você está certo. isPagingEnabled funcionou para mim.
BigSauce
@evya coisas boas !!
Anish Kumar
Como o pagingEnabled está funcionando para vocês? O meu fica super defeituoso antes de terminar no deslocamento de paginação original
Ethan Zhao
17

Paginação horizontal com largura de página personalizada (Swift 4 e 5)

Muitas soluções apresentadas aqui resultam em algum comportamento estranho que não parece uma paginação devidamente implementada.


A solução apresentada neste tutorial , no entanto, não parece ter nenhum problema. Parece um algoritmo de paginação funcionando perfeitamente. Você pode implementá-lo em 5 etapas simples:

  1. Adicione a seguinte propriedade ao seu tipo: private var indexOfCellBeforeDragging = 0
  2. Defina collectionView delegateassim:collectionView.delegate = self
  3. Adicionar conformidade a UICollectionViewDelegatepor meio de uma extensão:extension YourType: UICollectionViewDelegate { }
  4. Adicione o seguinte método à extensão que implementa a UICollectionViewDelegateconformidade e defina um valor para 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. Adicione o seguinte método à extensão que implementa a UICollectionViewDelegateconformidade, defina o mesmo valor para pageWidth(você também pode armazenar esse valor em um local central) e defina um valor para 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)
        }
    }
Fredpi
fonte
Para qualquer pessoa usando isso, você precisa mudar a Pop back (against velocity)parte a ser: collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true). Observe o.centeredHorizontally
matthew.kempson
@ matthew.kempson Depende de como você deseja que o layout se comporte. Para o layout com o qual usei, .leftestava bom
fredpi
Descobri que .leftnão funcionou como esperado. Pareceu empurrar a célula muito para trás @fredpi
matthew.kempson
13

Versão Swift 3 da resposta de 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)


}
StevenOjo
fonte
Quando você clica na lateral da célula, há um deslocamento estranho
Maor
Ei @Maor, não sei se você ainda precisa disso, mas no meu caso isso foi corrigido desativando a paginação na visualização da coleção.
Fernando Mata
2
Eu adorei isso, mas me senti um pouco lento com pequenos deslizes rápidos, então adicionei algo para levar em conta a velocidade e torná-lo muito mais suave: if(velocity.x > 1) { mod = 0.5; } else if(velocity.x < -1) { mod = -0.5; }então adicione + modapós o+ Double(0.5)
Captnwalker1
12

Esta é a maneira mais fácil que encontrei de fazer isso no Swift 4.2 para rolagem horinzontal :

Estou usando a primeira célula visibleCellse rolando para ela, se a primeira célula visível estiver mostrando menos da metade de sua largura, estou rolando para a próxima.

Se sua coleção de rolagem vertical , simplesmente mudar xpor ye widthporheight

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 )
}
Romulo BM
fonte
Você pode adicionar o link para o artigo de origem. Excelente resposta BTW.
Md. Ibrahim Hassan
@ Md.IbrahimHassan Não há nenhum artigo, eu sou a fonte. Thx
Romulo BM
funciona, mas infelizmente a experiência não é
fácil
O que você quer dizer com não suave? Para mim, o resultado é animado muito bom .. Veja meu resultado aqui
Rômulo BM
9

Parcialmente baseado na resposta de StevenOjo. Eu testei isso usando uma rolagem horizontal e sem Bounce UICollectionView. cellSize é o tamanho de CollectionViewCell. Você pode ajustar o fator para modificar a sensibilidade de rolagem.

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)
}
John cido
fonte
9

Esta é minha implementação no Swift 5 para paginação vertical baseada em células:

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

Algumas notas:

  • Não falha
  • DEFINIR PAGING PARA FALSO ! (caso contrário, isso não funcionará)
  • Permite que você defina sua própria velocidade de filme facilmente.
  • Se algo ainda não estiver funcionando depois de tentar fazer isso, verifique se você itemSizerealmente corresponde ao tamanho do item, pois isso costuma ser um problema, especialmente ao usar collectionView(_:layout:sizeForItemAt:), use uma variável personalizada com o itemSize.
  • Isso funciona melhor quando você define self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Aqui está uma versão horizontal (não testei completamente, então, perdoe quaisquer erros):

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

Este código é baseado no código que uso em meu projeto pessoal, você pode conferi-lo aqui baixando-o e executando o destino Exemplo.

JoniVR
fonte
1
Para Swift 5: use em .fastvez deUIScollViewDecelerationRateFast
José
Obrigado por apontar isso! Esqueci de atualizar esta resposta e acabou de fazer!
JoniVR
Olá, @JoniVR é um exemplo de explicação muito bom para mostrar como o deslizamento funcionará verticalmente. Será muito gentil de sua parte sugerir quais mudanças gerais no código são necessárias para fazer este trabalho perfeitamente em uma direção horizontal. Além do código acima, você sugeriu o deslizar horizontalmente na função de deslocamento de conteúdo alvo. Acho que há muitas mudanças a serem feitas para replicar o cenário exato horizontalmente. Corrija-me se eu estiver errada.
Shiv Prakash
7

Abordagem 1: Visualização da coleção

flowLayouté UICollectionViewFlowLayoutpropriedade

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

Abordagem 2: Controlador de visualização de página

Você pode usar, UIPageViewControllerse atender aos seus requisitos, cada página terá um controlador de visualização separado.

user1046037
fonte
Para isso, tenho que desabilitar a paginação e habilitar a rolagem apenas na visualização da coleção?
nr5
Isso não está funcionando para o swift4 / Xcode9.3 mais recente, targetContentOffset não tem um campo de memória. Eu implementei a rolagem, mas ela não está ajustando a localização da célula quando você "dá um toque".
Steven B.
Funciona com algumas células, mas quando chego à célula 13, ela começa a pular para a célula anterior e não dá para continuar.
Christopher Smit
4

Esta é uma maneira direta de fazer isso.

O caso é simples, mas finalmente bastante comum (scroller de miniaturas típico com tamanho de célula fixo e lacuna fixa entre as células)

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
}

Observe que não há razão para chamar um scrollToOffset ou mergulhar nos layouts. O comportamento de rolagem nativo já faz tudo.

Saúde a todos :)

alce
fonte
2
Você pode, opcionalmente, definir collectionView.decelerationRate = .fastpara paginação padrão mimmic mais próxima.
elfanek
1
Isso é muito bom. @elfanek Descobri que essa configuração funciona bem, a menos que você deslize levemente e com cuidado, então ela começa a piscar rapidamente.
mylogon de
3

Mais ou menos como a resposta de evya, mas um pouco mais suave porque não define targetContentOffset como zero.

- (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);
        }
    }
}
Skensell
fonte
2

Swift 5

Eu descobri uma maneira de fazer isso sem subclassificar UICollectionView, apenas calculando o contentOffset na horizontal. Obviamente, sem isPagingEnabled definido como true. Aqui está o código:

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
}
carlosobedgomez
fonte
1

Aqui está minha versão em Swift 3. Calcule o deslocamento após o término da rolagem e ajuste o deslocamento com animação.

collectionLayout é um 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)
}
Misha Kouznetsov
fonte
1

Além disso, você pode criar uma exibição de rolagem falsa para lidar com a rolagem.

Horizontal ou vertical

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

modificar a resposta Romulo BM para escuta de velocidade

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 )
}
Олень Безрогий
fonte
0

Ok, então as respostas propostas não funcionaram para mim porque eu queria rolar por seções em vez disso e, portanto, ter tamanhos de página de largura variável

Eu fiz isso (apenas vertical):

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

Neste caso, eu calculo cada tamanho de página com antecedência usando a altura da seção + cabeçalho + rodapé, e armazeno no array. Esse é o pagesSizesmembro

Antzi
fonte
0

Esta é a minha solução, no Swift 4.2, gostaria que pudesse ajudá-lo.

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)
  }
}
Leo
fonte
0
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)
    }
}
Mecid
fonte
Você não deve copiar / colar respostas. Marque-o como duplicado, se apropriado.
DonMag
-1

Aqui está minha maneira de fazer isso usando um UICollectionViewFlowLayoutpara substituir o targetContentOffset:

(Embora no final, eu acabe não usando isso e use UIPageViewController em vez disso.)

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

}
Hlung
fonte
-1

Eu criei um layout de exibição de coleção personalizado aqui que suporta:

  • paging uma célula de cada vez
  • paging 2+ células por vez, dependendo da velocidade de deslize
  • direções horizontais ou verticais

é tão fácil quanto:

let layout = PagingCollectionViewLayout()

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

você pode simplesmente adicionar PagingCollectionViewLayout.swift ao seu projeto

ou

adicione pod 'PagingCollectionViewLayout'ao seu podfile

ak.
fonte