我正在将NSFetchResultController
与UITableView
配合使用,以将数据显示在UITableView
中。我有一个问题:在插入/移动/删除新行时,contentOffSet.y
会更改UITableView
。当用户滚动到时,例如在中间,插入新行时UITableView
会反弹。
此github链接到一个项目,该项目包含用于再现此行为的最少代码:https://github.com/Jasperav/FetchResultControllerGlitch(代码也在下面)
这显示出故障。我站在contentOffSet.y
的中间,并且不断看到插入新行,而与当前rowHeight
无关。
How to prevent from scrolling UITableView up when NSFetchedResultsController add new record?
不相关,因为我明确设置了estimatedRowHeight
并且
endUpdates
。
Error: UITableView jump to top with UITableViewAutomaticDimension
在rowHeight
之前没有运气的情况下尝试过此操作
UITableView powered by FetchedResultsController with UITableViewAutomaticDimension - Cells move when table is reloaded
与第一个链接相同,我设置了estimatedRowHeight
和
performBatchUpdates
。
我还尝试了切换到begin/endUpdates
而不是UITableView
的问题,但也没有解决。
当用户看不到这些行时,{/ {1}}不应在插入/删除/移动行时移动。我希望像这样的东西开箱即用。
这是我最终想要的(只是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
}()
}
答案 0 :(得分:0)
我已经做到了。
缺点是禁用提取,也禁用现有行的更新或删除。 这些更改将在重新获取后应用。
我还尝试过调整controller(_:didChange:at:for:newIndexPath:)
上的contentOffset,但是它根本不起作用。
代码紧随其后。
import CoreData
import UIKit
class ViewController: UIViewController, NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate {
let tableView = MyTableView()
let resultController = ViewController.createResultController()
var needsSync = false
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()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let threshold = CGFloat(100)
if scrollView.contentOffset.y > threshold && resultController.delegate != nil {
resultController.delegate = nil
}
if scrollView.contentOffset.y <= threshold && resultController.delegate == nil {
resultController.delegate = self
needsSync = true
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>) {
if needsSync {
tableView.reloadData()
}
tableView.beginUpdates()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if needsSync {
needsSync = false
}
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
}()
}
答案 1 :(得分:0)
这样做
tableView.bounces = false
它将起作用
答案 2 :(得分:0)
表视图是一个复杂的野兽。它的行为取决于其配置。表格视图在插入,更新,删除和移动行时调整内容偏移量。如果在表视图控制器中使用了表视图,则将调用scrollview委托方法scrollViewDidScroll(_:)。
解决方案是在那里撤销内容偏移量调整。但是,这违背了表视图的意图,因此需要执行多次,直到调用viewDidLayoutSubviews()为止。因此,该解决方案不是最佳解决方案,但它可用于动态高度单元,节标题和节脚,并且应与您的目标匹配。
对于该解决方案,我已经重建了您的代码。您的ViewController不再基于UIViewController,而是基于UITableViewController。该解决方案的必要部分是处理和使用属性fixUpdateContentOffset。
import CoreData
import UIKit
class ViewController: UITableViewController, NSFetchedResultsControllerDelegate {
let resultController = ViewController.createResultController()
private var fixUpdateContentOffset: CGPoint?
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
tableView.estimatedRowHeight = 75
try! resultController.performFetch()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
fixUpdateContentOffset = nil
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
fixUpdateContentOffset = nil
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let fixUpdateContentOffset = fixUpdateContentOffset,
tableView.contentOffset.y.rounded(.toNearestOrAwayFromZero) != fixUpdateContentOffset.y.rounded(.toNearestOrAwayFromZero) {
tableView.contentOffset = fixUpdateContentOffset
}
}
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>) {
fixUpdateContentOffset = tableView.contentOffset
tableView.beginUpdates()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
fixUpdateContentOffset = tableView.contentOffset
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return resultController.fetchedObjects?.count ?? 0
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat(resultController.object(at: indexPath).height)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
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 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
}()
}
答案 3 :(得分:0)
第1步:定义“不动”的意思。对于人类来说,很明显它正在跳跃。但是计算机发现contentOffset保持不变。因此,让我们非常精确,并定义具有可见顶部的第一个单元格应保持更改后的确切位置。其他所有单元格都可以移动,但这是我们的锚点。
var somethingIdOfAnchorPoint:String?
var offsetAnchorPoint:CGFloat?
func findHighestCellThatStartsInFrame() -> UITableViewCell? {
var anchorCell:UITableViewCell?
for cell in self.tableView.visibleCells {
let topIsInFrame = cell.frame.origin.y >= self.tableView.contentOffset.y
if topIsInFrame {
if let currentlySelected = anchorCell{
let isHigerUpInView = cell.frame.origin.y < currentlySelected.frame.origin.y
if isHigerUpInView {
anchorCell = cell
}
}else{
anchorCell = cell
}
}
}
return anchorCell
}
func setAnchorPoint() {
self.somethingIdOfAnchorPoint = nil;
self.offsetAnchorPoint = nil;
if let cell = self.findHighestCellThatStartsInFrame() {
self.offsetAnchorPoint = cell.frame.origin.y - self.tableView.contentOffset.y
if let indexPath = self.tableView.indexPath(for: cell) {
self.somethingIdOfAnchorPoint = resultController.object(at: indexPath).something
}
}
}
调用setAnchorPoint
时,我们会发现并记住哪个实体(不是indexPath
,因为它可能会很快发生变化)位于顶部附近,以及距顶部到底有多远。
接下来,让我们在更改发生之前立即致电setAnchorPoint
:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.setAnchorPoint()
tableView.beginUpdates()
}
更改完成后,我们滚动回到假定没有任何动画的位置:
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
self.tableView.layoutSubviews()
self.scrollToAnchorPoint()
}
func scrollToAnchorPoint() {
if let somethingId = somethingIdOfAnchorPoint, let offset = offsetAnchorPoint {
if let item = resultController.fetchedObjects?.first(where: { $0.something == somethingId }),
let indexPath = resultController.indexPath(forObject: item) {
let rect = self.tableView.rectForRow(at: indexPath)
let contentOffset = rect.origin.y - offset
self.tableView.setContentOffset(CGPoint.init(x: 0, y: contentOffset), animated: false)
}
}
}
就是这样!当视图完全滚动到顶部时,这不会做任何事情,但是我相信您可以自己处理这种情况。
答案 4 :(得分:0)
let lastScrollOffset = tableView.contentOffset;
tableView.beginUpdates();
tableView.insertRows(at: [newIndexPath!], with: .automatic);
tableView.endUpdates();
tableView.layer.removeAllAnimations();
tableView.setContentOffset(lastScrollOffset, animated: false);
尽力为所有表格单元格类型确定估计的高度。即使高度有些动态,这也有助于UITableView。
保存滚动位置,并在更新tableView并调用endUpdates()之后重置内容偏移。
您也可以选中此tutorial
答案 5 :(得分:0)
您可以尝试在pooja的答案上方进行编辑,我遇到了像您这样的问题, UIView.performWithoutAnimation 帮我消除了这个问题。希望对您有所帮助。
UIView.performWithoutAnimation {
let lastScrollOffset = tableView.contentOffset;
tableView.beginUpdates();
tableView.insertRows(at: [newIndexPath!], with: .automatic);
tableView.endUpdates();
tableView.setContentOffset(lastScrollOffset, animated: false);
}
编辑
您也可以尝试上面的方法,但是除了插入行之外,您还可以在tableview上使用reload数据,但在此之前将获取的数据追加到数据源中,并在块内设置最后一个contentoffetet。