首页 > 解决方案 > UICollectionView:如何使用新的 iOS 13 API UICollectionViewDiffableDataSource & NSDiffableDataSourceSnapshot 进行分页集合?

问题描述

我正在尝试使用新的 UICollectionView API:UICollectionViewDiffableDataSource 和 NSDiffableDataSourceSnapshot,模拟基于网络偏移的 API。我的目标是有一个分页集合

问题: 当我调用loadMore滚动时,我得到了这个崩溃 self.presentingView?.dataSource.apply(snapshot, animatingDifferences: true, completion: nil)

2020-09-25 11:54:50.462562+0200 TestCollectionView[3518:152267] *** NSArray<UICollectionViewUpdateItem *> 中的断言失败 * _Nonnull _UIDiffableDataSourceApplyInsertUpdate(NSObject<_UIDiffableDataSourceUpdate> *__strong _Nonnull, NSMutableOrderedSet *__derstrong _N__ , _UIDataSourceSnapshotter *__strong _Nonnull, BOOL)(), _UIDiffableDataSourceHelpers.m:512 2020-09-25 11:54:50.472593+0200 TestCollectionView[3518:152267] *** 由于未捕获的异常“NSInternalInconsistencyException”而终止应用程序,原因:“无效参数不满足:索引!= NSNotFound' *** 第一次抛出调用堆栈:( 0 CoreFoundation 0x00007fff2043a126 __exceptionPreprocess + 242 1 libobjc.A.dylib 0x00007fff20177f78 objc_exception_throw + 48 2 CoreFoundation 0x00007fff20439f4f +[NSException raise:format:] + 0 3 Foundation 0x00007fff20788293 -[NSAssertionHandler handleFailureInFunction:file:lineNumber:description:] + 166 4 UIKitCore 0x00007fff24b1c9b0 _UIDiffableDataSourceApplyInsertUpdate + 1793 5 UIKitCore 0x00007fff23d0b52a -[__UIDiffableDataSource _commitUpdate:snapshot :completion:] + 323 6 UIKitCore 0x00007fff23d0af44 __113-[__UIDiffableDataSource _applyDifferencesFromSnapshot:viewPropertyAnimator:customAnimationsProvider:completion:]_block_invoke.228 + 182 7 UIKitCore 0x00007fff23d0b007 __113-[__UIDiffableDataSource:viewPropertyAnimator:customAnimationsProvider:completion:]_block_invoke.238 + 64 8 libdispatch.dylib 0x0000000101837a88 _dispatch_client_callout + 8 9 libdispatch.dylib 0x0000000101846cac _dispatch_lane_barrier_sync_invoke_and_complete + 132 10 UIKitCore 0x00007fff23d0a969 -[__UIDiffableDataSource _applyDifferencesFromSnapshot:viewPropertyAnimator:customAnimationsProvider:completion:] + 1211 11 UIKitCore 0x00007fff23d09874 -[ __UIDiffableDataSource applyDifferencesFromSnapshot:completion:] + 55 12 UIKitCore 0x00007fff23d09f25 -[_dylib 0x0000000101846cac _dispatch_lane_barrier_sync_invoke_and_complete + 132 10 UIKitCore 0x00007fff23d0a969 -[__UIDiffableDataSource _applyDifferencesFromSnapshot:viewPropertyAnimator:customAnimationsProvider:completion:] + 1211 11 UIKitCore 0x00007fff23d09874 -[__UIDiffableDataSource applyDifferencesFromSnapshot:completion:] + 55 12 UIKitCore 0x00007fff23d09f25 -[_dylib 0x0000000101846cac _dispatch_lane_barrier_sync_invoke_and_complete + 132 10 UIKitCore 0x00007fff23d0a969 -[__UIDiffableDataSource _applyDifferencesFromSnapshot:viewPropertyAnimator:customAnimationsProvider:completion:] + 1211 11 UIKitCore 0x00007fff23d09874 -[__UIDiffableDataSource applyDifferencesFromSnapshot:completion:] + 55 12 UIKitCore 0x00007fff23d09f25 -[_] + 55 12 UIKitCore 0x00007fff23d09f25 -[_] + 55 12 UIKitCore 0x00007fff23d09f25 -[_UIDiffableDataSource applyDifferencesFromSnapshot:animatingDifferences:completion:] + 71 13 libswiftUIKit.dylib 0x00007fff539f5a56 $s5UIKit34UICollectionViewDiffableDataSourceC5apply_20animatingDifferences10completionyAA010NSDiffableeF8SnapshotVyxq_G_SbyycSgtFTm + 230 14 TestCollectionView 0x00000001015645cd $s18TestCollectionView0B9PresenterC21fetchNextPageIfNeededyyFys6ResultOyAA5ModelVs5Error_pGcfU+ 1101 15 TestCollectionView 0x0000000101564031 $s18TestCollectionView0B12DataProviderC08retrieveD06offset5limit10completionySi_Siys6ResultOyAA5ModelVs5Error_pGctF + 753 16 TestCollectionView 0x0000000101564160 $s18TestCollectionView0B9PresenterC21fetchNextPageIfNeededyyF + 160 17 TestCollectionView 0x000000010156331c $s18TestCollectionView0bC10ControllerC8loadMore10completionyySbc_tF + 92 18 TestCollectionView 0x0000000101563379 $s18TestCollectionView0bC10ControllerCAA33VerticalPaginationManagerDelegateA2aDP8loadMore10completionyySbc_tFTW + 9 19 TestCollectionView 0x000000010155c283 $s18TestCollectionView25VerticalPaginationManagerC16setContentOffSet33_7A1CA207B09278E8BCBA7248F7151A31LLyySo7CGPointVF + 1187 20 TestCollectionView0x000000010155bcf9 $s18TestCollectionView25VerticalPaginationManagerC12observeValue10forKeyPath2of6change7contextySSSg_ypSgSDySo05NSKeyh6ChangeJ0aypGSgSvSgtF + 1625 21 TestCollectionView 0x000000010155c506 $s18TestCollectionView25VerticalPaginationManagerC12observeValue10forKeyPath2of6change7contextySSSg_ypSgSDySo05NSKeyh6ChangeJ0aypGSgSvSgtFTo + 582 22 Foundation 0x00007fff207d6124 NSKeyValueNotifyObserver + 329 23 Foundation 0x00007fff207d9858 NSKeyValueDidChange + 439 24 Foundation 0x00007fff207d91d8 -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 741 25 Foundation 0x00007fff207d9a3b - [NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68 26 Foundation 0x00007fff207d37db _NSSetPointValueAndNotify + 304 27 UIKitCore 0x00007fff24b2deaa -[UIScrollView _updatePanGesture] + 4833 28 UIKitCore 0x00007fff2419bee7 -[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 49 29 UIKitCore 0x00007fff241a6168 _UIGestureRecognizerSendTargetActions + 100 30 UIKitCore 0x00007fff241a2a2f _UIGestureRecognizerSendActions + 294 31 UIKitCore 0x00007fff241a1d8e -[UIGestureRecognizer _updateGestureForActiveEvents] + 725 32 UIKitCore 0x00007fff24194352 _UIGestureEnvironmentUpdate + 2652 33 UIKitCore 0x00007fff2419347a -[UIGestureEnvironment _updateForEvent:窗口:] + 887 34 UIKitCore 0x00007fff246a582d -[UIWindow sendEvent:] + 4752 35 UIKitCore 0x00007fff2467f376 -[UIApplication sendEvent:] + 633 36 UIKitCore 0x00007fff2470f8d6 __processEventQueue + 13895 37 UIKitCore 0x00007fff2470626c __eventFetcherSourceCallback + 104 38 CoreFoundation 0x00007fff203a8845CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION+ 17 39 CoreFoundation 0x00007fff203a873d __CFRunLoopDoSource0 + 180 40 CoreFoundation 0x00007fff203a7c1f __CFRunLoopDoSources0 + 248 41 CoreFoundation 0x00007fff203a23f7 __CFRunLoopRun + 878 42 CoreFoundation 0x00007fff203a1b9e CFRunLoopRunSpecific + 567 43 GraphicsServices 0x00007fff2b773db3 GSEventRunModal + 139 44 UIKitCore 0x00007fff24660af3 -[UIApplication _run] + 912 45 UIKitCore 0x00007fff24665a04 UIApplicationMain + 101 46 TestCollectionView 0x00000001015709db main + 75 47 libdyld.dylib 0x00007fff20257415 start + 1 ) libc++abi.dylib: 以 NSException 类型的未捕获异常终止 *** 由于未捕获异常而终止应用程序'NSInternalInconsistencyException',原因:'无效参数不满足:索引!= NSNotFound'以未捕获的 NSException 类型异常终止

