Paging UICollectionView par cellules, pas par écran

113

J'ai UICollectionViewavec le défilement horizontal et il y a toujours 2 cellules côte à côte pour tout l'écran. J'ai besoin que le défilement s'arrête au début d'une cellule. Lorsque la pagination est activée, la vue de collection fait défiler toute la page, soit 2 cellules à la fois, puis s'arrête.

J'ai besoin d'activer le défilement par une seule cellule, ou le défilement par plusieurs cellules avec arrêt au bord de la cellule.

J'ai essayé de sous UICollectionViewFlowLayout- classer et d'implémenter la méthode targetContentOffsetForProposedContentOffset, mais jusqu'à présent, je n'ai pu que casser la vue de ma collection et le défilement a cessé. Existe-t-il un moyen plus simple d'y parvenir et comment, ou dois-je vraiment implémenter toutes les méthodes de la UICollectionViewFlowLayoutsous-classe? Merci.

Martin Koles
la source
1
la largeur de votre collectionviewcell doit être égale à la largeur de l'écran et à la collectionView Paging enabled
Erhan
Mais je dois montrer 2 cellules à la fois. Je suis sur iPad, donc 2 cellules partagent la moitié de l'écran chacune.
Martin Koles
2
Utiliser targetContentOffsetForProposedContentOffset:withScrollingVelocity:et désactiver la pagination
Wain
C'est ce que j'essaye. Un exemple quelque part?
Martin Koles

Réponses:

23

remplacez simplement la méthode:

- (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
la source
1
Ce morceau de code m'a beaucoup aidé, j'ai dû ajouter le contrôle de la direction de défilement actuelle et adapté en conséquence la valeur +/- 0,5.
helkarli
1
Vous pouvez définir collectionView.pagingEnabled = true
evya
@evya Wow vous avez raison. isPagingEnabled a fonctionné pour moi.
BigSauce
@evya super truc !!
Anish Kumar
Comment fonctionne pagingEnabled pour vous les gars? Le mien devient super glitch avant de se retrouver au décalage de pagination d'origine
Ethan Zhao
17

Pagination horizontale avec largeur de page personnalisée (Swift 4 et 5)

De nombreuses solutions présentées ici entraînent un comportement étrange qui ne ressemble pas à une pagination correctement implémentée.


La solution présentée dans ce didacticiel , cependant, ne semble pas avoir de problèmes. Cela ressemble juste à un algorithme de pagination parfaitement fonctionnel. Vous pouvez l'implémenter en 5 étapes simples:

  1. Ajoutez la propriété suivante à votre type: private var indexOfCellBeforeDragging = 0
  2. Définissez collectionView delegatecomme ceci:collectionView.delegate = self
  3. Ajouter la conformité à UICollectionViewDelegatevia une extension:extension YourType: UICollectionViewDelegate { }
  4. Ajoutez la méthode suivante à l'extension implémentant la UICollectionViewDelegateconformité et définissez une valeur pour 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. Ajoutez la méthode suivante à l'extension implémentant la UICollectionViewDelegateconformité, définissez la même valeur pour pageWidth(vous pouvez également stocker cette valeur à un emplacement central) et définissez une valeur pour 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
la source
Pour toute personne qui utilise cela , vous devez changer la Pop back (against velocity)pièce à: collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true). Remarque le.centeredHorizontally
matthew.kempson
@ matthew.kempson Dépend de la façon dont vous voulez que la mise en page se comporte. Pour la mise en page avec laquelle je l'ai utilisé, .leftc'était bien
fredpi
J'ai trouvé que .leftcela ne fonctionnait pas comme prévu. Cela semblait pousser la cellule trop loin en arrière @fredpi
matthew.kempson
13

Version Swift 3 de la réponse d'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
la source
Lorsque vous cliquez sur le côté de la cellule, il y a un décalage étrange
Maor
Hey @Maor, je ne sais pas si vous en avez toujours besoin, mais dans mon cas, cela a été corrigé de la désactivation de la pagination sur la vue de la collection.
Fernando Mata
2
J'ai adoré cela mais je me sentais un peu lent avec de petits balayages rapides, alors j'ai ajouté quelque chose pour prendre en compte la vitesse et le rendre beaucoup plus fluide: if(velocity.x > 1) { mod = 0.5; } else if(velocity.x < -1) { mod = -0.5; }puis ajoutez + modaprès le+ Double(0.5)
Captnwalker1
12

Voici le moyen le plus simple que j'ai trouvé de faire cela dans Swift 4.2 pour le défilement horizontal :

J'utilise la première cellule visibleCellset je fais défiler jusqu'à alors, si la première cellule visible montre moins de la moitié de sa largeur, je défile vers la suivante.

Si votre collection défile verticalement , changez simplement xpar yet widthparheight

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
la source
Pouvez-vous s'il vous plaît ajouter le lien vers l'article source. Excellente réponse BTW.
Md. Ibrahim Hassan
@ Md.IbrahimHassan Il n'y a pas d'article, j'en suis la source. Thx
Romulo BM
cela fonctionne, mais malheureusement l'expérience n'est pas fluide
Alaa Eddine Cherbib
Que voulez-vous dire par pas lisse? Pour moi, le résultat est animé de façon très fluide .. Voir mon résultat ici
Romulo BM
1
Cela fonctionne bien
Anuj Kumar Rai
10

Voici mon implémentation dans Swift 5 pour la pagination verticale basée sur les cellules:

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

