Paging UICollectionView według komórek, a nie ekranu


113

Mam UICollectionViewprzewijanie w poziomie i zawsze są 2 komórki obok siebie na całym ekranie. Potrzebuję, aby przewijanie zatrzymało się na początku komórki. Po włączeniu stronicowania widok kolekcji przewija całą stronę, czyli 2 komórki jednocześnie, a następnie zatrzymuje się.

Muszę włączyć przewijanie o pojedynczą komórkę lub przewijanie o wiele komórek z zatrzymaniem na krawędzi komórki.

Próbowałem utworzyć podklasę UICollectionViewFlowLayouti zaimplementować metodę targetContentOffsetForProposedContentOffset, ale do tej pory udało mi się tylko złamać widok kolekcji i przestał się przewijać. Czy istnieje prostszy sposób na osiągnięcie tego i jak, czy naprawdę muszę zaimplementować wszystkie metody UICollectionViewFlowLayoutpodklasy? Dzięki.


1
szerokość twojej kolekcji viewcell musi być równa szerokości screnn, a collectionView Paging włączone
Erhan

Ale muszę pokazać jednocześnie 2 komórki. Jestem na iPadzie, więc każda komórka dzieli połowę ekranu.
Martin Koles,

2
Użyj targetContentOffsetForProposedContentOffset:withScrollingVelocity:i wyłącz stronicowanie
Wain

Właśnie tego próbuję. Jakiś przykład gdzieś?
Martin Koles,

Odpowiedzi:



23

po prostu zastąp metodę:

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

1
Ten fragment kodu bardzo mi pomógł, musiałem jednak dodać kontrolę bieżącego kierunku przewijania i odpowiednio dostosować wartość +/- 0,5.
helkarli

1
Możesz ustawić collectionView.pagingEnabled = true
evya

@evya Wow masz rację. isPagingEnabled pracował dla mnie.
BigSauce,

@evya great stuff !!
Anish Kumar,

Jak działa dla was pagingEnabled? Kopalnia dostaje super-Glitchy przed kończącym się w oryginalnym stronicowania offsetu
Ethan Zhao

17

Stronowanie poziome z niestandardową szerokością strony (Swift 4 i 5)

Wiele przedstawionych tutaj rozwiązań powoduje dziwne zachowanie, które nie przypomina poprawnie zaimplementowanego stronicowania.


Jednak rozwiązanie przedstawione w tym samouczku nie wydaje się mieć żadnych problemów. Po prostu wygląda jak doskonale działający algorytm stronicowania. Możesz to wdrożyć w 5 prostych krokach:

  1. Dodaj następującą właściwość do swojego typu: private var indexOfCellBeforeDragging = 0
  2. Ustaw w collectionView delegateten sposób:collectionView.delegate = self
  3. Dodaj zgodność do UICollectionViewDelegatepoprzez rozszerzenie:extension YourType: UICollectionViewDelegate { }
  4. Dodaj następującą metodę do rozszerzenia implementującego UICollectionViewDelegatezgodność i ustaw wartość dla 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. Dodaj następującą metodę do rozszerzenia implementującego UICollectionViewDelegatezgodność, ustaw tę samą wartość dla pageWidth(możesz również przechowywać tę wartość w centralnym miejscu) i ustaw wartość dla 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)
        }
    }

Dla każdego z zastosowaniem tego trzeba zmienić Pop back (against velocity)część być: collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true). Uwaga.centeredHorizontally
matthew.kempson

@ matthew.kempson Zależy od tego, jak ma się zachowywać układ. Układ, z którym to korzystałem, .leftbył w porządku
fredpi

Okazało się, że .leftnie działa zgodnie z oczekiwaniami. Wydawało się, że przesuwa celę zbyt daleko do tyłu @fredpi
matthew.kempson

13

Szybka 3 wersja odpowiedzi Evyi:

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)


}