执行

import UIKit

enum Section: CaseIterable {
    case main
}

// MARK: Controller

class CollectionViewController : UICollectionViewController {

    let presenter: CollectionPresenter
    
    // MARK: Init
    
    init(presenter: CollectionPresenter) {
        self.presenter = presenter
        super.init(collectionViewLayout: UICollectionViewFlowLayout())
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: Life cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollection()
        setupPagination()
        presenter.fetchPresentationData()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
//        presenter.fetchPresentationData()
    }
    
    // MARK: CollectionView
    
    lazy var dataSource = makeDataSource()
    private lazy var delegate = makeDelegate()
    private lazy var prefetchDataSource = makePrefetchDataSource()
    
    func setupCollection() {
        collectionView?.backgroundColor = .white
        collectionView?.register(Cell.self, forCellWithReuseIdentifier: "PlayCell")
        collectionView?.dataSource = dataSource
        collectionView?.prefetchDataSource = prefetchDataSource
        collectionView?.delegate = delegate
    }
    
    // MARK: Pagination
    private lazy var paginationManager: VerticalPaginationManager = {
        let manager = VerticalPaginationManager(scrollView: collectionView)
        manager.delegate = self
        return manager
    }()
}

extension CollectionViewController {
    func makeDataSource() -> UICollectionViewDiffableDataSource<Section, Model.Word> {
        return UICollectionViewDiffableDataSource<Section, Model.Word>(collectionView: collectionView)
        { collectionView, indexPath, item -> Cell? in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlayCell", for: indexPath) as? Cell
            cell?.backgroundColor = indexPath.row % 2 == 0 ? .systemBlue : .systemGray
            cell?.contentLabel.text = item.name
            return cell
        }
    }
    
