UICollectionView reloadData 两次只能工作一次
UICollectionView reloadData only works one time in two
我有一个 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()
,发生这种情况时也不会调用数据源和委托方法 (numberOfItems
/cellForItem
)。
知道发生了什么吗?
感谢您的帮助!
编辑:我可以确保 viewDidLoad
/appear
方法中没有任何其他代码,pickFile
函数实际上被调用得很好并且 url 被正确获取,urls
数组在调用 reloadData()
.
之前按应有的方式更新
另外,我在 UITableView
和 UICollectionView
上都试过了,我在每种情况下都遇到了这个问题。我使用的是 PDFView
或文档选择器,感觉好像出了点问题。
这是由您的单元格中的 PDFView 引起的,具体原因我不确定。 PDFView 可能对于在集合视图单元格中使用来说过于重量级,或者在重新加载或重新添加文档时可能会出现一些问题。我重现了你的问题,但没有设法解决它,但你可能需要牢记以下几点:
- 您正在为文档选择器使用已弃用的初始化程序
- 您得到的“收件箱”url 不能保证持久存在,实际上您可以看到随着 select 更多文件的出现和消失,所以您可能想要考虑将 selected 文件移动到更永久的位置
解决这些问题对奇怪的重新加载问题没有影响。我所做的是 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 缩略图,同时提高质量。
这是一个非常非常奇怪的错误,当您在 UICollectionViewCell
中使用 PDFView
时会发生这种错误。我在以下环境中确认了这一点 -
- Xcode12.5
- iPhone SE 2020 (iOS 14.6)
UICollectionView.reloadData()
调用 无法可靠地工作 当 PDFView
作为子视图添加到 UICollectionViewCell.contentView
.
中时
我们还能尝试什么?
令人惊讶的是 UICollectionView.insertItems(at:)
在 UICollectionView.reloadData()
不适用于这种情况的情况下有效。在这个答案的末尾提供了工作代码示例,供任何其他试图reproduce/confirm这个问题的人使用。
为什么会发生这种情况?
老实说不知道。 UICollectionView.reloadData()
应该保证 UI 与您的数据源同步。让我们看看 reloadData()
的堆栈跟踪(在这种情况下有效)和 insertItems(at:)
.
ReloadData_StackTrace
InsertItemsAtIndexPaths_StackTrace
结论
reloadData()
依靠 layoutSubviews()
来执行 UI 刷新。这是从 UIView
继承而来的,比如 - UIView.layoutSubviews() > UIScrollView > UICollectionView
。这是一个众所周知的 UI 事件,可以很容易地被 UIView
的任何子类拦截。 PDFView: UIView
也可以做到。 为什么会出现不一致的情况?只有会拆机检查PDFKit.framework
的人才知道。这显然是 PDFView.layoutSubviews()
实现中的一个错误,它干扰了其父视图的 layoutSubviews()
实现。
insertItems(at:)
在指定的 indexPath(s) 处添加单元格的新实例并且显然不依赖于 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
}()
}
collectionView 以某种方式缓存了毫秒 reloadingData
所以它不会在您调用 reloadData()
两次时重新加载两次,同样基于 apple developer documentation:
You should not call this method in the middle of animation blocks where items are being inserted or deleted. Insertions and deletions automatically cause the collection’s data to be updated appropriately.
我有一些解决方法可以实现此目的:
使 collectionView 布局无效:
据我所知里面有remove cache的方法collectionViewLayout prepare
collectionView!.reloadData()
collectionView!.collectionViewLayout.invalidateLayout()
collectionView!.layoutSubviews()
我有一个 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()
,发生这种情况时也不会调用数据源和委托方法 (numberOfItems
/cellForItem
)。
知道发生了什么吗?
感谢您的帮助!
编辑:我可以确保 viewDidLoad
/appear
方法中没有任何其他代码,pickFile
函数实际上被调用得很好并且 url 被正确获取,urls
数组在调用 reloadData()
.
另外,我在 UITableView
和 UICollectionView
上都试过了,我在每种情况下都遇到了这个问题。我使用的是 PDFView
或文档选择器,感觉好像出了点问题。
这是由您的单元格中的 PDFView 引起的,具体原因我不确定。 PDFView 可能对于在集合视图单元格中使用来说过于重量级,或者在重新加载或重新添加文档时可能会出现一些问题。我重现了你的问题,但没有设法解决它,但你可能需要牢记以下几点:
- 您正在为文档选择器使用已弃用的初始化程序
- 您得到的“收件箱”url 不能保证持久存在,实际上您可以看到随着 select 更多文件的出现和消失,所以您可能想要考虑将 selected 文件移动到更永久的位置
解决这些问题对奇怪的重新加载问题没有影响。我所做的是 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 缩略图,同时提高质量。
这是一个非常非常奇怪的错误,当您在 UICollectionViewCell
中使用 PDFView
时会发生这种错误。我在以下环境中确认了这一点 -
- Xcode12.5
- iPhone SE 2020 (iOS 14.6)
UICollectionView.reloadData()
调用 无法可靠地工作 当 PDFView
作为子视图添加到 UICollectionViewCell.contentView
.
我们还能尝试什么?
令人惊讶的是 UICollectionView.insertItems(at:)
在 UICollectionView.reloadData()
不适用于这种情况的情况下有效。在这个答案的末尾提供了工作代码示例,供任何其他试图reproduce/confirm这个问题的人使用。
为什么会发生这种情况?
老实说不知道。 UICollectionView.reloadData()
应该保证 UI 与您的数据源同步。让我们看看 reloadData()
的堆栈跟踪(在这种情况下有效)和 insertItems(at:)
.
ReloadData_StackTrace
InsertItemsAtIndexPaths_StackTrace
结论
reloadData()
依靠layoutSubviews()
来执行 UI 刷新。这是从UIView
继承而来的,比如 -UIView.layoutSubviews() > UIScrollView > UICollectionView
。这是一个众所周知的 UI 事件,可以很容易地被UIView
的任何子类拦截。PDFView: UIView
也可以做到。 为什么会出现不一致的情况?只有会拆机检查PDFKit.framework
的人才知道。这显然是PDFView.layoutSubviews()
实现中的一个错误,它干扰了其父视图的layoutSubviews()
实现。insertItems(at:)
在指定的 indexPath(s) 处添加单元格的新实例并且显然不依赖于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
}()
}
collectionView 以某种方式缓存了毫秒 reloadingData
所以它不会在您调用 reloadData()
两次时重新加载两次,同样基于 apple developer documentation:
You should not call this method in the middle of animation blocks where items are being inserted or deleted. Insertions and deletions automatically cause the collection’s data to be updated appropriately.
我有一些解决方法可以实现此目的:
使 collectionView 布局无效:
据我所知里面有remove cache的方法collectionViewLayout prepare
collectionView!.reloadData()
collectionView!.collectionViewLayout.invalidateLayout()
collectionView!.layoutSubviews()