I have an UICollectionView built programatically that doesn't allow me to scroll to the last item in it.
Here is how I set it up
private let collectionView: UICollectionView = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewCompositionalLayout { sectionIndex, _ -> NSCollectionLayoutSection? in
return WorkoutListViewController.createSectionLayout(section: sectionIndex)
})
private static func createSectionLayout(section: Int) -> NSCollectionLayoutSection {
// item
let item = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
)
item.contentInsets = NSDirectionalEdgeInsets(top: 14, leading: 0, bottom: 14, trailing: 0)
// group
let verticalGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(123)
),
repeatingSubitem: item,
count: 1
)
// section
let section = NSCollectionLayoutSection(group: verticalGroup)
return section
}
private func configureCollectionView() {
view.addSubview(collectionView)
collectionView.register(
WorkoutListCollectionViewCell.self,
forCellWithReuseIdentifier: WorkoutListCollectionViewCell.identifier
)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .systemBackground
}
I tried modifying the height by giving it a very large number (e.g 2000) and nothing changed. I also tried adding the UICollectionViewDelegateFlowLayout protocol to add the
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
}
function and return the CGSize of my item.
It seems your collection view is hiding behind the TabBar, So try to set the ContentInset of your collection view based on the bottom TabBar height.
Example:
collectionView.contentInset = .init(top: 0.0, left: 0.0, bottom: bottomHeight, right: 0.0)
Hope this helps!
The main problem is you didn't set the constraint for the bottom of the collection view.
Try to get border for the collection view and see where collection view is ending.
constraint for the collection view bottom should be top of the bottom bar...
though above answer of Natarajan is working but this is not the main cause of the issue. That is just a solution to avoid real problem.
Related
I have implemented a collection view to achieve a full screen image gallery where you can move between each image horizontally. I had a Flow Layout implemented, because it was a really simple layout, but I had problems invalidating its context to achieve the correct layout on device rotation.
That's where I switched to compositional layout, also with a really basic implementation:
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = .zero
layoutItem.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .none , top: .none, trailing: .none, bottom: .none)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [layoutItem])
layoutGroup.interItemSpacing = .fixed(0)
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.interGroupSpacing = 0
layoutSection.orthogonalScrollingBehavior = .groupPaging
let collectionViewLayout = UICollectionViewCompositionalLayout(section: layoutSection)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .clear
collectionView.bounces = false
collectionView.isPagingEnabled = true
collectionView.showsHorizontalScrollIndicator = false
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "...")
And the problem is that whenever the device is rotated to landscape, the collection view seems to be scrolling to a different cell. On FlowLayout I had implemented the targetContentForOffset, and with that + overriding the invalidation context I could manage to set the correct offset for when the device is changing orientation.
But in this case, that method is never called and I can't find any other way to avoid the incorrect cell being displayed.
If I see the orientation change with Slow animation, the cell is changing even before the rotation started. I've tried scrolling to the "correct" cell with viewWillTransition (just to see if that works) and it doesn't, it keeps jumping to a different cell. So I don't even know if this is related to the targetContentOffset.
Here is a gif with the slow animation: https://gifyu.com/image/S3M3d. Note that on the video the cell is changing from 2nd to 1st, but if I'm on the 3rd cell, it changes to the 2nd one. So it seems it's always to the previous one.
Thanks in advance!
I'm trying to accomplish a dynamic cell with the new collection view compositional layout
basically this is what I have:
UICollectionView (Compositional Layout)
UICollectionViewCell
innerStackView (Vertical UIStack ) // Contains postDescription and 'from Riyadh' Label
....
UICollectionView (Compositional Layout)
UICollectionView (Flow Layout -Horizontal)
UICollectionView(Compositional Layout)
I have tried two approaches:
1- Adding the inner collection views inside a vertical stack,
2- Adding the inner collection views inside the container view with constraints
This is what I have currently:
The top UICollectionView (Compositional Layout)
func setupCollectionViewLayout() -> UICollectionViewLayout {
let size = NSCollectionLayoutSize(
widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
heightDimension: NSCollectionLayoutDimension.estimated(300)
)
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .zero
section.interGroupSpacing = 0
let headerFooterSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3)
)
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top
)
section.boundarySupplementaryItems = [sectionHeader]
return UICollectionViewCompositionalLayout(section: section)
}
The Cell (Using Container View Constraints)
class FeedsCell: UICollectionViewCell {
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var innerStackView: UIStackView!
#IBOutlet weak var postDescription: UILabel!
var imageGalleryCollectionView: UICollectionView!
var reactionCollectionView: UICollectionView!
var commentCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
}
func with(viewModel: FeedsCellViewModel) -> Self {
postDescription.text = ".random(in: 0...255)"
if !viewModel.imageProvider.dataSource.isEmpty {
let imageLayout = setupImageLayout()
imageGalleryCollectionView = UICollectionView(frame: .zero, collectionViewLayout: imageLayout)
imageGalleryCollectionView.delegate = viewModel.imageProvider
imageGalleryCollectionView.dataSource = viewModel.imageProvider
containerView.addSubview(imageGalleryCollectionView)
}
if !viewModel.reactionProvider.dataSource.isEmpty {
let reactionLayout = UICollectionViewFlowLayout()
reactionLayout.scrollDirection = .horizontal
reactionCollectionView = UICollectionView(frame: .zero, collectionViewLayout: reactionLayout)
reactionCollectionView.showsHorizontalScrollIndicator = false
reactionCollectionView.dataSource = viewModel.reactionProvider
reactionCollectionView.delegate = viewModel.reactionProvider
innerStackView.addArrangedSubview(reactionCollectionView)
reactionCollectionView.anchor(heightConstant: 32)
}
if !viewModel.commentProvider.dataSource.isEmpty {
let commentLayout = setupCommentLayout()
commentCollectionView = UICollectionView(frame: .zero, collectionViewLayout: commentLayout)
commentCollectionView.dataSource = viewModel.commentProvider
commentCollectionView.delegate = viewModel.commentProvider
containerView.addSubview(commentCollectionView)
imageGalleryCollectionView.anchor(top: innerStackView.bottomAnchor, left: innerStackView.leftAnchor, bottom: commentCollectionView.topAnchor, right: innerStackView.rightAnchor)
commentCollectionView.anchor(top: imageGalleryCollectionView.bottomAnchor, left: innerStackView.leftAnchor, bottom: containerView.bottomAnchor, right: innerStackView.rightAnchor)
}
return self
}
override func prepareForReuse() {
super.prepareForReuse()
imageGalleryCollectionView?.removeFromSuperview()
reactionCollectionView?.removeFromSuperview()
commentCollectionView?.removeFromSuperview()
}
func setupCommentLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout.list(using: .init(appearance: .insetGrouped))
return layout
}
func setupImageLayout() -> UICollectionViewCompositionalLayout {
let mainItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(2/3),
heightDimension: .fractionalHeight(1.0)))
mainItem.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 0,
bottom: 0,
trailing: 2)
let pairItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)))
pairItem.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 2,
bottom: 0,
trailing: 2)
let trailingGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1/3),
heightDimension: .fractionalHeight(1.0)),
subitem: pairItem,
count: 2)
let mainWithPairGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(4/9)),
subitems: [mainItem, trailingGroup])
let nestedGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(1.0)),
subitems: [
mainWithPairGroup,
]
)
let section = NSCollectionLayoutSection(group: nestedGroup)
return UICollectionViewCompositionalLayout(section: section)
}
}
The Result
The Cell (using UIStackView)
func with(viewModel: FeedsCellViewModel) -> Self {
postDescription.text = ".random(in: 0...255)"
if !viewModel.imageProvider.dataSource.isEmpty {
let imageLayout = setupImageLayout()
imageGalleryCollectionView = UICollectionView(frame: .zero, collectionViewLayout: imageLayout)
imageGalleryCollectionView.delegate = viewModel.imageProvider
imageGalleryCollectionView.dataSource = viewModel.imageProvider
innerStackView.addArrangedSubview(imageGalleryCollectionView)
}
if !viewModel.reactionProvider.dataSource.isEmpty {
let reactionLayout = UICollectionViewFlowLayout()
reactionLayout.scrollDirection = .horizontal
reactionCollectionView = UICollectionView(frame: .zero, collectionViewLayout: reactionLayout)
reactionCollectionView.showsHorizontalScrollIndicator = false
reactionCollectionView.dataSource = viewModel.reactionProvider
reactionCollectionView.delegate = viewModel.reactionProvider
innerStackView.addArrangedSubview(reactionCollectionView)
reactionCollectionView.anchor(heightConstant: 32)
}
if !viewModel.commentProvider.dataSource.isEmpty {
let commentLayout = setupCommentLayout()
commentCollectionView = UICollectionView(frame: .zero, collectionViewLayout: commentLayout)
commentCollectionView.dataSource = viewModel.commentProvider
commentCollectionView.delegate = viewModel.commentProvider
innerStackView.addArrangedSubview(commentCollectionView)
}
return self
}
The Result
It's really been a struggle, it's my 5th day trying to figure this out.
I currently have a couple of collection views that use compositional layouts that I'm trying to add to two separate vertical stack views on top of each other.
One of my collection view will be going into the headerContentView and the other will be going into the actual contentView.
My collectionView should be self-sizing by invalidating the intrinsic content size. However, for some reason, only one of the collectionView is visible because one will size itself to the content size. However, the other will have a height of 0.
As a note, the stack view that contains the second collectionView also has a height of zero.
I need help to have the collection views size properly.
Thanks for your time.
StackViews:
internal let contentStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
}()
Constraints on StackView:
private func setUpView() {
backgroundColor = .blue
layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 24, right: 16)
addSubview(headerView)
addSubview(contentStackView)
// Preserve superview layout margins so that sub-elements can be laid out using layoutMarginsGuide
contentStackView.preservesSuperviewLayoutMargins = true
[headerView, contentStackView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: topAnchor),
contentStackView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 16),
layoutMarginsGuide.bottomAnchor.constraint(lessThanOrEqualTo: contentStackView.bottomAnchor),
headerView.leadingAnchor.constraint(equalTo: leadingAnchor),
headerView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}
private func setUpHeaderView() {
// Preserve superview layout margins so that sub-elements can be laid out using layoutMarginsGuide
headerView.preservesSuperviewLayoutMargins = true
headerContentStackView.preservesSuperviewLayoutMargins = true
headerView.addSubview(headerContentStackView)
headerContentStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
headerContentStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
headerView.bottomAnchor.constraint(lessThanOrEqualTo: headerContentStackView.bottomAnchor),
headerContentStackView.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
headerContentStackView.trailingAnchor.constraint(equalTo: headerView.trailingAnchor)
])
// Add header contents to header content stack view
headerContentStackView.addArrangedSubview(headerBarView)
headerContentStackView.addArrangedSubview(notificationView)
}
Compostional layout:
private static func generateCompositionalLayout() -> UICollectionViewLayout {
// Items
let itemSize: NSCollectionLayoutSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(UIConstantsIconTileCollectionView.PhaseTwo.fractionalWidthForItems),
heightDimension: .fractionalHeight(UIConstantsIconTileCollectionView.PhaseTwo.fullWidthOrHeight)
)
let fullItem: NSCollectionLayoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
// Groups
let groupSize: NSCollectionLayoutSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(UIConstantsIconTileCollectionView.PhaseTwo.fullWidthOrHeight),
heightDimension: .fractionalWidth(UIConstantsIconTileCollectionView.PhaseTwo.fractionalWidthForGroups)
)
let group: NSCollectionLayoutGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitem: fullItem,
count: UIConstantsIconTileCollectionView.PhaseTwo.numberOfItemsPerGroup
)
group.interItemSpacing = .fixed(UIConstantsIconTileCollectionView.PhaseTwo.interGroupSpacing)
// Section
let section: NSCollectionLayoutSection = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: UIConstantsIconTileCollectionView.PhaseTwo.interSectionSpacing,
bottom: 0,
trailing: UIConstantsIconTileCollectionView.PhaseTwo.interSectionSpacing
)
return UICollectionViewCompositionalLayout(section: section)
}
Self Sizing Collection View:
// MARK: - View Layout
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return contentSize
}
My solution to this was the way I was implementing the CollectionViewController. I was overriding the loadView() and replacing the view itself with the collectionView. Example, below:
override func loadView() {
view = collectionView
}
However, when I override viewDidLoad and add the collectionView as a subview and constraint it to the view controllers view it works out just fine. Example, below:
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
// Constraint collectionView -> view
}
I couldn't find why this plays nicer with stackViews because it kind of blows my mind but this solved the issue. If anyone has more knowledge as to WHY essentially just having a container view for the collectionView plays nicely with the stackView I would appreciate the knowledge.
Requirement
I am trying to make a CollectionView section that has 1 large item then 4 half-width items below like this:
Each cell will contain labels that support multiline text or variable length with dynamic type support.
Approach
I have written the following compositional layout code which constructs 3 groups. A fullWidthGroup contains a single item that is full width (item 1). A halfWidthGroup will hold 2 items per "row" and a main outer group group which is made up of 1 fullWidthGroup and 2 halfWidthGroups.
private let compositionalLayout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, environment) -> NSCollectionLayoutSection? in
let margin: CGFloat = 8
// Items
let fullWidthItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
let fullWidthItem = NSCollectionLayoutItem(layoutSize: fullWidthItemSize)
fullWidthItem.contentInsets = .init(top: 0, leading: margin, bottom: 0, trailing: margin)
let halfWidthItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(50))
let halfWidthItem = NSCollectionLayoutItem(layoutSize: halfWidthItemSize)
halfWidthItem.contentInsets = .init(top: 0, leading: margin, bottom: 0, trailing: margin)
// Groups
let fullWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: fullWidthItemSize, subitems: [fullWidthItem])
let halfWidthGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: halfWidthItemSize.heightDimension)
let halfWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: halfWidthGroupSize, subitems: [halfWidthItem])
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(150))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [fullWidthGroup, halfWidthGroup, halfWidthGroup])
group.interItemSpacing = .fixed(margin)
// Section
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = margin
return section
})
This gives the desired result for basic cells that do not contain any constraints (to self size them).
Problem
As soon as I create a cell that has constraints that can be used to correctly evaluate the size of the cell width of the cells is unexpectedly changed.
Resulting in the following layout.
Expectation
I would expect the last 4 items to take up half the width but instead, they appear to be taking up less space. Item 1 which should be full width with an 8 point margin on each side seems to also be sized incorrectly with ~16 point margin on the trailing edge.
Full Code
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let compositionalLayout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, environment) -> NSCollectionLayoutSection? in
let margin: CGFloat = 8
// Items
let fullWidthItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
let fullWidthItem = NSCollectionLayoutItem(layoutSize: fullWidthItemSize)
fullWidthItem.contentInsets = .init(top: 0, leading: margin, bottom: 0, trailing: margin)
let halfWidthItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(50))
let halfWidthItem = NSCollectionLayoutItem(layoutSize: halfWidthItemSize)
halfWidthItem.contentInsets = .init(top: 0, leading: margin, bottom: 0, trailing: margin)
// Groups
let fullWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: fullWidthItemSize, subitems: [fullWidthItem])
let halfWidthGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: halfWidthItemSize.heightDimension)
let halfWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: halfWidthGroupSize, subitems: [halfWidthItem])
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(150))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [fullWidthGroup, halfWidthGroup, halfWidthGroup])
group.interItemSpacing = .fixed(margin)
// Section
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = margin
return section
})
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(UINib(nibName: "BasicCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
collectionView.collectionViewLayout = compositionalLayout
collectionView.dataSource = self
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.contentView.backgroundColor = .red
return cell
}
}
The BasicCollectionViewCell is created in a nib and contains a stack view (added to the contentView). The StackView is pinned to all edges with a priority of 1000. Inside the stackview is a label.
I'm having trouble with adding a footer in UICollectionViewCompositionalLayout on top of the cells. Specifically I want to have an interactive control in my footer (in my case a UIPageControl). This is an example layout,
func generateLayout() -> UICollectionViewCompositionalLayout {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(213)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [footer()]
return UICollectionViewCompositionalLayout(section: section)
}
The footer is configured like,
private func footer() -> NSCollectionLayoutBoundarySupplementaryItem {
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(20))
let item = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: size,
elementKind: "footer-kind",
alignment: .bottom,
absoluteOffset: .init(x: 0, y: -30)
)
item.zIndex = 1000
return item
}
Lets say I have a button in my Footer that I want to be tappable. I have changed the zIndex of the footer so it is higher than other views in my section.
class FooterCell: UICollectionReusableView {
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
layer.zPosition = CGFloat(layoutAttributes.zIndex)
}
}
The result of the snippets above is that the button is visible on top of the cells, but not tappable. After some inspections in the view debugger it is clear that the supplementary view is added to the view hierarchy before the cells and changing the zIndex only moves the layer to the front, not the complete view. Is there someway to change the order on how the views are added in the compositional layout? Or is there any other way to bring the footer view above the cells?