Search code examples
iosswiftuicollectionviewuiscrollviewuirefreshcontrol

UIRefreshControl endRefresh jumps when used with Large Title enabled


I'm trying to use UIRefreshControl, but when I call endRefreshing() it jumps the UINavigationBar. The problem only happens when I use UIRefreshControl along with large titles. Looking at some similar issues (UIRefreshControl glitching in combination with custom TableViewCell) reported here, I tried to refresh only after dragging ends, nevertheless, the bug still occurs. Also tried to use

self.navigationController?.navigationBar.isTranslucent = false and self.extendedLayoutIncludesOpaqueBars = true

But, none of the solutions found on other questions seems to resolve the problem, it still not smooth.

The video of what is happening:

https://www.youtube.com/watch?v=2BBRnZ444bE

The app delegate

import UIKit

@UIApplicationMain
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()
        let nav = UINavigationController()
        nav.title = "My Nav"
        nav.navigationBar.prefersLargeTitles = true
        nav.viewControllers = [ViewController()]
        window.rootViewController = nav

        self.window = window
        return true
    }

}

Observe that I'm using large titles:

        let nav = UINavigationController()
        nav.title = "My Nav"
        nav.navigationBar.prefersLargeTitles = true

The ViewController:

import UIKit
import Foundation

final class ViewController: UICollectionViewController {
    let randomHeight = Int.random(in: 100..<300)

    init() {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.estimatedItemSize = CGSize(width: 20, height: 20)
        super.init(collectionViewLayout: layout)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Try to refresh"

        self.navigationController?.navigationBar.isTranslucent = false
        self.extendedLayoutIncludesOpaqueBars = true

        collectionView.backgroundColor = .white
        registerCells()
        setupRefreshControl()
    }

    private func registerCells() {
        self.collectionView.register(
            Cell.self,
            forCellWithReuseIdentifier: "Cell"
        )
    }

    private func setupRefreshControl() {
        let refreshControl = UIRefreshControl()
        refreshControl.addTarget(
            self,
            action: #selector(refreshControlDidFire),
            for: .valueChanged
        )
        self.collectionView.refreshControl = refreshControl
    }

    @objc private func refreshControlDidFire(_ sender: Any?) {
        if let sender = sender as? UIRefreshControl, sender.isRefreshing {
            refresh()
        }
    }

    override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if collectionView.refreshControl!.isRefreshing {
            refresh()
        }
    }

    private func refresh() {
        if !collectionView.isDragging {
            collectionView.refreshControl!.endRefreshing()
            collectionView.perform(#selector(collectionView.reloadData), with: nil, afterDelay: 0.05)
        }
    }
}

extension ViewController {
    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    override func collectionView(
        _ collectionView: UICollectionView,
        numberOfItemsInSection section: Int
        ) -> Int {
        return 10
    }

    override func collectionView(_ collectionView: UICollectionView,
                                 cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(
            withReuseIdentifier: "Cell", for: indexPath
            ) as? Cell else {
                return UICollectionViewCell()
        }

        cell.label.text = "Text number \(indexPath.row), with height \(randomHeight)"
        cell.heightAnchorConstraint.constant = CGFloat(randomHeight)
        return cell
    }
}

extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
    }
}

final class Cell: UICollectionViewCell {
    private let shadowView = UIView()
    private let containerView = UIView()
    private let content = UIView()
    let label = UILabel()
    var heightAnchorConstraint: NSLayoutConstraint!

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        setupViews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupViews() {
        insertSubview(shadowView, at: 0)
        addSubview(containerView)
        containerView.addSubview(label)
        containerView.addSubview(content)
        activateConstraints()
    }

    private func activateConstraints() {
        self.translatesAutoresizingMaskIntoConstraints = false
        shadowView.translatesAutoresizingMaskIntoConstraints = false
        containerView.translatesAutoresizingMaskIntoConstraints = false
        label.translatesAutoresizingMaskIntoConstraints = false
        content.translatesAutoresizingMaskIntoConstraints = false

        shadowView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
        shadowView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
        shadowView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
        shadowView.bottomAnchor
            .constraint(equalTo: self.bottomAnchor).isActive = true

        containerView.backgroundColor = .white
        containerView.layer.cornerRadius = 14

        containerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
        containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
        containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
        containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
        let widthAnchorConstraint = containerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 20)
        widthAnchorConstraint.identifier = "Width ContainerView"
        widthAnchorConstraint.priority = .defaultHigh
        widthAnchorConstraint.isActive = true

        label.numberOfLines = 0
        label.textAlignment = .center
        label.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true

        content.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20).isActive = true
        content.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
        content.bottomAnchor.constraint(lessThanOrEqualTo: containerView.bottomAnchor, constant: -10).isActive = true
        heightAnchorConstraint = content.heightAnchor.constraint(greaterThanOrEqualToConstant: 220)
        heightAnchorConstraint.identifier = "Height Content"
        heightAnchorConstraint.priority = .defaultHigh
        heightAnchorConstraint.isActive = true
        content.widthAnchor.constraint(equalToConstant: 40).isActive = true
        content.backgroundColor = .red

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        applyShadow(width: 0.20, height: -0.064)
    }

    private func applyShadow(width: CGFloat, height: CGFloat) {
        let shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 14.0)
        shadowView.layer.masksToBounds = false
        shadowView.layer.shadowRadius = 8.0
        shadowView.layer.shadowColor = UIColor.black.cgColor
        shadowView.layer.shadowOffset = CGSize(width: width, height: height)
        shadowView.layer.shadowOpacity = 0.3
        shadowView.layer.shadowPath = shadowPath.cgPath
    }
}

Solution

  • The problem is related to layout.estimatedItemSize = CGSize(width: 20, height: 20)

    When we use AutoLayout to resize the cell, it creates a bug with UIRefreshControl and navigation bar large title. So, if you use layout.estimatedItemSize with an equal or greater size than we expected. So the bug will not happen and the glitch will not happen.

    Basically, the problem is when we call updateData but the cell is bigger than we expect and each cell of the UICollectinView will resize to a bigger size then the UICollectionViewController will glitches.