    func makePrefetchDataSource() -> UICollectionViewDataSourcePrefetching {
        let dataSource = CollectionViewDataSource()
        dataSource.presenter = presenter
        return dataSource
    }
    
    func makeDelegate() -> UICollectionViewDelegate {
        let delegate = CollectionViewDelegate()
        delegate.presenter = presenter
        return delegate
    }
}

private class CollectionViewDelegate: NSObject, UICollectionViewDelegate {
    
    weak var presenter: CollectionPresenter?
    
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
//        let scrollVelocity = collectionView.panGestureRecognizer.velocity(in: collectionView.superview)
//        if scrollVelocity.y > 0.0 {
//
//            print("going down")
//        } else if scrollVelocity.y < 0.0 {
//            print("going up, \(collectionView.collectionViewLayout.collectionViewContentSize.height) && \(cell.frame.origin.y)")
//            let size = cell.frame.origin.y / collectionView.collectionViewLayout.collectionViewContentSize.height
//            print(size)
//            if size > 0.3 {
//                presenter?.fetchNextPageIfNeeded(whileIsPreloadingCellAtIndex: indexPath.row)
//            }
//        }
//
//        print("\(cell.frame.origin.y) \(indexPath)")
//
//        presenter?.fetchNextPageIfNeeded(whileIsPreloadingCellAtIndex: indexPath.row)
    }
}

private class CollectionViewDataSource: NSObject, UICollectionViewDataSourcePrefetching {
    
    weak var presenter: CollectionPresenter?
    
    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
//        let scrollVelocity = collectionView.panGestureRecognizer.velocity(in: collectionView.superview)
//        if scrollVelocity.y > 0.0 {
//
//            print("going down")
//        } else if scrollVelocity.y < 0.0 {
//            print("going up, \(collectionView.collectionViewLayout.collectionViewContentSize.height)")
//            presenter?.fetchNextPageIfNeeded(whileIsPreloadingCells: indexPaths.map({ $0.row }))
//        }
    }
    
    
}

// MARK: Pagination

extension CollectionViewController: VerticalPaginationManagerDelegate {
    
    private func setupPagination() {
        self.paginationManager.refreshViewColor = .clear
        self.paginationManager.loaderColor = .white
    }
    
    func refreshAll(completion: @escaping (Bool) -> Void) {
    }
    
