【问题标题】:UICollectionView reloadData only works one time in twoUICollectionView reloadData 只能分两次工作
【发布时间】:2021-08-31 11:00:09
【问题描述】:

我有一个UIViewController,它提供一个UIDocumentPicker 来选择PDF 文件,并包含一个显示它们的UICollectionView(每个单元格都包含一个PDFView 来这样做)。

代码如下:

import MobileCoreServices; import PDFKit; import UIKit

class ViewController: UIViewController {
    var urls: [URL] = []
    @IBOutlet weak var collectionView: UICollectionView!

    @IBAction func pickFile() {
        DispatchQueue.main.async {
            let documentPicker = UIDocumentPickerViewController(documentTypes: [kUTTypePDF as String], in: .import)
            documentPicker.delegate = self
            documentPicker.modalPresentationStyle = .formSheet
            self.present(documentPicker, animated: true, completion: nil)
        }
    }
    
    override func viewDidLoad() {
        collectionView.register(UINib(nibName: PDFCollectionViewCell.identifier, bundle: .main),
                                forCellWithReuseIdentifier: PDFCollectionViewCell.identifier)
    }
    
    init() { super.init(nibName: "ViewController", bundle: .main) }
    required init?(coder: NSCoder) { fatalError() }
}

extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PDFCollectionViewCell.identifier, for: indexPath) as! PDFCollectionViewCell
        cell.pdfView.document = PDFDocument(url: urls[indexPath.row])
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return urls.count
    }

    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize {
        CGSize(width: 150, height: 150)
    }
}

extension ViewController: UIDocumentPickerDelegate {
    // MARK: PDF Picker Delegate
    func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
        controller.dismiss(animated: true, completion: nil)
        
    }
    
    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
        controller.dismiss(animated: true, completion: {
            DispatchQueue.main.async {
                self.urls.append(contentsOf: urls)
                self.collectionView.reloadData()
            }
        })
    }
}

class PDFCollectionViewCell: UICollectionViewCell {
    static let identifier = "PDFCollectionViewCell"
    
    @IBOutlet weak var pdfView: PDFView! { didSet { setPdfViewUI() } }
    
    func setPdfViewUI() {
        pdfView.displayMode = .singlePage
        pdfView.autoScales = true
        pdfView.displayDirection = .vertical
        pdfView.isUserInteractionEnabled = false
    }
}

现在,出于某种原因,collectionView.reloadData() 实际上只能在两次中使用一次。它第一次工作,然后第二次没有任何反应,然后第三次再次使用三个预期元素更新集合视图...

我意识到,即使我正在调用 reloadData(),当这种情况发生时,dataSource 和委托方法 (numberOfItems/cellForItem) 也不会被调用。

知道发生了什么吗?

感谢您的帮助!

编辑:我可以确保我在 viewDidLoad/appear 方法中没有任何其他代码,pickFile 函数实际上被正常调用,并且 URL 被正确获取,urls在调用 reloadData() 之前,数组正在更新。

另外,我已经对UITableViewUICollectionView 进行了尝试,在每种情况下我都遇到了这个问题。感觉就像我使用PDFView 或文档选择器有问题。

【问题讨论】:

  • viewWillAppear() / viewDidAppear() 中是否还有其他问题在此处不可见?
  • 嗨@TarunTyagi,绝对不是,我已经粘贴了整个代码(顺便说一句,除了 XIB 文件)
  • 您是否确保异步块内部的reloadData 在您期望的时候被调用?也许还要确保您附加的 urls 数组非空。另外,我没有看到调用pickFile 的内容,我想知道pickFiledocumentPicker 是否存在释放问题,因为它是在该调度块内创建的?
  • @WongWray 感谢您的评论。我确保了你所说的一切,我什至创建了一个 TestCollectionView 子类,它覆盖了 UICollectionView 及其函数 reloadData() 以记录我实际输入此方法的事实,这就是发生的事情。而且我还检查了urls 不为空,pickFile 执行良好。

标签: ios swift uicollectionview pdfkit


【解决方案1】:

collectionView 以毫秒为单位缓存reloadingData,因此它不会在您调用reloadData() 两次时重新加载两次,同样基于apple developer documentation

