Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have used UITableView.refreshControl with large titles. 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

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. Thanks for taking a look at this unique issue :)!!!

like image 330
crizzwald Avatar asked Jun 03 '20 12:06

crizzwald


1 Answers

The reason is in used reloadData which synchronously drops & rebuilds everything thus breaking end refreshing animation.

The possible solution is to give time to end animation and only then perform reload data.

Tested with Xcode 11.4 / Playground

demo

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

demo2

@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()
    }
}
like image 115
Asperi Avatar answered Nov 15 '22 05:11

Asperi