简体   繁体   English

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

[英]UITableView UIRefreshControl with Large Title navigation does not stick to top - it moves

I have used UITableView.refreshControl with large titles.我使用了带有大标题的 UITableView.refreshControl。 I am trying to mimic the way the native Mail app works with pull to refresh.我试图模仿本机邮件应用程序与拉刷新的工作方式。 The refresh control, for me, moves and doesn't stay stuck at the top of the screen like the Mail app does.对我来说,刷新控件会移动,并且不会像邮件应用程序那样停留在屏幕顶部。 Here is a playground:这是一个游乐场:

//: 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

How can I get the spinner to stick to the top, and have it behave like the Mail app does?我怎样才能让微调器粘在顶部,并让它像邮件应用程序一样运行? Also the navigation title does this weird thing where it moves back slower than the table data and causes some overlap to happen... Any help is appreciated!导航标题也做了这个奇怪的事情,它比表格数据移动得慢,并导致发生一些重叠......感谢任何帮助!

EDIT JUNE 7, 2020编辑 2020 年 6 月 7 日

I switched to using a diffable datasource which fixes the overlap animation, however, there is still this weird glitch right when the haptic feedback occurs and causes the spinner to shoot up to the top and back down, so my original question still remains - how do we get the spinner to stay pinned at the top like the mail app.我切换到使用修复重叠 animation 的 diffable 数据源,但是,当触觉反馈发生时仍然存在这个奇怪的故障并导致微调器向上射到顶部并返回,所以我原来的问题仍然存在 - 怎么做我们让微调器像邮件应用程序一样保持在顶部。 Thanks for taking a look at this unique issue:)!!!感谢您查看这个独特的问题:)!!!

The reason is in used reloadData which synchronously drops & rebuilds everything thus breaking end refreshing animation.原因在于使用的reloadData同步删除并重建所有内容,从而打破了结束刷新 animation。

The possible solution is to give time to end animation and only then perform reload data.可能的解决方案是给结束 animation 的时间,然后才执行重新加载数据。

Tested with Xcode 11.4 / Playground使用 Xcode 11.4 / 游乐场测试

演示

Modified code:修改后的代码:

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

Alternate approach , if you known modified data structure, would be to use beginUpdate/endUpdate pair另一种方法,如果您知道修改过的数据结构,将使用beginUpdate/endUpdate

演示2

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

I don't really know how to answer the first question, but since you asked 2 questions I'll go ahead and try to help you with the second in the meanwhile.我真的不知道如何回答第一个问题,但既然你问了 2 个问题,我会提前 go 并尝试帮助你同时解决第二个问题。

The problem with the rows getting to the top too fast is due to the use of tableView.reloadData() .行到达顶部太快的问题是由于使用了tableView.reloadData() That is always done without animation, so what is happening is to be expected.这总是在没有 animation 的情况下完成,所以发生的事情是可以预料的。

What you could do is to use other methods on the table view, like insert or delete rows like this:您可以做的是在表格视图上使用其他方法,例如插入或删除这样的行:

@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().map{IndexPath(row: self.data.count+$0.offset, section: 0)}

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

In this way the rows will be added with an animation and the scroll down animation won't be interrupted by any reloadData() .这样,行将添加 animation 并且向下滚动 animation 不会被任何reloadData()中断。

If you don't have only rows to add (or delete) and maybe have to do some sort of combination of both, then you can use the performBatchUpdates method to do both add and delete in the same animation block.如果您不仅要添加(或删除)行,并且可能必须对两者进行某种组合,那么您可以使用performBatchUpdates方法在同一个 animation 块中进行添加和删除。 (iOS 11+) (iOS 11+)

That's, of course, only viable if you are able to calculate the indexes of the items to add and delete.当然,这只有在您能够计算要添加和删除的项目的索引时才可行。 Keep in mind that the amount of rows you add and delete should always be reflected by the actual change in your data structure, or you'll have a nasty exception thrown by the table view.请记住,您添加和删除的行数应始终反映在数据结构中的实际更改中,否则您将遇到由表视图引发的令人讨厌的异常。

Anyway that should always be feasible, but if you want it out of the box you could use iOS 13 diffable data source or, if you want it even before iOS 13, you could use instagram's IGListKit .无论如何,这应该总是可行的,但如果你想开箱即用,你可以使用 iOS 13 diffable 数据源,或者,如果你甚至在 iOS 13 之前想要它,你可以使用 instagram 的IGListKit

Both pretty much use the same concept of diffing your array of input and automatically detect deletions and inserts.两者几乎都使用相同的概念,即区分输入数组并自动检测删除和插入。 So changing the array will automatically reflect in the animated change in your tableView rows.因此,更改数组将自动反映在您的 tableView 行中的动画更改中。 Of course both of them are very different ways of implementing a tableView, so you really can't just change a little piece of your code to make it work.当然,它们都是实现 tableView 的非常不同的方式,所以你真的不能只改变你的一小部分代码来让它工作。

Hope this can help.希望这能有所帮助。

I think the problems is tableView.reloadData() .我认为问题是tableView.reloadData() You can change it to tableView.beginUpdates() and tableView.endUpdates()您可以将其更改为tableView.beginUpdates()tableView.endUpdates()

In this demo, I will create an extension for easier for tableView to reload with beginUpdate and enUpdates.在这个演示中,我将创建一个扩展,以便更轻松地使用 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()
    }

}

Then you just need to change for function refreshIt()然后你只需要更改为 function 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)
        }
    }

Happy coding快乐编码

One way to solve this natively.一种本地解决此问题的方法。

First of all mail application is using UITableViewController instead of UIViewController .首先,邮件应用程序使用UITableViewController而不是UIViewController

By changing your UIViewController to UITableViewController you will be able to implement the UIRefreshControl in about 30 seconds with the desired behavior.通过将UIViewController更改为UITableViewController ,您将能够在大约 30 秒内以所需的行为实现UIRefreshControl

Note: this is only achievable by storyboards for now ( if anyone could find a way programmatically please update my answer )注意:目前这只能通过情节提要来实现(如果有人能以编程方式找到方法,请更新我的答案)

Now let's dig into it.现在让我们深入研究一下。

After you change the to UITableViewController on the inspector the right panel you have a property called Refreshing with a drop-down to be disabled by default change that to Enabled在检查器右侧面板上更改为UITableViewController后,您有一个名为Refreshing的属性,默认情况下禁用下拉菜单将其更改为Enabled 在此处输入图像描述

And a UIRefreshControl view will appear in your left view hierarchy like this UIRefreshControl视图将像这样出现在您的左视图层次结构中在此处输入图像描述

Now simply drag a IBAction from that view into your class and reload table and end refresh just like below.现在只需将该视图中的IBAction拖入您的 class 并重新加载表并结束刷新,如下所示。

   @IBAction func refresh(_ sender: UIRefreshControl) {
    tableView.reloadData()
    sender.endRefreshing()
}

Another solution is to make a custom UIView that has UIRefreshControl stuck in the desired position and simply adding it to the UITableView but this is a long implementation process that I might not be the one to help with.另一种解决方案是制作一个自定义的UIView ,它的UIRefreshControl卡在所需的 position 中,然后简单地将其添加到UITableView中,但这是一个漫长的实现过程,我可能不会有帮助。

Side note: You can fix the size of the large title also to make it exactly like the mail app using navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: )]旁注:您也可以使用 navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: )] 修复大标题的大小,使其与邮件应用程序完全相同

This should also remove the refresh glitch in title.这也应该消除标题中的刷新故障。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 带有导航栏大标题的UITableView怪异的滚动行为,滚动到顶部时,顶部的反弹效果自动切断/生涩 - UITableView weird scroll behaviour with navigation bar large title, bounce effect at the top auto cut off / jerky when scroll to top 拉入UICollectionView后,UIRefreshControl不会粘住 - UIRefreshControl does not stick after pulled in UICollectionView 使用大标题时,不会触发UIScrollView中的UIRefreshControl - UIRefreshControl in a UIScrollView is not triggered when Large title is used 如何将UITableView的顶部粘贴到UISearchBar的底部? - How to stick top of the UITableView to the bottom of the UISearchBar? UITableView将tableHeaderView和sectionHeaderView放在顶部 - UITableView stick tableHeaderView and sectionHeaderView together on top 启用大标题时,UIRefreshControl endRefresh跳转 - UIRefreshControl endRefresh jumps when used with Large Title enabled UIRefreshControl在具有大标题的导航控制器中不显示横向 - UIRefreshControl not showing in landscape when in a navigation controller with large titles 导航栏中带有大标题或搜索栏的 UIRefreshControl 的故障动画 - Glitchy animation of UIRefreshControl with large titles or searchbar in navigation bar UITableView UIRefreshControl第一次不显示它的视图 - UITableView UIRefreshControl Does Not Show Its View The First Time UITableView使用UIRefreshControl闪烁 - UITableView Flickering with UIRefreshControl
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM