Pagination is jumpy using IGListKit

604 views Asked by At

I am trying to implement infinite scrolling w/ IGListKit. I have followed their example code, however this is a simple text cell.

I am using self sizing cells that render images.

My images are cached using Kingfisher. I would expect to scroll view to maintain its position and append any new items below, essentially increasing the scrollable area.

Instead what is happening is my feed jumps back to the first item after adding the new items.

I have mocked the latency of a network call in my scrollViewWillEndDragging method, appending new items to the end of the collection and calling adapter.performUpdates(animated: false) when the collection didSet is triggered.

The model I am rendering is

class FeedImage {
    let id = UUID()
    let url: URL
    let width: CGFloat
    let height: CGFloat

    init(url: String, size: CGSize) {
        self.url = URL(string: url)!
        self.width = size.width
        self.height = size.height
    }

    var heightForFeed: CGFloat {
        let ratio = CGFloat(width) / CGFloat(height)
        let height = (UIScreen.main.bounds.width - 32) / ratio
        return height
    }
}

extension FeedImage: ListDiffable {
    func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        if let object = object as? FeedImage {
            return url == object.url
        }
        return false
    }

    func diffIdentifier() -> NSObjectProtocol {
        return id as NSObjectProtocol
    }
}

You can see the behaviour here.

ImageFeedViewController

class ImageFeedViewController: UIViewController {

    let vOne = [
        FeedImage(url: "https://pbs.twimg.com/media/D-D70C8W4AEtCav.jpg", size: .init(width: 1280, height: 1657)),
        FeedImage(url: "https://pbs.twimg.com/media/D-DpfrbWsAI6KaC.jpg", size: .init(width: 911, height: 683)),
        FeedImage(url: "https://pbs.twimg.com/media/D-EU-79W4AAlIk6.jpg", size: .init(width: 499, height: 644 )),
        FeedImage(url: "https://pbs.twimg.com/media/D-ECq-dX4AA1U40.jpg", size: .init(width: 1035, height: 1132)),
    ]

    var feed = [FeedImage]() {
        didSet {
            self.adapter.performUpdates(animated: false)
            loading = false
        }
    }

    var loading = false
    var hasHadded = false // prevent mock pagination from firing over and over

    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.estimatedItemSize = .init(width: view.frame.width, height: 10)

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.alwaysBounceVertical = true
        collectionView.backgroundColor = .darkGray

        return collectionView
    }()

    private lazy var adapter: ListAdapter = {
        return ListAdapter(
            updater: ListAdapterUpdater(),
            viewController: self,
            workingRangeSize: 0)
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        adapter.collectionView = collectionView
        adapter.dataSource = self
        adapter.scrollViewDelegate = self

        view.addSubview(collectionView)

        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])

        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            self.feed = self.vOne
        }
    }
}

extension ImageFeedViewController: ListAdapterDataSource {
    func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
        return feed as [ListDiffable]
    }

    func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
        return ImageSectionController()
    }

    func emptyView(for listAdapter: ListAdapter) -> UIView? {
        return nil
    }
}

extension ImageFeedViewController: UIScrollViewDelegate {
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let distance = scrollView.contentSize.height - (targetContentOffset.pointee.y + scrollView.bounds.height)

        if distance < 200 && !loading && !hasHadded {
            loading = true
            hasHadded = true
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.feed.append(contentsOf: [
                    FeedImage(url: "https://pbs.twimg.com/media/D-EL1DNXYAAMv1R.jpg", size: .init(width: 500, height: 616)),
                    FeedImage(url: "https://pbs.twimg.com/media/D-EHQDcWwAUImah.jpg", size: .init(width: 800, height: 592)),
                    FeedImage(url: "https://pbs.twimg.com/tweet_video_thumb/D-EZkIkXUAEQwHC.jpg", size: .init(width: 500, height: 280)),
                ])
            }
        }
    }
}

ImageSectionController

class ImageSectionController: ListSectionController {
    var image: FeedImage!

    override func numberOfItems() -> Int {
        return 1
    }

    override func sizeForItem(at index: Int) -> CGSize {
        return .init(width: collectionContext!.containerSize.width, height: 1)
    }

    override func cellForItem(at index: Int) -> UICollectionViewCell {
        let cell = collectionContext?.dequeueReusableCell(of: CustomCellTwo.self, for: self, at: index) as! CustomCellTwo
        cell.render(model: image)
        return cell
    }

    override func didUpdate(to object: Any) {
        image = object as? FeedImage
    }
}

CustomCellTwo

class CustomCellTwo: UICollectionViewCell {
    private lazy var width: NSLayoutConstraint = {
        let width = contentView.widthAnchor.constraint(equalToConstant: bounds.size.width)
        width.isActive = true
        return width
    }()

    var imageBodyBottomAnchor: NSLayoutConstraint!
    var imageHeightAnchor: NSLayoutConstraint!

    let image = UIImageView(frame: .zero)
    lazy var imageHeight: CGFloat = 0

    override init(frame: CGRect) {
        super.init(frame: frame)
        imageHeightAnchor = image.heightAnchor.constraint(equalToConstant: 0)
        imageHeightAnchor.isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        return nil
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        image.kf.cancelDownloadTask()
    }

    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        width.constant = bounds.size.width
        return contentView.systemLayoutSizeFitting(CGSize(width: targetSize.width, height: 1))
    }

    func render(model: FeedImage) {

        image.translatesAutoresizingMaskIntoConstraints = false
        imageHeightAnchor.constant = model.heightForFeed
        image.kf.setImage(with: model.url)

        contentView.addSubview(image)

        NSLayoutConstraint.activate([
            image.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
            image.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            image.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16)
        ])

        if let lastSubview = contentView.subviews.last {
            imageBodyBottomAnchor = contentView.bottomAnchor.constraint(equalTo: lastSubview.bottomAnchor, constant: 0)
            imageBodyBottomAnchor.priority = .init(999)
            imageBodyBottomAnchor.isActive = true
        }
    }
}
0

There are 0 answers