您不应在插入或删除项目的动画块中间调用此方法。插入和删除会自动导致集合的数据得到适当的更新。

我有一些解决方法来实现这一点:

使 collectionView 布局无效:

据我所知collectionViewLayout prepare里面有remove cache方法

collectionView!.reloadData()
collectionView!.collectionViewLayout.invalidateLayout()
collectionView!.layoutSubviews()

【讨论】:

    【解决方案2】:

    这是一个非常奇怪的错误,当您在 UICollectionViewCell 中使用 PDFView 时会发生这种错误。我在以下环境中确认了这一点-

    1. Xcode 12.5
    2. iPhone SE 2020 (iOS 14.6)
    PDFView 作为子视图添加到UICollectionViewCell.contentView 中时,

    UICollectionView.reloadData() 调用无法可靠地工作

    我们还能尝试什么?

    令人惊讶的是,UICollectionView.insertItems(at:)UICollectionView.reloadData() 不适用于这种情况下。此答案末尾提供了工作代码示例,供其他尝试重现/确认问题的人使用。

    为什么会发生这种情况?

    老实说不知道。 UICollectionView.reloadData() 应该保证 UI 与您的数据源同步。让我们看看reloadData()(在这种情况下工作时)和insertItems(at:)的堆栈跟踪。

    ReloadData_StackTrace

    InsertItemsAtIndexPaths_StackTrace

    结论

    1. reloadData() 依赖 layoutSubviews() 来执行 UI 刷新。这是从UIView 继承的,例如 - UIView.layoutSubviews() > UIScrollView > UICollectionView。这是一个众所周知的 UI 事件,很容易被UIView 的任何子类拦截。 PDFView: UIView 也可以这样做。 为什么会出现不一致的情况?只有可以拆卸和检查PDFKit.framework 的人才可能知道这一点。这显然是PDFView.layoutSubviews() 实现中的一个错误,它会干扰它的superview 的layoutSubviews() 实现。

    2. insertItems(at:) 在指定的 indexPath 处添加新的单元格实例,显然不依赖于 layoutSubviews(),因此在这种情况下可以可靠地工作。

    示例代码

    import MobileCoreServices
    import PDFKit
    import UIKit
    
    class ViewController: UIViewController {
        
        // MARK: - Instance Variables
        private lazy var flowLayout: UICollectionViewFlowLayout = {
            let sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
            
            let layout = UICollectionViewFlowLayout()
            layout.scrollDirection = .vertical
            layout.sectionInset = sectionInset
            layout.itemSize = CGSize(width: 150, height: 150)
            layout.minimumInteritemSpacing = 20
            layout.minimumLineSpacing = 20
            
            return layout
        }()
        
        private lazy var pdfsCollectionView: UICollectionView = {
            let cv = UICollectionView(frame: self.view.bounds, collectionViewLayout: flowLayout)
            cv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            cv.backgroundColor = .red
            
            cv.dataSource = self
            cv.delegate = self
            return cv
        }()
        
        private lazy var pickFileButton: UIButton = {
            let button = UIButton(frame: CGRect(x: 300, y: 610, width: 60, height: 40)) // hard-coded for iPhone SE
            button.setTitle("Pick", for: .normal)
            button.setTitleColor(.white, for: .normal)
            button.backgroundColor = .purple
            
            button.addTarget(self, action: #selector(pickFile), for: .touchUpInside)
            return button
        }()
        
        private var urls: [URL] = []
        
        
        // MARK: - View Life Cycle
        override func viewDidLoad() {
            super.viewDidLoad()
            
            self.view.addSubview(pdfsCollectionView)
            pdfsCollectionView.register(
                PDFCollectionViewCell.self,
                forCellWithReuseIdentifier: PDFCollectionViewCell.cellIdentifier
            )
            
            self.view.addSubview(pickFileButton)
        }
        
        
        // MARK: - Helpers
        @objc private func pickFile() {
            let documentPicker = UIDocumentPickerViewController(documentTypes: [kUTTypePDF as String], in: .import)
            documentPicker.delegate = self
            documentPicker.modalPresentationStyle = .formSheet
            self.present(documentPicker, animated: true, completion: nil)
        }
        
    }
    
    extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PDFCollectionViewCell.cellIdentifier, for: indexPath) as! PDFCollectionViewCell
            cell.pdfView.document = PDFDocument(url: urls[indexPath.row])
            cell.contentView.backgroundColor = .yellow
            return cell
        }
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return urls.count
        }
    }
    
    extension ViewController: UIDocumentPickerDelegate {
        func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
            controller.dismiss(animated: true, completion: nil)
        }
        
        func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
            
            // CAUTION:
            // These urls are in the temporary directory - `.../tmp/<CFBundleIdentifier>-Inbox/<File_Name>.pdf`
            // You should move/copy these files to your app's document directory
            
            controller.dismiss(animated: true, completion: {
                DispatchQueue.main.async {
                    let count = self.urls.count
                    var indexPaths: [IndexPath] = []
                    for i in 0..<urls.count {
                        indexPaths.append(IndexPath(item: count+i, section: 0))
                    }
                    
                    self.urls.append(contentsOf: urls)
                    
                    // Does not work reliably
                    /*
                    self.pdfsCollectionView.reloadData()
                    */
                    
                    // Works reliably
                    self.pdfsCollectionView.insertItems(at: indexPaths)
                }
            })
        }
    }
    
    
    class PDFCollectionViewCell: UICollectionViewCell {
        static let cellIdentifier = "PDFCollectionViewCell"
        
        lazy var pdfView: PDFView = {
            let view = PDFView(frame: self.contentView.bounds)
            view.displayMode = .singlePage
            view.autoScales = true
            view.displayDirection = .vertical
            view.isUserInteractionEnabled = false
            
            self.contentView.addSubview(view)
            view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            view.backgroundColor = .yellow
            
            return view
        }()
    }
    

    【讨论】:

    • 很棒的调查!在我重现该问题后,我打算尝试插入,但由于某种原因从未解决过
    • 嗨@TarunTyagi,非常感谢您的出色调查。很高兴知道还有一种方法可以使用 PDFKit 做到这一点。也感谢您关于将文件复制到更安全的地方的建议,.documentsDirectory 可以吗?
    • 是的,.documentsDirectory 应该没问题。
    • 如果这能解决您的问题,请考虑接受答案。
    【解决方案3】:

    这是由您的单元格中的 PDFViews 引起的,原因我不确定。 PDFView 可能太重,无法在集合视图单元格中使用,或者在重新加载或重新添加文档时可能会出现问题。我复制了您的问题但没有设法解决它,但您可能需要牢记以下几点:

    • 您正在为文档选择器使用已弃用的初始化程序
    • 您获得的“收件箱”网址不能保证是持久的,事实上,当您选择更多文档时,您可以看到文件在其中出现和消失,因此您可能希望考虑将选定的文件移动到更永久的位置

    解决这些问题对奇怪的重新加载问题没有任何影响。我所做的是import QuickLookThumbnailing,将图像视图添加到单元格而不是 PDF 视图,然后添加以下代码:

    class PDFCollectionViewCell: UICollectionViewCell {
        static let identifier = "PDFCollectionViewCell"
        
        @IBOutlet var imageView: UIImageView!
        private var request: QLThumbnailGenerator.Request?
            
        func load(_ url: URL) {
            let req = QLThumbnailGenerator.Request.init(fileAt: url, size: bounds.size, scale: UIScreen.main.scale, representationTypes: .all)
            
            QLThumbnailGenerator.shared.generateRepresentations(for: req) {
                [weak self]
                rep, type, error in
                DispatchQueue.main.async {
                    self?.imageView.image = rep?.uiImage
                }
            }
            request = req
        }
        
        override func prepareForReuse() {
            if let request = request {
                QLThumbnailGenerator.shared.cancel(request)
                self.request = nil
            }
            imageView.image = nil
        }
    }
    

    这会异步呈现您传递给它的任何 URL 的缩略图,从而提高质量。

    【讨论】:

    • 非常感谢!由于我也在使用带有图像视图的单元格来在我的项目中实现相同的结果,但使用 UIImage,我将使用这个非常方便的解决方案。简单的问题,什么时候调用load(_ url: URL) cell 方法?
    • 在cellForItemAt中配置单元格时
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-28
    相关资源
    最近更新 更多