Search code examples
iosswiftuicollectionviewuikit

collectionView.collectionViewLayout.invalidateLayout()


I have a dynamic collectionView in TableView and there are an bug when I add invalidateLayout (picture is below) but if I delete invalidateLayout the first four cells are hidden and the layout is not displayed correctly, the video

please help I searched the whole stack but nothing helped thanks

if you need test project

MainViewController

class ViewController: UIViewController, UIScrollViewDelegate {


  override func viewDidLoad() {
        super.viewDidLoad()
        setting()
    }

  func setting(){

   getData()

   tableView.dataSource = self
   tableView.delegate = self

   tableView.estimatedRowHeight = UITableView.automaticDimension 
   tableView.rowHeight = UITableView.automaticDimension    

  }

         //load data
   func pagination(_ completion: (()->())?){

      SmartNetworkSevrice.getGoods(with: url) { [unowned self] (data) in
         guard data.modals.count > 0 else {
             self.tableView.tableFooterView = nil
             return
         }

         self.goods.append(contentsOf: data.modals)
         self.offSet += data.modals.count
         DispatchQueue.main.async {
             let indexPath = IndexPath(row: 0, section: 0)
             self.tableView.tableFooterView = nil
             if self.goods.count == data.modals.count || self.isRefresh {
                self.tableView.reloadRows(at: [indexPath], with: .none)
             } else {
                if let cell = self.tableView.cellForRow(at: indexPath) as? TVCellGoods {
                    UIView.performWithoutAnimation {
                        self.tableView.beginUpdates()
                        cell.insertGoods(data.modals)
                        cell.layoutIfNeeded()
                        cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height
                        self.tableView.endUpdates()
                     }

                }
             }
            completion?()
         }
     }

     // define bottom of tableView
    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        guard scrollView == self.tableView else {  return }
        if (!isMoreDataLoading) {

            // Вычислить позицию длины экрана до нижней части результатов
            let scrollViewContentHeight = scrollView.contentSize.height
            let scrollOffsetThreshold = scrollViewContentHeight - scrollView.bounds.size.height
            if(scrollView.contentOffset.y > scrollOffsetThreshold && scrollView.isDragging) {
                isMoreDataLoading = true
                self.tableView.isScrollEnabled = false;
                self.tableView.isScrollEnabled = true;
                pagination(nil)
            }
        }

       }

    }

extension ViewController: UITableViewDataSource {


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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "goods", for: indexPath) as? TVCellGoods else { return UITableViewCell() }
              guard bestGoods.count != 0, goods.count != 0 else { return UITableViewCell() }
            cell.delegate = self
            cell.configure(bestGoods, goods, categories)
            // автообновление высоты
            self.tableView.beginUpdates()
            cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height
            self.tableView.endUpdates()
            return cell 
    }

}

extension ViewController: UITableViewDelegate, CallDelegate {

    func callMethod() {}

    func callMethod(push vc:UIViewController) {
        self.navigationController?.pushViewController(vc, animated: true)
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return UITableView.automaticDimension
    }

    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
            return UITableView.automaticDimension
    }

}

TableViewCell with collectionView

class TVCellGoods: UITableViewCell {

    @IBOutlet weak var collectionView:UICollectionView!
    @IBOutlet weak var collectionViewHeight:NSLayoutConstraint!

    weak var delegate:CallDelegate?
    var bestGoods = [Goods]() // лучшие товары
    var goods = [Goods]() // все товары
    var categories = [Menu]()

    override func layoutSubviews() {
       super.layoutSubviews()
       self.collectionView.collectionViewLayout.invalidateLayout()
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.tag = 2
        collectionView.isScrollEnabled = false

    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

    func configure(_ best:[Goods],_ goods:[Goods], _ category:[Menu]) {

        self.bestGoods = best
        self.goods = goods
        self.categories = category
        self.collectionView.reloadData()

    }

    func insertGoods(_ data:[Goods]) {

        self.goods.append(contentsOf: data)
        let count = self.bestGoods.count + self.categories.count  + self.goods.count
        let indexPaths = ((count - data.count) ..< count)
            .map { IndexPath(row: $0, section: 0) }
        self.collectionView.performBatchUpdates({
            self.collectionView.insertItems(at: indexPaths)
        }, completion: nil)

    }

}

and CollectionViewCell

class CVCellGoods: UICollectionViewCell {

    @IBOutlet weak var bgView: UIView!
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var title: UILabel!
    @IBOutlet weak var price: UILabel!
    @IBOutlet weak var delivery: UIImageView!
    @IBOutlet weak var premium: UIImageView!

    override func prepareForReuse() {
        super.prepareForReuse()
        delivery.image = nil
        premium.image = nil
        title.text = nil
        price.text = nil
        imageView.image = nil
        imageView.sd_cancelCurrentImageLoad()
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        imageView.backgroundColor = UIColor(red: 215/255, green: 215/255, blue: 215/255, alpha: 1)

        self.contentView.layer.cornerRadius = 5
        self.contentView.layer.masksToBounds = true

        self.layer.shadowColor = UIColor.black.cgColor
        self.layer.shadowOffset = CGSize(width: 0, height: 0.5)
        self.layer.shadowRadius = 1
        self.layer.shadowOpacity = 0.3
        self.layer.masksToBounds = false
        self.layer.shouldRasterize = true
        self.layer.rasterizationScale = UIScreen.main.scale

    }

}

Edit

if I use

    self.collectionView.performBatchUpdates({
            self.collectionView.insertItems(at: indexPaths)
        }, completion: { _ in
            self.collectionView.reloadItems(at: indexPaths)
        })

with invalidateLayout then I get an empty space at the bottom of the screen screen but if I delete invalidateLayout I get wrong Layout Layout

if you don’t have it like mine, then discard the project with your improvements

SECOND Edit

I noticed that there is an indentation on the iPhone 11 Pro and everything is fine on the iPhone 11 Pro Max

iPhone 11 Pro IOS 13.3

iPhone 11 Pro Max IOS 13.3


Solution

  • UPDATE: I verified this works on the simulators for iPhone SE, 11, and 11 Max with no dead space at the end of the collection view.

    My simulator is set to dark mode to show that the bottom of the collection view is just enough for the loading icon.

    enter image description here

    I had to make a few changes for this to properly size. First, I changed your insertGoods function in TVCellGoods class to reload only the newly added items after inserting and it no longer has missing items at the top of the list:

        self.collectionView.performBatchUpdates({
            self.collectionView.insertItems(at: indexPaths)
        }, completion: { _ in
            self.collectionView.reloadItems(at: indexPaths)
        })
    

    A big problem was setting the estimated row height for your tableView cells. I've removed the estimatedHeightForRowAt override and replaced your heightForRowAt with this:

       func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    
            if indexPath.row == 0 {
                return (tableView.frame.width / 2.5) + 20
            } else if indexPath.row == 1 {
                return 70
            } else {
                guard let cell = tableView.cellForRow(at: indexPath) as? TVCellGoods else { return UITableView.automaticDimension }
    //            print(cell.collectionViewHeight.constant)
                return cell.collectionViewHeight.constant
            }
        }
    

    I also had to add layoutIfNeeded to your cellForRow at in your ViewController UITableViewDataSource extension:

    self.tableView.beginUpdates()
    cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height
    self.tableView.endUpdates()
    cell.collectionView.layoutIfNeeded()
    self.tableView.layoutIfNeeded()
    

    Lastly, I had to update your layoutSubviews to assign the correct height after invalidating the layout in TVCellGoods:

    override func layoutSubviews() {
        super.layoutSubviews()
    
        self.collectionView.collectionViewLayout.invalidateLayout()
    
        self.collectionViewHeight.constant = self.collectionView.collectionViewLayout.collectionViewContentSize.height
    }
    

    Here is the updated source code: https://drive.google.com/open?id=1ldr4Ml2prZKmr1C4i1s5Sg7LvvxL7-jO