带有大标题导航的 UITableView UIRefreshControl 不会停留在顶部 - 它会移动

Posted

技术标签:

【中文标题】带有大标题导航的 UITableView UIRefreshControl 不会停留在顶部 - 它会移动【英文标题】:UITableView UIRefreshControl with Large Title navigation does not stick to top - it moves 【发布时间】:2020-06-03 12:27:50 【问题描述】:

我使用了带有大标题的 UITableView.refreshControl。我试图模仿本机邮件应用程序与拉刷新的工作方式。对我来说,刷新控件会移动,并且不会像邮件应用程序那样停留在屏幕顶部。这是一个游乐场:

//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class ViewController: UIViewController 
    public init() 
        super.init(nibName: nil, bundle: nil)
    
    required init?(coder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    
    private lazy var refresh: UIRefreshControl = 
        let refreshControl = UIRefreshControl()
        refreshControl.backgroundColor = .clear
        refreshControl.tintColor = .black
        refreshControl.addTarget(self, action: #selector(refreshIt), for: .valueChanged)
        return refreshControl
    ()
    private lazy var tableView: UITableView = 
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.refreshControl = refresh
        tableView.delegate = self
        tableView.dataSource = self
        return tableView
    ()
    private var data: [Int] = [1,2,3,4,5]

    override func viewDidLoad() 
        super.viewDidLoad()
        view.backgroundColor = .white
        // Add tableview
        addTableView()

        navigationController?.navigationBar.prefersLargeTitles = true
        navigationItem.title = "View Controller"
    
    private func addTableView() 
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
            tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    
    @objc func refreshIt(_ sender: Any) 
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) 
            self.refresh.endRefreshing()
            self.data = [6,7,8,9,10]
            self.tableView.reloadData()
        
    

extension ViewController: UITableViewDelegate, UITableViewDataSource 
    func numberOfSections(in tableView: UITableView) -> Int 
        1
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        return self.data.count
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
        let cell = UITableViewCell()
        cell.textLabel?.text = "Row \(data[indexPath.row])"
        return cell
    

let vc = ViewController()

let nav = UINavigationController(rootViewController: vc)

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = nav

我怎样才能让微调器粘在顶部,并让它像邮件应用一样运行?导航标题也做了这个奇怪的事情,它比表格数据慢,并导致一些重叠发生......任何帮助表示赞赏!

2020 年 6 月 7 日编辑

我切换到使用可修复重叠动画的 diffable 数据源,但是,当触觉反馈发生时仍然存在这个奇怪的故障并导致微调器向上弹射并返回,所以我原来的问题仍然存在- 我们如何让微调器像邮件应用程序一样保持在顶部。感谢您查看这个独特的问题:)!!!

【问题讨论】:

你试过我的答案了吗? 【参考方案1】:

原因在于使用了reloadData,它同步删除并重建所有内容,从而破坏了结束刷新动画。

可能的解决方案是给时间结束动画,然后才执行重新加载数据。

使用 Xcode 11.4 / Playground 测试

修改代码:

@objc func refreshIt(_ sender: Any) 
    // simulate loading
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) 
        self.refresh.endRefreshing() // << animating

        // simulate data update
        self.data = [6,7,8,9,10]

        // forcefully synchronous, so delay a bit
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) 
            self.tableView.reloadData()
        
    

替代方法,如果您知道修改过的数据结构,将是使用beginUpdate/endUpdate

@objc func refreshIt(_ sender: Any) 
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) 
        self.refresh.endRefreshing()
        self.data = [6,7,8,9,10]

        // more fluent, but requires knowledge of what is modified,
        // in this demo it is simple - first section
        self.tableView.beginUpdates()
        self.tableView.reloadSections(IndexSet([0]), with: .automatic)
        self.tableView.endUpdates()
    

【讨论】:

刷新控件还在上下移动。我需要它保持在顶部。我转而使用可区分的数据源,它消除了奇怪的动画重叠。更大的问题是微调器没有卡在顶部。 @crizzwald,标准刷新控制布局由 UITableView 管理 @Asperi 谢谢,当我将 CollectionView 配置为旧方式(代表)时,延迟起作用了。但是当我使用 Diffable 数据源时,我该如何处理这个问题,因为快照应用于其他一些函数【参考方案2】:

我真的不知道如何回答第一个问题,但既然你问了两个问题,我会继续尝试帮助你解决第二个问题。

