material-components-ios icon indicating copy to clipboard operation
material-components-ios copied to clipboard

[FlexibleHeader] Header jumping on scroll up after UITableView reloadData.

Open fanij opened this issue 4 years ago • 0 comments

Using MDCFlexibleHeaderView, with set 'headerView.canAlwaysExpandToMaximumHeight = true'. After reloadData is called on UITableView (and table has more rows than before), when scrolling up to the top of the table view flexible header is jumping. Getting the same result if I use headerView.shiftBehavior = .enabled.

Reproduction steps

  1. Using the following code, add to catalog, open the 'App Bar' - 'Issue with UITableView reloadData' example.
  2. When scrolling to the bottom of UITableView, we have endless scroll - in the example number of rows is increased and UITableView 'reloadData' method is called.
  3. Scroll up to the top of the table view.

Expected behavior

Flexible header should be visible all the time.

Actual behavior

Flexible header is jumping (hiding and showing): flexible-app-bar-issue

Platform (please complete the following information)

  • Device: iPhone7, iPhone12
  • OS: iOS14.2

Code to repro:

// Copyright 2021-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import UIKit
import CoreGraphics

import MaterialComponents.MaterialTabs

class HeaderTabViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var headerView: MDCFlexibleHeaderView?
    var tableView = UITableView()
    var tableRows = 50

    override func loadView() {
        super.loadView()
        
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        tableView.delegate = self
        tableView.dataSource = self
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableRows
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel!.text = "table first: Row \(indexPath.item)"
        cell.backgroundColor = .gray
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 200
    }
    
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        let lastSectionIndex = tableView.numberOfSections - 1
        let lastRowIndex = tableView.numberOfRows(inSection: lastSectionIndex) - 1
        if indexPath.section ==  lastSectionIndex && indexPath.row == lastRowIndex
            && tableView.contentOffset.y > 0 {
            loadMoreRows()
        }
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        headerView?.trackingScrollDidScroll()
    }

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        headerView?.trackingScrollDidEndDecelerating()
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        headerView?.trackingScrollWillEndDragging(withVelocity: velocity, targetContentOffset: targetContentOffset)
    }

    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        headerView?.trackingScrollDidEndDraggingWillDecelerate(decelerate)
    }
    
    func loadMoreRows(){
        if  tableRows == 50 {
            tableRows += 50
            tableView.reloadData()
        }
    }
}

class HeaderTabBarExample: UIViewController {

  lazy var appBarViewController: MDCAppBarViewController = self.makeAppBar()

  let tab = HeaderTabViewController()

  override func viewDidLoad() {
    super.viewDidLoad()

    view.backgroundColor = UIColor.white
    view.addSubview(appBarViewController.view)
    appBarViewController.didMove(toParent: self)

    self.title = "Title"

    self.appBarViewController.headerView.trackingScrollWillChange(toScroll: tab.tableView)

    self.view.addSubview(tab.view)
    self.view.sendSubviewToBack(tab.view)
    tab.didMove(toParent: self)

    tab.headerView = self.appBarViewController.headerView

    let tabView = tab.view!
    tabView.translatesAutoresizingMaskIntoConstraints = false
    tabView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
    tabView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
    tabView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
    tabView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true

    self.appBarViewController.headerView.trackingScrollView = tab.tableView
  }

  // MARK: Private

  func makeAppBar() -> MDCAppBarViewController {
      let appBarViewController = MDCAppBarViewController()

    self.addChild(appBarViewController)

      // Give the tab bar enough height to accomodate all possible item appearances.
      appBarViewController.headerView.minMaxHeightIncludesSafeArea = false
      appBarViewController.headerView.canAlwaysExpandToMaximumHeight = true

      appBarViewController.headerView.minimumHeight = 56
      appBarViewController.headerView.maximumHeight = 128
      appBarViewController.headerStackView.setNeedsLayout()

      return appBarViewController
  }

    override var childForStatusBarStyle: UIViewController? {
      return appBarViewController
  }
}

extension HeaderTabBarExample {

    @objc class func catalogMetadata() -> [String: Any] {
        return [
            "breadcrumbs": ["App Bar", "Issue with UITableView reloadData"],
            "primaryDemo": false,
            "presentable": true,
        ]
    }

    func catalogShouldHideNavigation() -> Bool {
        return true
    }
}

fanij avatar Feb 18 '21 17:02 fanij