【发布时间】:2023-03-12 20:51:01
【问题描述】:
故障
我正在使用CoreData 和NSFetchResultController 来让数据显示在UITableView 中。我有一个问题:UITableView 在插入/移动/删除新行时更改了contentOffSet.y。当用户滚动到时,例如中间,UITableView 在插入新行时弹跳。
复制项目
此 github 链接指向包含重现此行为的最少代码的项目:https://github.com/Jasperav/FetchResultControllerGlitch(代码也在下方)
这是显示故障。我站在我的UITableView 中间,我不断地看到新行被插入,不管当前的contentOffSet.y。:
类似问题
-
How to prevent from scrolling UITableView up when NSFetchedResultsController add new record? 不相关,因为我明确设置了
rowHeight和estimatedRowHeight. -
Error: UITableView jump to top with UITableViewAutomaticDimension 在
endUpdates之前尝试过这个没有运气 -
UITableView powered by FetchedResultsController with UITableViewAutomaticDimension - Cells move when table is reloaded 与第一个链接相同,我设置了
rowHeight和estimatedRowHeight.
担忧
我也尝试切换到performBatchUpdates 而不是begin/endUpdates,但也没有成功。
UITableView 在用户看不到这些行时插入/删除/移动行时不应该移动。我希望这样的事情应该开箱即用。
最终目标
这就是我最终想要的(只是复制 WhatsApp 的聊天屏幕):
- 当用户完全滚动到插入新行的顶部(对于 WhatsApp,这是底部),
UITableView应该为新插入的行设置动画并更改当前的contentOffSet.y。 - 当用户没有完全滚动到顶部(或底部,取决于插入新行的位置)时,用户看到的单元格不应在插入新行时反弹。这对应用程序的用户体验非常不利。
- 它应该适用于动态高度单元格。
- 我在移动/删除单元格时也看到了这种行为。这里的所有故障都有什么简单的解决方法吗?
如果UICollectionView 更合适,那很好。
用例
我正在尝试复制 WhatsApp 聊天屏幕。我不确定他们是否使用 NSFetchResultController,但除此之外,最终目标是为他们提供准确的用户体验。因此,插入、移动、删除和更新单元格应该按照 WhatsApp 的方式进行。因此,对于一个工作示例:转到 WhatsApp,对于一个不工作示例:下载项目。
复制粘贴代码
代码(ViewController.swift):
import CoreData
import UIKit
class ViewController: UIViewController, NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate {
let tableView = MyTableView()
let resultController = ViewController.createResultController()
override func viewDidLoad() {
super.viewDidLoad()
// Initial cells
for i in 0...40 {
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = randomString(length: i + 1)
x.date = Date()
x.height = Float.random(in: 50...100)
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = self.randomString(length: Int.random(in: 10...50))
x.date = Date()
x.height = Float.random(in: 50...100)
}
resultController.delegate = self
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 75
try! resultController.performFetch()
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .update:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return resultController.fetchedObjects?.count ?? 0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat(resultController.object(at: indexPath).height)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
cell.textLabel?.text = resultController.object(at: indexPath).something
return cell
}
private static func createResultController() -> NSFetchedResultsController<SomeEntity> {
let fetchRequest: NSFetchRequest<SomeEntity> = SomeEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
return NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataContext.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0...length-1).map{ _ in letters.randomElement()! })
}
}
class MyTableView: UITableView {
init() {
super.init(frame: .zero, style: .plain)
register(MyTableViewCell.self, forCellReuseIdentifier: "cell")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyTableViewCell: UITableViewCell {
}
class CoreDataContext {
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FetchViewControllerGlitch")
container.loadPersistentStores(completionHandler: { (nsPersistentStoreDescription, error) in
guard let error = error else {
return
}
fatalError(error.localizedDescription)
})
return container
}()
}
【问题讨论】:
-
UIKit 太糟糕了。 :( 有时甚至不可能用标准组件使简单的事情工作。
-
只要玩
contentOffset,当你添加新行时,获取新行的高度并更新tableview的contentoffset以保持tableview的位置相同。确保你保持动画=假
标签: ios swift uitableview core-data nsfetchedresultscontroller