    func loadMore(completion: @escaping (Bool) -> Void) {
        presenter.fetchNextPageIfNeeded()
        completion(true)
    }
}


// MARK: Presenter

class CollectionPresenter {
    
    weak var presentingView: CollectionViewController?
    let dataProvider: CollectionDataProvider
    private var currentOffset: Int = 0
    
    init(dataProvider: CollectionDataProvider) {
        self.dataProvider = dataProvider
    }
    
    func fetchPresentationData() {
        fetchFirstPage()
    }
    
    func fetchFirstPage() {
        dataProvider.retrieveData { result in
            switch result {
            case .success(let data):
                self.updateOffset(data.next)
                var snapshot = NSDiffableDataSourceSnapshot<Section, Model.Word>()
                snapshot.appendSections([.main])
                snapshot.appendItems(data.words, toSection: .main)
                self.presentingView?.dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
            case .failure(_):
                break
            }
        }
    }
    
    func fetchNextPageIfNeeded() {

        dataProvider.retrieveData(offset: currentOffset, limit: 10) { result in
            switch result {
            case .success(let data):
                self.updateOffset(data.next)
                if var snapshot = self.presentingView?.dataSource.snapshot() {
                    snapshot.appendItems(data.words, toSection: .main)
                    self.presentingView?.dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
                } 
            case .failure(_):
                break
            }
        }
    }
    
    private func updateOffset(_ next: String?) {
        guard let next = next else { return }
        guard let offset = extractParam(from: next, paramName: "offset") else { return }
        
        if let newOffset = Int(offset) {
            currentOffset = newOffset
            print(currentOffset)
        }
    }
    
    fileprivate func extractParam(from query: String, paramName: String) -> String? {
        let params = query.components(separatedBy: "&")
        for param in params {
            let paramKeyValue = param.components(separatedBy: "=")
            if let name = paramKeyValue.first {
                if name == paramName {
                    return paramKeyValue.last
                }
            }
        }
        return nil
    }
}

// MARK: DataProvider

final class CollectionDataProvider {
    
    private lazy var poetry = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? I. Quae res in civitate duae plurimum possunt, eae contra nos ambae faciunt in hoc tempore, summa gratia et eloquentia; quarum alterum, C. Aquili, vereor, alteram metuo. Eloquentia Q. Hortensi ne me in dicendo impediat, non nihil commoveor, gratia Sex. Naevi ne P. Quinctio noceat, id vero non mediocriter pertimesco. Neque hoc tanto opere querendum videretur, haec summa in illis esse, si in nobis essent saltem mediocria; verum ita se res habet, ut ego, qui neque usu satis et ingenio parum possum, cum patrono disertissimo comparer, P. Quinctius, cui tenues opes, nullae facultates, exiguae amicorum copiae sunt, cum adversario gratiosissimo contendat. Illud quoque nobis accedit incommodum, quod M. Iunius, qui hanc causam aliquotiens apud te egit, homo et in aliis causis exercitatus et in hac multum ac saepe versatus, hoc tempore abest nova legatione impeditus, et ad me ventum est qui, ut summa haberem cetera, temporis quidem certe vix satis habui ut rem tantam, tot controversiis implicatam, possem cognoscere. Ita quod mihi consuevit in ceteris causis esse adiumento, id quoque in hac causa deficit. Nam, quod ingenio minus possum, subsidium mihi diligentia comparavi; quae quanta sit, nisi tempus et spatium datum sit, intellegi non potest. Quae quo plura sunt, C. Aquili, eo te et hos qui tibi in consilio sunt meliore mente nostra verba audire oportebit, ut multis incommodis veritas debilitata tandem aequitate talium virorum recreetur. Quod si tu iudex nullo praesidio fuisse videbere contra vim et gratiam solitudini atque inopiae, si apud hoc consilium ex opibus, non ex veritate causa pendetur, profecto nihil est iam sanctum atque sincerum in civitate, nihil est quod humilitatem cuiusquam gravitas et virtus iudicis consoletur. Certe aut apud te et hos qui tibi adsunt veritas valebit, aut ex hoc loco repulsa vi et gratia locum ubi consistat reperire non poterit. II. Non eo dico, C. Aquili, quo mihi veniat in dubium tua fides et constantia, aut quo non in his quos tibi advocavisti viris lectissimis civitatis spem summam habere P. Quinctius, debeat. Quid ergo est? Primum magnitudo periculi summo timore hominem adficit, quod uno iudicio de fortunis omnibus decernit, idque dum cogitat, non minus saepe ei venit in mentem potestatis quam aequitatis tuae, propterea quod omnes quorum in alterius manu vita posita est saepius illud cogitant, quid possit is cuius in dicione ac potestate sunt quam quid debeat facere. Deinde habet adversarium P. Quinctius verbo Sex. Naevium, re ura huiusce aetatis homines disertissimos, fortissimos, florentissimos nostrae civitatis, qui communi studio summis opibus Sex Naevium defendunt, si id est defendere, cupiditati alterius obtemperare quo is facilius quem velit iniquo iudicio opprimere possit. Nam quid hoc iniquius aut indignius, C. Aquili, dici aut commemorari potest, quam me qui caput alterius, famam fortunasque defendam priore loco causam dicere? cum praesertim Q. Hortensius qui in hoc iudicio partis accusatoris obtinet contra me sit dicturus, cui summam copiam facultatemque dicendi natura largita est. Ita fit ut ego qui tela depellere et volneribus mederi debeam tum id facere cogar cum etiam telum adversarius nullum iecerit, illis autem id tempus impugnandi detur cum et vitandi illorum impetus potestas adempta nobis erit et, si qua in re, id quod parati sunt facere, falsum crimen quasi venenatum aliquod telum iecerint, medicinae faciendae locus non erit. Id accidit praetoris iniquitate et iniuria, primum quod contra omnium consuetudinem iudicium prius de probro quam de re maluit fieri, deinde quod ita constituit id ipsum iudicium ut reus, ante quam verbum accusatoris audisset, causam dicere cogeretur. Quod eorum gratia et potentia factum ao est qui, quasi sua res aut honos agatur, ita diligenter Sex. Naevi studio et cupiditati morem gerunt et in eius modi rebus opes suas experiuntur, in quibus, quo plus propter virtutem nobilitatemque possunt, eo minus quantum possint debent ostendere."
    
    func retrieveData(offset: Int = 0, limit: Int = 256, completion: @escaping (Result<Model, Error>) -> Void) {
        let count       = poetry.count
        let words       = makeWords(offset: offset, limit: limit, count: count)
        let next        = makeNext(offset: offset, limit: limit, count: count)
        let previous    = makePrevious(offset: offset, limit: limit, count: count)
        let model       = Model(words: words,
                                count: count,
                                next: next,
                                previous: previous)
        completion(Result.success(model))
    }
    
    private func makeWords(offset: Int = 0, limit: Int = 256, count: Int) -> [Model.Word] {
        let components = poetry.components(separatedBy: " ")
        print(components)
        
        if limit < 1 || limit > count {
            return []
        }
        
        let newOffset = offset + limit
//        let newLimit = min(count - limit, limit)
        
        if newOffset > count {
            return []
        }
        
        
        return components[offset..<offset + limit].map({ Model.Word(name: $0) })
    }
    
    private func makeNext(offset: Int = 0, limit: Int = 256, count: Int) -> String? {
        if limit < 1 || limit > count {
            return nil
        }
        
        let newOffset = offset + limit
        let newLimit = min(count - limit, limit)
        
        if newOffset > count {
            return nil
        } else {
            return "offset=\(newOffset)&limit=\(newLimit)"
        }
    }

    private func makePrevious(offset: Int = 0, limit: Int = 256, count: Int) -> String? {
        if limit < 1 || limit > count {
            return nil
        }
        
        let newOffset = offset - limit
        let newLimit = min(count - limit, limit)
        
        if newOffset < 0 {
            return nil
        } else {
            return "offset=\(newOffset)&limit=\(newLimit)"
        }
    }
}

// MARK: Model

struct Model: Codable {
    struct Word: Codable {
        let name: String
    }
    
    let words: [Word]
    
    // Pagination
    let count: Int
    let next: String?
    let previous: String?
}

extension Model: Hashable {
    var identifier: UUID { UUID() }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }
}

extension Model: Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        return lhs.identifier == rhs.identifier
    }
}

extension Model.Word: Hashable {
    var identifier: UUID { UUID() }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }
}

extension Model.Word: Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        return lhs.identifier == rhs.identifier
    }
}

