I'm working on a UI that has a UICollectionView which is horizontally scrollable with a UITableView below it:
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:
and like this after scrolling a bit:
and like this when we've scrolled all the way down:
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
}
}