Home > Software design >  Synchronize UICollectionView with UITableview
Synchronize UICollectionView with UITableview

Time:12-20

I'm working on a UI that has a UICollectionView which is horizontally scrollable with a UITableView below it:

enter image description here

My goal is that whenever I scroll within the TableView, the CollectionView will move at exactly the same speed and the same amount/direction as the Tableview did and vice versa.

So I somehow need to connect them. I played around with getting the myTableView.contentOffset.y and putting this value in the CollectionView myCollectionView.setContentOffset(CGPoint(x: offset, y: 0), animated: true).

However this function would need to be called all the time in shortest possible intervals and its not really synchron nor smooth and fast.

Is there any other possibility to achieve this?

Thankful for any input!

CodePudding user response:

"this function would need to be called all the time in shortest possible intervals" is not really true. You only need to synch the scrolling when the tableView (or collectionView) is scrolled.

To do that, implement scrollViewDidScroll.

But, you'll have several issues to deal with...

First, unless your rows are the same height as your cells are wide, you can't use .contentOffset.y directly.

For example, if your rows are 50-pts high, and your cells are 100-pts wide, when you've scrolled down 10 rows the .contentOffset.y will be 500. If you set .contentOffset.x on your collectionView to 500, it will only scroll 5 cells instead of 10.

So, you'd need to use the offset percentage -- not too tough to calculate.

However...

Based on the image you posted, you're showing about 13 rows and about 3 cells. As you scroll down, converting to percentage offset, when Row 2 is at the top Cell 2 will be at the left edge. When Row 3 is at the top Cell 3 will be at the left edge. And so on... which is great.

Until you've scrolled all the way down. Now, if you have a total of 100 rows, the top row will be row 87, and the visible cells in your collectionView will be 87, 88 & 89... no way to get to cells 90 through 100.

Here is some example code that shows synched scrolling and shows the problem when you get to the bottom of the table (using 50 data items):

class CvTvViewController: UIViewController {
    
    var myData: [String] = []
    
    let colors: [UIColor] = [
        .systemRed, .systemGreen, .systemBlue,
        .systemPink, .systemYellow, .systemTeal,
    ]
    
    var collectionView: UICollectionView!
    
    var tableView: UITableView!
    
    var tableViewActive: Bool = false
    
    var cvCellWidth: CGFloat = 120
    let tvRowHeight: CGFloat = 50
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // fill myData array with 50 strings
        myData = (1...50).map { "Data: \($0)" }
        
        let cvl = UICollectionViewFlowLayout()
        cvl.itemSize = CGSize(width: cvCellWidth, height: 100)
        cvl.minimumLineSpacing = 0
        cvl.minimumInteritemSpacing = 0
        cvl.scrollDirection = .horizontal
        
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: cvl)
        
        tableView = UITableView()
        tableView.rowHeight = tvRowHeight
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        tableView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(collectionView)
        view.addSubview(tableView)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            collectionView.heightAnchor.constraint(equalToConstant: 100.0),
            
            tableView.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 0.0),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
            
        ])
        
        collectionView.dataSource = self
        collectionView.delegate = self
        
        collectionView.register(MyCVCell.self, forCellWithReuseIdentifier: "cvCell")
        
        tableView.dataSource = self
        tableView.delegate = self
        
        tableView.register(MyTVCell.self, forCellReuseIdentifier: "tvCell")
        
    }
    
}

extension CvTvViewController: UIScrollViewDelegate {

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        tableViewActive = scrollView == tableView
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        if tableViewActive && scrollView == tableView {
            let pctOffset = tableView.contentOffset.y / (CGFloat(myData.count) * tableView.rowHeight)
            let xOffset = (CGFloat(myData.count) * cvCellWidth) * pctOffset
            collectionView.contentOffset.x = xOffset
        }
        if !tableViewActive && scrollView == collectionView {
            let pctOffset = collectionView.contentOffset.x / (CGFloat(myData.count) * cvCellWidth)
            let xOffset = (CGFloat(myData.count) * tvRowHeight) * pctOffset
            tableView.contentOffset.y = xOffset
        }
        
    }

}

extension CvTvViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "tvCell", for: indexPath) as! MyTVCell
        c.label.text = myData[indexPath.row]
        return c
    }
}

extension CvTvViewController: UICollectionViewDataSource, UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return myData.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: "cvCell", for: indexPath) as! MyCVCell
        c.contentView.backgroundColor = colors[indexPath.item % colors.count]
        c.label.text = myData[indexPath.item]
        return c
    }
}

class MyTVCell: UITableViewCell {
    let label: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        return v
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        contentView.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
        ])
    }
}

class MyCVCell: UICollectionViewCell {
    let label: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        contentView.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
        ])
    }
}

It will look like this on launch:

enter image description here

and like this after scrolling a bit:

enter image description here

and like this when we've scrolled all the way down:

enter image description here


Edit if you want to be able to scroll all the way to the last row / item (which can be a little disconcerting, as it's not what users are accustomed to, but...), add this code:

// track the table view frame height and
//  collection view frame width
var tvHeight: CGFloat = 0
var cvWidth: CGFloat = 0

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // we only want to call this if
    //  the tableView height or
    //  the collectionView width changes
    if tvHeight != tableView.frame.height || cvWidth != collectionView.frame.width {
        tvHeight = tableView.frame.height
        cvWidth = collectionView.frame.width
        
        var edge = tableView.contentInset
        edge.bottom = tableView.frame.height - tvRowHeight
        tableView.contentInset = edge

        edge = collectionView.contentInset
        edge.right = collectionView.frame.width - cvCellWidth
        collectionView.contentInset = edge
    }
}

enter image description here

  • Related