Quelques notes:

  • Ne pique pas
  • RÉGLER LA PAGING SUR FAUX ! (sinon cela ne fonctionnera pas)
  • Vous permet de définir facilement votre propre vitesse de défilement .
  • Si quelque chose ne fonctionne toujours pas après avoir essayé cela, vérifiez si votre itemSizecorrespond réellement à la taille de l'élément car c'est souvent un problème, en particulier lors de l'utilisation collectionView(_:layout:sizeForItemAt:), utilisez plutôt une variable personnalisée avec le itemSize.
  • Cela fonctionne mieux lorsque vous définissez self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Voici une version horizontale (je ne l'ai pas testée à fond, veuillez pardonner toute erreur):

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

Ce code est basé sur le code que j'utilise dans mon projet personnel, vous pouvez le vérifier ici en le téléchargeant et en exécutant la cible Exemple.

JoniVR
la source
1
Pour Swift 5: utiliser à la .fastplace deUIScollViewDecelerationRateFast
José
Merci d'avoir fait remarquer cela! J'ai oublié de mettre à jour cette réponse et je viens de le faire!
JoniVR
Salut, @JoniVR très bel exemple d'explication pour montrer comment le balayage fonctionnera verticalement. Ce sera très gentil de votre part de suggérer les modifications globales du code nécessaires pour que cela fonctionne parfaitement dans une direction horizontale. En dehors du code ci-dessus, vous avez suggéré pour le balayage horizontal dans la fonction de décalage du contenu cible. Je pense qu'il y a beaucoup de changements à apporter pour reproduire le scénario exact horizontalement. Corrigez-moi si je me trompe.
Shiv Prakash
9

En partie basé sur la réponse de StevenOjo. J'ai testé cela en utilisant un défilement horizontal et pas de Bounce UICollectionView. cellSize est la taille de CollectionViewCell. Vous pouvez modifier le facteur pour modifier la sensibilité du défilement.

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
la source
7

Approche 1: Vue de la collection

flowLayoutest la UICollectionViewFlowLayoutpropriété

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

Approche 2: contrôleur d'affichage de page

Vous pouvez utiliser UIPageViewControllersi cela répond à vos exigences, chaque page aurait un contrôleur de vue distinct.

utilisateur1046037
la source
Pour cela, je dois désactiver la pagination et activer le défilement uniquement dans collectionview?
nr5
Cela ne fonctionne pas pour le dernier swift4 / Xcode9.3, targetContentOffset n'a pas de champ mémoire. J'ai implémenté le défilement mais il ne règle pas l'emplacement de la cellule lorsque vous "feuilletez".
Steven B.
Fonctionne avec quelques cellules, mais lorsque j'arrive à la cellule 13, il commence à revenir à la cellule précédente et vous ne pouvez pas continuer.
Christopher Smit
4

C'est une manière directe de le faire.

Le cas est simple, mais finalement assez courant (scroller typique de vignettes avec taille de cellule fixe et espace fixe entre les cellules)

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
}

Notez qu'il n'y a aucune raison d'appeler un scrollToOffset ou de plonger dans les mises en page. Le comportement de défilement natif fait déjà tout.

Salut à tous :)

élan
la source
2
Vous pouvez éventuellement définir collectionView.decelerationRate = .fastune pagination par défaut mimmic plus proche.
elfanek
1
C'est vraiment sympa. @elfanek J'ai trouvé ce paramètre qui fonctionne bien à moins que vous ne fassiez un petit coup doux, il semble juste clignoter rapidement.
mylogon
3

Un peu comme la réponse d'Evya, mais un peu plus fluide car elle ne définit pas targetContentOffset à zéro.

- (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
la source
3

modifier la réponse Romulo BM pour l'écoute de la vitesse

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

Swift 5

J'ai trouvé un moyen de le faire sans sous-classer UICollectionView, en calculant simplement contentOffset en horizontal. De toute évidence, sans isPagingEnabled défini sur true. Voici le code:

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
la source
1

En voici ma version dans Swift 3. Calculez le décalage une fois le défilement terminé et ajustez le décalage avec l'animation.

collectionLayout est un 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
la source
1

Vous pouvez également créer une fausse vue de défilement pour gérer le défilement.

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
la source
0

Ok donc les réponses proposées n'ont pas fonctionné pour moi car je voulais faire défiler les sections à la place, et donc avoir des tailles de page de largeur variable

J'ai fait ceci (vertical uniquement):

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

Dans ce cas, je calcule au préalable chaque taille de page en utilisant la hauteur de section + en-tête + pied de page, et je la stocke dans le tableau. C'est le pagesSizesmembre

Antzi
la source
0

C'est ma solution, dans Swift 4.2, j'aimerais qu'elle puisse vous aider.

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
la source
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
la source
Vous ne devez pas copier / coller les réponses. Marquez-le comme duplicata le cas échéant.
DonMag le
0

La réponse originale de Олень Безрогий avait un problème, donc la dernière vue de la collection de cellules défilait jusqu'au début

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 )
}
Arthur Avagyan
la source
-1

Voici ma façon de le faire en utilisant a UICollectionViewFlowLayoutpour remplacer le targetContentOffset:

(Bien qu'à la fin, je ne l'utilise pas et j'utilise UIPageViewController à la place.)

/**
 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
la source
-1

Vous pouvez utiliser la bibliothèque suivante: https://github.com/ink-spot/UPCarouselFlowLayout

C'est très simple et vous n'avez souvent pas besoin de penser aux détails comme le contiennent les autres réponses.

levan
la source
-1

j'ai créé une mise en page personnalisée collection de vue ici que les supports:

  • pagination d'une cellule à la fois
  • pagination de plus de 2 cellules à la fois en fonction de la vitesse de balayage
  • directions horizontales ou verticales

c'est aussi simple que:

let layout = PagingCollectionViewLayout()

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

vous pouvez simplement ajouter PagingCollectionViewLayout.swift à votre projet

ou

ajouter pod 'PagingCollectionViewLayout'à votre podfile

ak.
la source