Adrian Le Roy Devezin
Adrian Le Roy Devezin

Reputation: 843

How do I make a fully expanded TableView inside of UICollectionViewCell?

I am trying to programatically create a tableview inside an UICollectionViewCell. I have seen other questions but they seem to be using AutoLayout. I was able to get data to show however my cell's do not display properly. The cells seem to overlap in one way or another no matter what I do. I need the tableview to be fully expanded inside the UICollectionViewCell.

import Foundation
import UIKit
import MaterialComponents

class FollowingItemCollectionViewCell: MDCCardCollectionCell, UITableViewDataSource, UITableViewDelegate {
    
    static let Identifer = "FollowingItemCollectionViewCell"
    private var title: UILabel!
    private var viewMoreButton: MDCButton!
    private var tableView: SelfSizingTableView!
    private var items: [Any] = []
    private var item: FollowingItem!
    private var clickListener: (FollowingItemClick) -> Void = { _ in }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        createView()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        createView()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        backgroundColor = Colors.Card.background
    }
    
    private func createView() {
        backgroundColor = Colors.Card.background
        createTitle()
        createTableView()
        createViewMoreButton()
    }
    
    private func createTitle() {
        title = UILabel()
        title.font = UIFont(name: "HelveticaNeue-Bold", size: 24)
        title.translatesAutoresizingMaskIntoConstraints = false
        title.textColor = Colors.Card.text
        
        contentView.addSubview(title)
        
        title.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 2).isActive = true
        title.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true
        title.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2).isActive = true
    }

    private func createTableView() {
        tableView = SelfSizingTableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(ArticleUiTableViewCell.self, forCellReuseIdentifier: ArticleUiTableViewCell.Identifier)
        tableView.register(SearchTableViewCell.self, forCellReuseIdentifier: SearchTableViewCell.Identifier)
        tableView.backgroundColor = Colors.Card.background

        let titleGuide = UILayoutGuide()
        contentView.addSubview(tableView)
        contentView.addLayoutGuide(titleGuide)

        titleGuide.bottomAnchor.constraint(equalTo: title.bottomAnchor).isActive = true

        tableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        tableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        tableView.topAnchor.constraint(equalTo: titleGuide.bottomAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    }
    
    private func createViewMoreButton() {
        viewMoreButton = MDCButton()
        viewMoreButton.translatesAutoresizingMaskIntoConstraints = false
        viewMoreButton.setBackgroundColor(Colors.clear)
        viewMoreButton.setTitleColor(Colors.royal, for: .normal)
        viewMoreButton.addTarget(self, action: #selector(onViewMoreClick), for: .touchUpInside)

        let tableViewGuide = UILayoutGuide()
        contentView.addSubview(viewMoreButton)
        contentView.addLayoutGuide(tableViewGuide)

        tableViewGuide.bottomAnchor.constraint(equalTo: tableView.bottomAnchor).isActive = true

        viewMoreButton.topAnchor.constraint(equalTo: tableViewGuide.bottomAnchor).isActive = true
        viewMoreButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        viewMoreButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
    }
    
    @objc func onViewMoreClick() {
        if item is FollowingSearchItem {
            clickListener(FollowingItemClick.SearchViewMoreClick)
        } else if item is FollowingArticleItem {
            clickListener(FollowingItemClick.ArticleViewMoreClick)
        }
    }

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

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = items[indexPath.row]
        if let item = item as? Article {
            let cell = ArticleUiTableViewCell(frame: CGRect(x: 0, y: 0, width: contentView.frame.width, height: 80))
            cell.setArticle(article: item, menuClickListener: { article in
                self.clickListener(FollowingItemClick.ArticleMenuClick(article))
            }, contentClickListener: { article in
                self.clickListener(FollowingItemClick.ArticleClick(article))
            })
            cell.backgroundColor = Colors.Card.background
            return cell
        } else if let item = item as? SearchPresentable {
            let cell = SearchTableViewCell(frame: CGRect(x: 0, y: 0, width: contentView.frame.width, height: 80))
            cell.setData(searchPresentable: item, menuClickListener: { searchPresentable in
                self.clickListener(FollowingItemClick.SearchMenuClick(searchPresentable))
            }, contentClickListener: { searchPresentable in
                self.clickListener(FollowingItemClick.SearchClick(searchPresentable))
            })
            cell.backgroundColor = Colors.Card.background
            return cell
        } else {
            fatalError("No proper item type found for \(item)")
        }
    }
    
    func setData(item: FollowingItem, width: CGFloat, clickListener: @escaping (FollowingItemClick) -> Void) {
        widthAnchor.constraint(equalToConstant: width).isActive = true
        contentView.widthAnchor.constraint(equalToConstant: width).isActive = true
        self.clickListener = clickListener
        self.item = item
        if let item = item as? FollowingArticleItem {
            items = item.items
        } else if let item = item as? FollowingSearchItem {
            items = item.items
        }
        tableView.reloadData()
        title.text = item.title
        viewMoreButton.setTitle(item.viewMoreText, for: .normal)
    }
}

Self Sizing TableView

class SelfSizingTableView: UITableView {

    override func reloadData() {
        super.reloadData()
        invalidateIntrinsicContentSize()
        layoutIfNeeded()
    }

    override var contentSize: CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }
    override var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        let height = contentSize.height
        return CGSize(width: contentSize.width, height: height)
    }
}

Screenshot when first opening the tab. Notice there are still more articles in the list ( A total of 3 articles). The cells seem to only first the screen view. enter image description here

Screenshot when starting to scroll within the tab. Notice the cells being to overlap. enter image description here

Screenshot when navigating to another tab, and back to this one. Notice everything just goes crazy. enter image description here

Upvotes: 0

Views: 50

Answers (2)

rptwsthi
rptwsthi

Reputation: 10172

So I noticed. This is some complicated architecture you have implemented here. I would suggest you to design this page using Table view only (if scroll in only vertical we can achieve that).

But lets solve this problem first as it's a complicated architecture of a tableView being inside the cell and cell doesn't know how to adjust size (as a scroll is involved) in this case. Hence you will have to manually calculate the size of each collection view component from inside func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { . That you can achieve by writing some function which will calculate number of rows that will be in cell and multiply that with size.

But Again. I will suggests to make this UI using just tableView with multiple section and cell type.

Upvotes: 1

irs8925
irs8925

Reputation: 186

Try implementing the UITableViewDelegate method to return the height of the row.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  let item = items[indexPath.row]
  if let item = item as? Something {
    return 80 //Or whatever value it should be
  } else {
    return 0 //Or whatever value it should be
  }
}

Upvotes: 0

Related Questions