弹跳改变行时发生

问题描述 投票:15回答:6

毛刺

我使用CoreDataNSFetchResultController有一个UITableView显示的数据。我有一个问题:UITableView的变化时插入一个新的行/移动/删除的contentOffSet.y。当用户已滚动于,例如中间,当插入新行的UITableView反弹。

复制项目

这github上链接到一个项目,该项目包含最小代码重现此问题:https://github.com/Jasperav/FetchResultControllerGlitch(代码是向下跌破以及)

这正显示出毛刺。我站在我的UITableView的中间,我不断看到新行插入,而不考虑当前contentOffSet.y的:

enter image description here

类似的问题

关注

我也试着开关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
    }()
}
ios swift uitableview core-data nsfetchedresultscontroller
6个回答
1
投票
   let lastScrollOffset = tableView.contentOffset;
   tableView.beginUpdates();
   tableView.insertRows(at: [newIndexPath!], with: .automatic);
   tableView.endUpdates();
   tableView.layer.removeAllAnimations();
   tableView.setContentOffset(lastScrollOffset, animated: false);
  1. 做最好的,你可以建立高度估计为所有的表格单元格类型。即使高度有些动态这有助于UITableView的。
  2. 保存您的滚动位置并更新你的tableView,使()调用endUpdates后重置内容偏移。

您还可以检查此tutorial


0
投票

我已经成功地做到这一点。

  • 停止,如果用户向下滚动表视图应用更新,通过移除resultController.delegate
  • 重新启动应用,如果用户是回表视图上,通过重新设定resultController.delegate
  • 禁用时间之间的同步的diff

缺点是禁用取还禁用或更新现有的行删除。取重启后这些变化将被应用。

我也尝试对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
    }()
}

0
投票

做这个

tableView.bounces = false

它会工作


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)
    }
  }
}

这就是它!这不会做你的时候,当视图是完全滚动到顶部,但我相信你可以自己处理这种情况。


0
投票

你可以试试这个它上面POOJA的回答一个编辑,我所面临的问题像你这样的UIView.performWithoutAnimation消除了me.Hope它有助于问题。

 UIView.performWithoutAnimation {

        let lastScrollOffset = tableView.contentOffset;
        tableView.beginUpdates();
        tableView.insertRows(at: [newIndexPath!], with: .automatic);
        tableView.endUpdates();
        tableView.setContentOffset(lastScrollOffset, animated: false); 
    }

编辑

你也可以尝试上述但不是插入行,你可以使用重载数据上的tableview但在这之前获取的数据添加到您的数据源,并设置块内的最后contentoffeset。


0
投票

表视图是一个复杂的野兽。它的行为不同,这取决于它的配置。表视图调整内容时插入,更新,删除和移动的行偏移。如果表视图表格视图控制器内使用的滚动视图的委托方法scrollViewDidScroll(_ :)被调用。

该解决方案是撤销内容偏移调整在那里。然而,这是对表视图的意图,因此需要做几次,直到viewDidLayoutSubviews()被调用。所以,解决的办法是不是最佳的,但它的工作原理与动力高度的细胞,章节标题,页脚部分,并应符合你的目标。

对于解决办法,我已经重建你的代码。您的视图控制器不再基于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
    }()
}
© www.soinside.com 2019 - 2024. All rights reserved.