// MARK: Cell

class Cell: UICollectionViewCell {
    lazy var contentLabel: UILabel = {
        let label = UILabel(frame: .zero)
        label.textColor = .systemRed
        label.textAlignment = .center
        label.adjustsFontSizeToFitWidth = true
        label.translatesAutoresizingMaskIntoConstraints = false
        
        self.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: self.centerYAnchor),
            label.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
            label.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8)
        ])
        
        return label
    }()
}
import Foundation
import UIKit

protocol VerticalPaginationManagerDelegate: class {
    func refreshAll(completion: @escaping (Bool) -> Void)
    func loadMore(completion: @escaping (Bool) -> Void)
}

class VerticalPaginationManager: NSObject {
    
    private var isLoading = false
    
    private var isObservingKeyPath: Bool = false
    
    private var scrollView: UIScrollView!
    
    private var bottomMostLoader: UIView?
    
    var refreshViewColor: UIColor = .white
    var loaderColor: UIColor = .white
    
    weak var delegate: VerticalPaginationManagerDelegate?
    
    init(scrollView: UIScrollView) {
        super.init()
        self.scrollView = scrollView
        self.addScrollViewOffsetObserver()
    }
    
    deinit {
        self.removeScrollViewOffsetObserver()
    }
    
    func initialLoad() {
        self.delegate?.refreshAll(completion: { _ in })
    }
    
}

// MARK: BOTTOM LOADER

extension VerticalPaginationManager {
    
    private func addBottomMostControl() {
        let view = UIView()
        view.backgroundColor = self.refreshViewColor
        
        let activity = UIActivityIndicatorView(style: .medium)
        activity.color = self.loaderColor
        activity.frame = view.bounds
        activity.startAnimating()
        
        view.addSubview(activity)
        activity.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            activity.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            activity.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        
        self.bottomMostLoader = view
        self.scrollView.addSubview(view)
        
        view.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            view.topAnchor.constraint(equalTo: scrollView.bottomAnchor),
            view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            view.heightAnchor.constraint(equalToConstant: 60)
        ])
    }
    
    func removeBottomLoader() {
        self.bottomMostLoader?.removeFromSuperview()
        self.bottomMostLoader = nil
    }
    
}

// MARK: OFFSET OBSERVER

extension VerticalPaginationManager {
    
    private func addScrollViewOffsetObserver() {
        if self.isObservingKeyPath { return }
        self.scrollView.addObserver(
            self,
            forKeyPath: "contentOffset",
            options: [.new],
            context: nil
        )
        self.isObservingKeyPath = true
    }
    
    private func removeScrollViewOffsetObserver() {
        if self.isObservingKeyPath {
            self.scrollView.removeObserver(self,
                                           forKeyPath: "contentOffset")
        }
        self.isObservingKeyPath = false
    }
    
    override public func observeValue(forKeyPath keyPath: String?,
                                      of object: Any?,
                                      change: [NSKeyValueChangeKey : Any]?,
                                      context: UnsafeMutableRawPointer?) {
        guard let object = object as? UIScrollView,
            let keyPath = keyPath,
            let newValue = change?[.newKey] as? CGPoint,
            object == self.scrollView, keyPath == "contentOffset" else { return }
        self.setContentOffSet(newValue)
    }
    
    private func setContentOffSet(_ offset: CGPoint) {
        let offsetY = offset.y
        let contentHeight = self.scrollView.contentSize.height
        let frameHeight = self.scrollView.bounds.size.height
        let diffY = contentHeight - frameHeight
        if contentHeight > frameHeight,
        offsetY > (diffY + 130) && !self.isLoading {
            self.isLoading = true
            self.addBottomMostControl()
            self.delegate?.loadMore { success in
                self.isLoading = false
                self.removeBottomLoader()
            }
        }
    }
    
}

根本原因

func fetchNextPageIfNeeded() {

        dataProvider.retrieveData(offset: currentOffset, limit: 10) { result in
            switch result {
            case .success(let data):
                self.updateOffset(data.next)
                if var snapshot = self.presentingView?.dataSource.snapshot() {
                    snapshot.appendItems(data.words, toSection: .main)
                    self.presentingView?.dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
                } 
            case .failure(_):
                break
            }
        }
    }

标签: iosswiftuicollectionview

解决方案


推荐阅读