Po kliknięciu boku komórki pojawia się dziwne przesunięcie
Maor,

Hej @Maor, nie wiem, czy nadal tego potrzebujesz, ale w moim przypadku naprawiono wyłączenie stronicowania w widoku kolekcji.
Fernando Mata

2
Podobało mi się to, ale czułem się trochę ospale przy szybkich, małych przesunięciach, więc dodałem coś, aby wziąć pod uwagę prędkość i if(velocity.x > 1) { mod = 0.5; } else if(velocity.x < -1) { mod = -0.5; }+ mod+ Double(0.5)
sprawić,

12

Oto najłatwiejszy sposób, w jaki znalazłem to w Swift 4.2 dla przewijania poziomego :

Używam pierwszej komórki visibleCellsi przewijam do tego momentu, jeśli pierwsza widoczna komórka pokazuje mniejszą połowę swojej szerokości, przewijam do następnej.

Jeśli zbiór przewijać w pionie , wystarczy zmienić xprzez yi widthprzezheight

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

Czy możesz dodać link do artykułu źródłowego. Świetna odpowiedź BTW.
Md. Ibrahim Hassan

@ Md.IbrahimHassan Nie ma artykułu, jestem źródłem. Thx
Romulo BM

działa, ale niestety doświadczenie nie jest płynne
Alaa Eddine Cherbib

Co masz na myśli mówiąc nie gładko? Jak dla mnie wynik jest animowany bardzo płynnie .. Zobacz mój wynik tutaj
Romulo BM

1
To działa dobrze
Anuj Kumar Rai

11

Oto moja implementacja w Swift 5 dla pionowego stronicowania opartego na komórkach:

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

Kilka uwag:

  • Nie glitch
  • USTAW PAGING NA FAŁSZ ! (w przeciwnym razie to nie zadziała)
  • Pozwala łatwo ustawić własną prędkość ruchu .
  • Jeśli po wypróbowaniu tego coś nadal nie działa, sprawdź, czy itemSizefaktycznie pasuje on do rozmiaru przedmiotu, ponieważ często stanowi to problem, zwłaszcza podczas używania collectionView(_:layout:sizeForItemAt:), zamiast tego użyj niestandardowej zmiennej z itemSize.
  • Działa to najlepiej, gdy ustawisz self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Oto wersja pozioma (nie przetestowałem jej dokładnie, więc proszę wybaczyć wszelkie błędy):

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

Ten kod jest oparty na kodzie, którego używam w moim osobistym projekcie, możesz go sprawdzić tutaj , pobierając go i uruchamiając przykładowy cel.


1
Dla Swift 5: użyj .fastzamiastUIScollViewDecelerationRateFast
José

Dziękuję za zwrócenie uwagi! Zapomniałem zaktualizować tę odpowiedź i właśnie to zrobiłem!
JoniVR

Cześć, @JoniVR bardzo fajny przykład wyjaśnienia, aby pokazać, jak przesuwanie będzie działać w pionie. Będziesz bardzo miły z twojej strony, jeśli zasugerujesz, jakie ogólne zmiany w kodzie są potrzebne, aby ta praca działała bezbłędnie w kierunku poziomym. Oprócz powyższego kodu zasugerowałeś przesuwanie w poziomie w funkcji przesunięcia zawartości docelowej. Myślę, że trzeba wprowadzić wiele zmian, aby odtworzyć dokładny scenariusz w poziomie. Popraw mnie, jeśli się mylę.
Shiv Prakash

9

Częściowo na podstawie odpowiedzi StevenOjo. Przetestowałem to przy użyciu przewijania poziomego i bez Bounce UICollectionView. cellSize to rozmiar CollectionViewCell. Możesz dostosować współczynnik, aby zmodyfikować czułość przewijania.

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

7

Podejście 1: Widok kolekcji

flowLayoutjest UICollectionViewFlowLayoutwłasnością

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