行到达顶部太快的问题是由于使用了tableView.reloadData()。 这总是在没有动画的情况下完成的,所以发生的事情是意料之中的。

您可以做的是在表格视图上使用其他方法,例如插入或删除这样的行:

@objc func refreshIt(_ sender: Any) 
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) 
            self.refresh.endRefreshing()


            let newData = [6,7,8,9,10]
            let newIndexPaths = newData.enumerated().mapIndexPath(row: self.data.count+$0.offset, section: 0)

            self.data.append(contentsOf: newData)
            self.tableView.insertRows(at: newIndexPaths, with: .automatic)
        
    

这样,行将添加动画,并且向下滚动动画不会被任何reloadData()打断。

如果您不仅要添加(或删除)行,并且可能必须将两者结合起来,那么您可以使用performBatchUpdates 方法在同一个动画块中进行添加和删除。 (ios 11+)

当然,这只有在您能够计算要添加和删除的项目的索引时才可行。请记住,您添加和删除的行数应始终反映在数据结构的实际更改中,否则表格视图将引发令人讨厌的异常。

无论如何,这应该总是可行的,但如果你想要它开箱即用,你可以使用 iOS 13 diffable data source,或者,如果你甚至在 iOS 13 之前想要它,你可以使用 instagram 的 IGListKit。

两者几乎都使用相同的概念,即区分输入数组并自动检测删除和插入。因此,更改数组将自动反映在您的 tableView 行中的动画更改中。当然,它们都是实现 tableView 的非常不同的方式,所以你真的不能只改变一小段代码来让它工作。

希望这能有所帮助。

【讨论】:

我切换到一个 diffable 数据源(在看到这个之前)并正在使用 datasource.apply,但是,当我下拉刷新时,我仍然会遇到这个奇怪的故障,只要新鲜触发器(就像当你感觉到触觉反馈时一样)一切都会上下跳跃。我还尝试切换到平移控件,以了解我何时抬起手指以开始任何类型的数据切换。看来,如果微调器会保持在顶部,这将不会发生,因为微调器的跳跃看起来会导致怪异。我也试过不结束刷新,没有骰子。 @crizzwald 您也可以尝试分享新实现的代码,以便我们更好地帮助您【参考方案3】:

我认为问题出在tableView.reloadData()。可以改成tableView.beginUpdates()tableView.endUpdates()

在这个演示中,我将创建一个扩展,以便更轻松地使用 beginUpdate 和 enUpdates 重新加载 tableView。

extension UITableView 

    func reloadWithNewData<T:Equatable>(_ newDatas : [T], _ oldDatas : [T],_ animation : UITableView.RowAnimation) 
        var deleteOnes : [IndexPath] = []
        var appendOnes : [IndexPath] = []

        for i in 0 ..< oldDatas.count 
            for j in 0 ..< newDatas.count 
                if newDatas[j] == oldDatas[i] 
                    break
                

                if j == newDatas.count - 1 
                    deleteOnes.append(IndexPath(row: i, section: 0))
                
            
        

        for i in 0 ..< newDatas.count 
            for j in 0 ..< oldDatas.count 
                if newDatas[i] == oldDatas[j] 
                    break
                

                if j == oldDatas.count - 1 
                    appendOnes.append(IndexPath(row: i, section: 0))
                
            
        



        self.beginUpdates()
        self.deleteRows(at: deleteOnes, with: animation)
        self.insertRows(at: appendOnes, with: animation)
        self.endUpdates()
    


那么你只需要更改函数refreshIt()

@objc func refreshIt(_ sender: Any) 
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) 
            self.refresh.endRefreshing()
            let oldData = self.data
            self.data = [6,7,8,9,10]
            self.tableView.reloadWithNewData(self.data, oldData, .fade)
        
    

愉快的编码

【讨论】:

以上是关于带有大标题导航的 UITableView UIRefreshControl 不会停留在顶部 - 它会移动的主要内容,如果未能解决你的问题,请参考以下文章

带有 UISearchController 的 UITableView 在进入结果视图并返回时进入导航栏

黑色半透明导航栏/UITableView/内容插入/滚动位置问题

UiTableView 作为 iOS 中的导航方法

从 iOS 11 搜索控制器导航时不需要的 UITableView 重新加载动画

UIScrollview有一个大视图,还是带有多个视图的UITableview?

带有大标题的导航栏上的动画segue错误