'UICollectionView + UIRefreshControl animation differences to Apple Mail app

I am implementing a ViewController with an UICollectionView + UIRefreshControl and large titles. Its pull to refresh animation looks different compared to the ones from Apple Apps like Mail.

In the mail app the UIActivityIndicator is somehow "building up" based on how far the user has dragged, whereas in my sample implementation the indicator is fading in. I already tried to play with the top anchors or different ways to trigger the refresh but I cannot get the "native" animation of the UIRefreshControl. At the following link, 2 videos are shown, one showing the mail app animation, the other one my custom view implementations: https://imgur.com/a/ekfmZBu

Are there any hidden configurations or maybe some other ways to implement this "native" animation. What I don't know though is whether the Apple mail app is using an UICollectionView. I also tried it with a UITableView and was also facing the "fading in" animation rather than this "growing" animation.

Maybe someone already had experienced this and knows what could be the issue.

Any help is appreciated.

EDIT:

This is the code of the View Controller that is creating the UICollectionView. It also contains some references to a ViewModel and some custom types, but these should not interfere with the UI differences I see.

class CollectionViewTestViewController: UIViewController {

    // MARK: - UI Properties
    private lazy var collectionView = makeCollectionView()
    private lazy var dataSource = makeDataSource()

    private(set) lazy var refreshControl: UIRefreshControl = {
        let refreshControl = UIRefreshControl()
        refreshControl.addAction(
                UIAction { [weak self] _ in
                    self?.viewModel.refresh()
                }, for: .valueChanged
        )
        return refreshControl
    }()

    // MARK: - ViewModel
    private let viewModel: CollectionViewTestViewModel

    // MARK: - Combine
    private let cancelBag = CancelBag()

    // MARK: - Initializer
    init(viewModel: CollectionViewTestViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)

        self.title = "CollectionView"
    }

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

    // MARK: - Lifecylce
    override func viewDidLoad() {
        super.viewDidLoad()
        self.configureUI()
        self.bindToViewModel()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
        self.navigationController?.navigationBar.prefersLargeTitles = true
    }
}

// MARK: - UI Setup
extension CollectionViewTestViewController {

    private func configureUI() {
        view.backgroundColor = .systemBackground

        view.addSubview(collectionView)
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
        ])
    }
}

// MARK: - Collection View Handling
extension CollectionViewTestViewController {

    // MARK: - UICollectionView
    func makeCollectionView() -> UICollectionView {
        let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        let layout = UICollectionViewCompositionalLayout.list(using: config)
        let view =  UICollectionView(frame: .zero, collectionViewLayout: layout)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.refreshControl = refreshControl
        view.contentInset = .init(top: 16, left: 0, bottom: 0, right: 0)

        return view
    }


    // MARK: - Cell Registration
    func makeCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, Contact> {
        UICollectionView.CellRegistration { cell, indexPath, contact in
            // Configuring each cell's content:
            var config = cell.defaultContentConfiguration()
            config.text = contact.name
            config.secondaryText = "\(contact.birthday)"
            cell.contentConfiguration = config

            // Showing a disclosure indicator as the cell's accessory:
            cell.accessories = [.disclosureIndicator()]
        }
    }

    // MARK: - Data Source
    func makeDataSource() -> UICollectionViewDiffableDataSource<Section, Contact> {
        let cellRegistration = makeCellRegistration()

        return UICollectionViewDiffableDataSource<Section, Contact>(
            collectionView: collectionView,
            cellProvider: { view, indexPath, item in
                view.dequeueConfiguredReusableCell(
                    using: cellRegistration,
                    for: indexPath,
                    item: item
                )
            }
        )
    }
}

// MARK: - MVVM
extension CollectionViewTestViewController {
    private func bindToViewModel() {
        self.viewModel.$contacts
            .receive(on: RunLoop.main)
            .sink { [weak self] sections in
                switch sections {
                case .initial:
                    break
                case .loading:
                    self?.collectionView.refreshControl?.beginRefreshing()
                case let .value(data):
                    self?.collectionView.refreshControl?.endRefreshing()
                    self?.updateList(with: data)
                case let .error(error):
                    self?.collectionView.refreshControl?.endRefreshing()
                }
            }
            .store(in: self.cancelBag)
    }
}

// MARK: - Data Updating
extension CollectionViewTestViewController {
    func updateList(with data: [Contact]) {
           var snapshot = NSDiffableDataSourceSnapshot<Section, Contact>()
           snapshot.appendSections(Section.allCases)
           snapshot.appendItems(data, toSection: .all)
           dataSource.apply(snapshot)
       }
}



Solution 1:[1]

You don't have to work with top anchors. The "growing" animation is a result of the valueChanged event of UIRefreshControl. Add a target to your UIRefreshControl and set it to your UICollectionView like this:

refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
collectionView.refreshControl = refreshControl

And for the "refresh" function:

@objc func refresh(sender: UIRefreshControl) {
   // End refreshing at some point
   self.refreshControl.endRefreshing()
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 yjoon