Podejście 2: Kontroler wyświetleń strony

Możesz użyć, UIPageViewControllerjeśli spełnia Twoje wymagania, każda strona miałaby oddzielny kontroler widoku.


W tym celu muszę wyłączyć stronicowanie i włączyć przewijanie tylko w widoku kolekcji?
nr5

To nie działa w przypadku najnowszego swift4 / Xcode9.3, targetContentOffset nie ma pola pamięci. Zaimplementowałem przewijanie, ale nie dostosowuję lokalizacji komórki, kiedy „pstrykasz”.
Steven B.

Działa z kilkoma komórkami, ale kiedy dotrę do komórki 13, zaczyna przeskakiwać z powrotem do poprzedniej komórki i nie można kontynuować.
Christopher Smit,

4

To jest prosty sposób na zrobienie tego.

Sprawa jest prosta, ale w końcu dość powszechna (typowy scroller miniaturek ze stałym rozmiarem komórki i stałą przerwą między komórkami)

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
}

Zwróć uwagę, że nie ma powodu, aby wywoływać scrollToOffset lub zagłębiać się w układy. Natywne zachowanie przewijania już robi wszystko.

Pozdrawiam wszystkich :)


2
Możesz opcjonalnie ustawić collectionView.decelerationRate = .fastbliższe mimiczne domyślne stronicowanie.
elfanek

1
To jest naprawdę miłe. @elfanek Odkryłem, że ustawienie działa dobrze, chyba że wykonasz małe i delikatne przesunięcie, a potem po prostu wydaje się szybko migotać.
mylogon

3

Coś jak odpowiedź evyi, ale trochę płynniejsza, ponieważ nie ustawia parametru targetContentOffset na 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);
        }
    }
}

3

zmodyfikuj odpowiedź Romulo BM na słuchanie z prędkością

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

2

Szybki 5

Znalazłem sposób, aby to zrobić bez podklasy UICollectionView, po prostu obliczając contentOffset w poziomie. Oczywiście bez isPagingEnabled ustawiono wartość true. Oto kod:

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
}

1

Oto moja wersja w Swift 3. Oblicz przesunięcie po zakończeniu przewijania i dostosuj przesunięcie za pomocą animacji.

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

1

Możesz także utworzyć fałszywy widok przewijania do obsługi przewijania.

Poziomo lub pionowo

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

0

Ok, więc proponowane odpowiedzi nie zadziałały dla mnie, ponieważ zamiast tego chciałem przewijać sekcje, a zatem mieć różne rozmiary stron

Zrobiłem to (tylko w pionie):

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

W tym przypadku każdy rozmiar strony obliczam wcześniej, używając wysokości sekcji + nagłówek + stopka, i przechowuję go w tablicy. To jest pagesSizesczłonek


0

To jest moje rozwiązanie, w Swift 4.2 chciałbym, żeby ci pomogło.

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

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

Nie należy kopiować / wklejać odpowiedzi. W razie potrzeby oznacz je jako duplikat.
DonMag

0

Oryginalna odpowiedź Олень Безрогий zawierała problem, więc w ostatnim widoku zbioru komórek przewijano do początku

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

-1

Oto mój sposób, aby to zrobić, używając UICollectionViewFlowLayoutdo zastąpienia targetContentOffset:

(Chociaż w końcu nie używam tego i zamiast tego używam 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)
  }

}


-1

utworzyłem tutaj niestandardowy układ widoku kolekcji , który obsługuje:

  • stronicowanie po jednej komórce na raz
  • stronicowanie 2+ komórek naraz w zależności od szybkości przesuwania
  • kierunki poziome lub pionowe

to jest tak proste, jak:

let layout = PagingCollectionViewLayout()

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

możesz po prostu dodać PagingCollectionViewLayout.swift do swojego projektu

lub

dodaj pod 'PagingCollectionViewLayout'do swojego pliku podfile

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.