简体   繁体   中英

Looping data in UICollectionView (or UITableView) swift

I'm attempting to make a UICollectionView that will scroll indefinitely. Idea being that when you get to the bottom the data array it starts over.

I'm doing this by returning a larger number for numberOfItemsInSection and then doing a % to get the data out of the array.

This works fine which I understand:

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 500
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! PhotoCell

    let index = indexPath.item % photos.count
    let url = photos[index]
}

My question is, is this the best way to achieve this functionality? I've been looking around endlessly online and can't find any other suggestions on how to do it (while using UICollectionView ).

What you have is perfectly fine. Another option is to build a collection that wraps your data source array ( photos ) and offers looped access to its contents:

struct LoopedCollection<Element>: CollectionType {
    let _base: AnyRandomAccessCollection<Element>

    /// Creates a new LoopedCollection that wraps the given collection.
    init<Base: CollectionType where Base.Index: RandomAccessIndexType, Base.Generator.Element == Element>(_ base: Base, withLength length: Int = Int.max) {
        self._base = AnyRandomAccessCollection(base)
        self.endIndex = length
    }

    /// The midpoint of this LoopedCollection, adjusted to match up with
    /// the start of the base collection.
    var startAlignedMidpoint: Int {
        let mid = endIndex / 2
        return mid - mid % numericCast(_base.count)
    }

    // MARK: CollectionType

    let startIndex: Int = 0
    let endIndex: Int

    subscript(index: Int) -> Element {
        precondition(index >= 0, "Index must not be negative.")
        let adjustedIndex = numericCast(index) % _base.count
        return _base[_base.startIndex.advancedBy(adjustedIndex)]
    }
}

You can declare this looping collection alongside your photos array:

let photos: [NSURL] = ...
lazy var loopedPhotos: LoopedCollection<NSURL> = LoopedCollection(self.photos)

And then you can eventually transition your collection view methods to be more generic on the looped collection, or use the looped collection directly:

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return loopedPhotos.count
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! PhotoCell

    let url = loopedPhotos[index]
}

Interesting question! Not a bad approach but the downside is that the size of the scroll bar indicator will be very small.

You could set the number of items to twice the number of actual items, then once the user has scrolled into the second half (and scrolling stopped), re-adjust the data offset and the scroll position, then reload. The user would see no change, but once they went to scroll again the scroller position would seem to have jumped up near the top. I like this since the scroll indicator size will stay reasonable, and the user will actually get some visual feedback that they scrolled past the end and are now repeating.

Your code is the simplest solution. And in most cases it will perfectly fit. If you'd like to implement honest infinity scroll you should create your own layout and cells caching.

you can find more details here

Source : iosnomad 在此输入图像描述

What you need is two things:

  1. Monitor current scroll offset and notify when the user is close to the bottom.
  2. When this threshold is triggered, signal the UICollectionView/UITableView that there are more rows, append new rows to your data source.

To do 1 is fairly trivial, we can simply extend UIScrollView:

extension UIScrollView {
    /**
    A convenience method that returns true when the scrollView is near to the bottom        
    - returns: true when the current contentOffset is close enough to the bottom to merit initiating a load for the next page
    */
    func canStartLoadingNextPage() -> Bool {
        //make sure that we have content and the scrollable area is at least larger than the scroll views bounds.
        if contentOffset.y > 0 && contentSize.height > 0 && (contentOffset.y + CGRectGetHeight(bounds))/contentSize.height > 0.7 
            return true
        }
        return false
    }
}

This function will return true when you reach 70% of the current content size but feel free to tweak as needed.

Now in our cellForRowAtIndexPath we could technically call our function to determine whether we can append our dataset.

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("niceReuseIdentifierName", forIndexPath: indexPath)        

    if collectionView.canStartLoadingNextPage() {
        //now we can append our data
        self.loadNextPage()
    }

    return cell
}

func loadNextPage() {
    self.collectionView.performBatchUpdates({ () -> Void in
        let nextBatch = self.pictures//or however you get the next batch
        self.pictures.appendContentsOf(nextBatch)
        self.collectionView.reloadSections(NSIndexSet(index: 0))
        }, completion: nil)
}

And Voila, you should now have infinite scroll.

Improvements and Future Proofing

To improve this code you could have an object that can facilitate this preload for any UIScrollView subclass. This would also make it easier to transition over to networking calls and more complicated logic:

class ScrollViewPreloader {

    enum View {
        case TableView(tableView: UITableView)
        case CollectionView(collectionView: UICollectionView)
    }

    private (set) var view: View
    private (set) var pictures: [Picture] = []

    init(view: View) {
        self.view = view
    }

    func loadNextPageIfNeeded() {

        func shouldLoadNextPage(scrollView: UIScrollView) -> Bool {
            return scrollView.canStartLoadingNextPage()
        }

        switch self.view {
        case .TableView(tableView: let tableView):
            if shouldLoadNextPage(tableView) {
               loadNextPageForTableView(tableView)
            }
            break
        case .CollectionView(collectionView: let collectionView):
            if shouldLoadNextPage(collectionView) {
                loadNextPageForCollectionView(collectionView)
            }
            break
        }
    }

    private func loadNextBatchOfPictures() -> [Picture] {
        let nextBatch = self.pictures//or however you get the next batch
        self.pictures.appendContentsOf(nextBatch)
        return self.pictures
    }

    private func loadNextPageForTableView(tableView: UITableView) {
        tableView.beginUpdates()
        loadNextBatchOfPictures()
        tableView.reloadData()//or you could call insert functions etc
        tableView.endUpdates()
    }

    private func loadNextPageForCollectionView(collectionView: UICollectionView) {
        collectionView.performBatchUpdates({ () -> Void in
            self.loadNextBatchOfPictures()
            collectionView.reloadSections(NSIndexSet(index: 0))
            }, completion: nil)

    }
}

struct Picture {

}

and you would be able to call it using the loadNextPageIfNeeded() function inside cellForRow

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM