What am I trying to achieve:
I have a collectionView which represents events in a timeline. These cells (events) have description label that is constrained to the left edge.
When scrolling to events to the left on the timeline, some events are too wide and are not showing the description label immediately (i must scroll to the very beginning of the cell on timeline to see it)
The idea was to setup constraints between cells label to collectionView frame so that you could see event description immediately, but these are different view hierarchies.
What would be the best way to approach this?
What i have tried:
Setting up the constraint crashes the app:
Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors <NSLayoutXAxisAnchor:0x600002cbe700 "UILabel:0x11ef0ea20.leading"> and <NSLayoutXAxisAnchor:0x600002cbf240 "UILayoutGuide:0x6000000289a0'UIScrollView-frameLayoutGuide'.leading"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.'
Additional Info:
UICollectionViewCompositionalLayout:
UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
let item = data.items[sectionIndex]
let contentWidth = data.contentWidth
return context.coordinator.layout(item, contentWidth)
}
NSCollectionLayoutSection:
var layout: (Item, CGFloat) -> NSCollectionLayoutSection = { item, contentWidth in
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(contentWidth), heightDimension: .absolute(100))
let itemSizes = item.items.map { NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(CGFloat($0.duration)), heightDimension: .absolute(100)))}
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: itemSizes)
group.interItemSpacing = .fixed(0)
group.contentInsets = .init(top: 0, leading: item.leftGap, bottom: 0, trailing: item.rightGap)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 0
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: .init(widthDimension: .absolute(255), heightDimension: .absolute(100)),
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .leading
)}
}
header.pinToVisibleBounds = true
section.boundarySupplementaryItems = [header]
return section
}
CollectionViewCell:
class MyCollectionViewCell: UICollectionViewCell {
static let identifier: String = "MyCollectionViewCell"
var item: DataState.Item = .dummy {
didSet {
descriptionLabel.text = item.title
descriptionTimeLabel.text = item.timeDescription
}
}
var containerView: UIView = {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.layer.cornerRadius = 15
containerView.clipsToBounds = true
return containerView
}()
var bg: UIView = {
let bg = UIView()
bg.translatesAutoresizingMaskIntoConstraints = false
bg.backgroundColor = .black
return bg
}()
var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 31, weight: .regular)
return label
}()
var descriptionTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 23, weight: .regular)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(containerView)
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5).isActive = true
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5).isActive = true
containerView.addSubview(bg)
bg.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
bg.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
bg.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
bg.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
containerView.addSubview(descriptionLabel)
descriptionLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
containerView.addSubview(descriptionTimeLabel)
descriptionTimeLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
descriptionTimeLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
}
}
Just to remind whole point is to make labels of cell visible immediately as the wide cell being revealed and be continously updated so when the cell is fully visible label's leadingAnchor and containerView leadingAnchor equals 10
So turns out that if you add constraints to cell as properties and then in: scrollViewDidScroll(UIScrollView) find visibleCells and filter those which are visible just halfway or so. you can then change constants on constraints and update the view.
works great. looks weird.
example:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let collectionView = scrollView as? UICollectionView else { return } // move elsewhere just to access reference to collectionView and dont cast it everytime offset changes (which could be many times a second)
let scrollOffset = scrollView.contentOffset.x
if let visible = collectionView?.visibleCells as? [MyCell] {
let visibleDisappearing = visible.filter({ $0.frame.minX <= scrollOffset })
let visibleFull = visible.filter({ $0.frame.minX > scrollOffset })
for disappearingCell in visibleDisappearing {
let distance = scrollView.contentOffset.x - disappearingCell.frame.minX + 10 // 10 would be the original distance from edge to label
disappearingCell.descriptionLeadingConstraint.constant = distance
disappearingCell.descriptionTimeLeadingConstraint.constant = distance
disappearingCell.layoutIfNeeded()
}
for fullCell in visibleFull {
fullCell.descriptionLeadingConstraint.constant = 10
fullCell.descriptionTimeLeadingConstraint.constant = 10
fullCell.layoutIfNeeded()
}
}
}