RefreshControl + large title doesn't work well in UITabBarController

I have UIViewController with NavigationBar that is a part of UITabController.

Inside UIViewController I have only UITableView. NavigationBar is transparent and blurred.

TableView top constraint is to superview, so when I scroll content it goes behind navigation bar.

Problem: loadData data triggered immediately when I start to scroll down. Right after I scroll few pixels down.

All works fine if I remove largeTitles, but with large titles it feels like refreshControl already at position when it ready to trigger .valueChanged

Also it works fine if to remove tab bar and load navigationController directly as root. But I need tabbar.

Full code:

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication,     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
        let window = UIWindow(frame: UIScreen.main.bounds)
        
        window.makeKeyAndVisible()
        self.window = window
        
        window.rootViewController = TabBarController()
        
        return true
    }
}

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let navigationController = UINavigationController(rootViewController: ViewController())
        navigationController.navigationBar.prefersLargeTitles = true
        
        viewControllers = [navigationController]
    }
}

class ViewController: UIViewController {
    private let tableView = UITableView()
    
    private let refreshControl = UIRefreshControl()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationItem.title = "Test"
        
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        tableView.delegate = self
        tableView.dataSource = self
        tableView.refreshControl = refreshControl
        refreshControl.addTarget(self, action: #selector(loadData), for: .valueChanged)
    }
    
    @objc func loadData() {
        print("loadData triggered")
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
            self.refreshControl.endRefreshing()
        }
    }
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell: UITableViewCell
        if let defaultCell = tableView.dequeueReusableCell(withIdentifier: "defaultCell") {
            cell = defaultCell
        } else {
            cell = UITableViewCell(style: .value1, reuseIdentifier: "defaultCell")
        }
        cell.textLabel?.text = "Test"
        return cell
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        10
    }
}

BUT

Everything works fine if same thing done from StoryBoard:

Post not yet marked as solved Up vote post of dbyst Down vote post of dbyst
1.1k views

Replies

I ran into this same thing as well and managed to fix it by putting this code in the view controller:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    navigationController?.navigationBar.prefersLargeTitles = true
}

This was driving me crazy for the past couple of weeks. Solution I found was to add the refresh control in viewDidAppear.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    let refresh = UIRefreshControl(frame: .zero)
    refresh.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
    myTableView.refreshControl = refresh
}