CollectionView: HeaderView Bounce - swift

pretty new to CollectionView so hope to have a direction here.
I have a CollectionView with an header enabled. All works fine but I want the HeaderView stay still while scrolling the main section.
Question:
Is that possible in iOS given a collectionview? Or I have to create a dedicated still view for the header?
I set the below in the UICollectionViewFlowLayout
if let layout: UICollectionViewFlowLayout = self.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .vertical
layout.sectionHeadersPinToVisibleBounds = true
}
which makes the header behave as below:
When scrolling down the main collection, the header goes up until it's lower margin reaches the top of the screen, then bunches in the middle of the sceen
When going up the header follows the main collection and become hided
I am pretty sure I am not the only one which has had this issue. Maybe I am doing something wrong here?

layout.sectionHeadersPinToVisibleBounds = true
that makes your header:
stick to the cells, when you scroll down
begin to hide after bottom of cells reaches bottom of the header
I see that you want the header to be still at all times. Of course, as you said, you could add a UIView on top of the collectionView, which would do the job. A different solution may be this:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath)
headerContainer.backgroundColor = .yellow
headerContainer.frame = header.frame
header.layer.masksToBounds = false // that's important
header.addSubview(self.headerContainer)
return header
}
So basically, we're adding a subview inside our header (header needs to be transparent then). Now, we're using the fact that UICollectionView is scrollable:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
if offset < 0 {
headerContainer.transform = CGAffineTransform(translationX: 0, y: offset)
}
}
So if we are scrolling down by offset value, we transform the container by adding offset value to its Y position.
Unfortunately, I haven't come up with the case of scrolling up (it still begins to hide after some time) - I guess it's doable.
I based on this topic:
TableView header bouncing
I think I'd rather make a UIView outside of UICollectionView anyway.

Related

how to adjust layout of tableview cells having collectionView on screen rotation on all screen sizes?

I have CollectionView embedded in TableView cells to show images posted by a user. Im targeting this app on every device from iPhone 8 to Mac and I do have screen rotation on for iPhones and iPads. This is my first time developing app for all platforms & dealing with screen rotation & after searching online I came up with many solutions for handling layout on screen rotation but none of them worked for TableView cells that contain CollectionView and are repeated.
Here I would like to know the best solutions among these for different situations and how to use them to handle CollectionView inside TableView cells.
The first solution I implemented is to handle everything by overriding viewWillLayoutSubviews() in my main View Controller and wrote something like this:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let index = IndexPath(row: 0, section: 0)
guard let cell = tblHome.cellForRow(at: index) as? PriorityTaskTableViewCell else {
return
}
guard let flowLayout = cell.cvPriorityTask.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
flowLayout.invalidateLayout()
cell.layoutIfNeeded()
cell.heightCvPriorityTask.constant = cell.height + 40
}
I had to add cell.layoutIfNeeded() & cell.heightCvPriorityTask.constant = cell.height + 40 to update cell height after rotation. The above approach worked fine for me on every device but only for one TableView cell as you can see I can access CollectionView present in 1st row of TableView only. This approach did not help me deal with situation where I have multiple rows with CollectionView like in case of a social media feed.
Then I decided to deal with rotation in TableView Cell Subclass and came up with following code inside PriorityTaskTableViewCell:
weak var weakParent: HomeViewController?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
cvPriorityTask.contentInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
cvPriorityTask.delegate = self
cvPriorityTask.dataSource = self
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard
let previousTraitCollection = previousTraitCollection,
self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass ||
self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass
else {
return
}
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
weakParent?.tblHome.updateConstraints()
DispatchQueue.main.async {
self.cvPriorityTask?.reloadData()
self.weakParent?.tblHome.updateConstraints()
}
}
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
viewWillTransition(to: size, with: coordinator)
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
weakParent?.tblHome.updateConstraints()
coordinator.animate(alongsideTransition: { context in
}, completion: { context in
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
self.weakParent?.tblHome.updateConstraints()
})
}
And this is how I'm setting up CollectionView Layout:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var width = 0.0
if traitCollection.horizontalSizeClass == .compact {
width = (weakParent?.view.frame.size.width ?? 200) * 0.88
}
else {
width = (weakParent?.view.frame.size.width ?? 200) * 0.425
}
if traitCollection.userInterfaceIdiom == .mac {
height = width * 0.75
}
else {
height = width * 1.13
}
return CGSize(width: width, height: height)
}
This is again another modified code and it seems to work perfectly for small devices like iPhone 8. But does not have any impact on larger displays on rotation. Maybe it works only for traitCollection.horizontalSizeClass == .compact If I could get this working I would have solved my issue for dealing my repeating CollectionView inside multiple rows but I have no idea why it doesn't work on iPhone 13 Pro Max or iPads.
After trying for hours I removed code and just called cvPriorityTask.collectionViewLayout.invalidateLayout() in layoutSubviews() function and it worked too for all cells so I removed all above code from Cell subclass and left with this only:
override func layoutSubviews() {
super.layoutSubviews()
cvPriorityTask.collectionViewLayout.invalidateLayout()
}
This alone made sure collectionView embedded in TableView cells were changing layout as expected but they started cutting TableView cell as main TableView wasn't being updated so I modified my code to somehow refresh TableView too and came up with something like this:
override func layoutSubviews() {
super.layoutSubviews()
cvPriorityTask.collectionViewLayout.invalidateLayout()
layoutIfNeeded()
heightCvPriorityTask.constant = height + 40
weakParent?.tblHome.rowHeight = UITableView.automaticDimension
weakParent?.tblHome.updateConstraints()
}
It did not work at all. If somehow I could update layout of the main table too from the cell subclass.
So im confused which should be right approach. The first one seemed to be most common one I find on internet but I don't know how to handle multiple cells containing CollectionViews with that approach.
Second one works only for iPhone 8 and I have no idea why. And I don't think reloading collectionView is the right approach but it works for small screens so I wouldn't mind if somehow works for large screens.
The third one seems totally wrong to me but actually works on perfectly for every device. However only cell layout is adjusted and I tried to update layout of whole tableView from Cell Subclass but no luck.
So how to deal with layout issues for CollectionView which is embedded in TableView cells? Am I on right track or there is another better way to deal with this situtaion?

How to update UITableview Header height while scrolling programatically

In My application I have top navigation bar and a tableview below the navigation bar. I have CollectionViewCell with two rows which added inside the UITableViewHeader programmatically. When ever I scroll the the TableView to top, i want the header to stop just below the navigation bar, and update the TableView Header height so I can show only one row. I just want to do an animation (like Shrinked)when the TableViewHeader sticks to the navigationbar the two collectionview rows should turn into one row by decreasing the Header Height. How can I do it programmatically
Below is my code for showing CustomHeaderView
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView.init(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 183))
let headerCell = tableView.dequeueReusableCell(withIdentifier: kLastPlayedidentifier) as! LastPlayedTVC
headerCell.frame = headerView.frame
headerCell.category = lastPlayedData
headerView.addSubview(headerCell)
return headerView
}
Also i'm checking for the scroll position to set the tableview header height progmmatically which isn't successful for me.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
if scrollView.contentOffset.y > 237 //This value is to check when the header reached the top position {
//Condition to check and animate the headerview height to make collectionview cell two rows into one rows.
}
How can I achieve the TableViewHeader height update when header sticks on top while scrolling.
Any help is appreciated.
What you are looking for is "sticky header"
and you want to change the header as well.
Sticky part is built in automatically I think if you just use UITableViewController(style: .plain), if that doesn't work for you, you can just google sticky header and there are lots of answers.
the part about changing the height or animating it. you are doing it right, just do something like:
// update your viewForHeader method to account for headerRows variable above
// update your viewForHeader method to account for headerRows variable above
// default 2, you modify this in your scroll
var headerRows = 2
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let height = headerRows == 2 ? 183 : 91
let headerView = UIView.init(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: height))
let headerCell = tableView.dequeueReusableCell(withIdentifier: kLastPlayedidentifier) as! LastPlayedTVC
headerCell.frame = headerView.frame
headerCell.category = lastPlayedData
headerView.addSubview(headerCell)
return headerView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
if scrollView.contentOffset.y > 237 {
updatedHeader.frame.size.height = 40
self.tableviewObj.tableHeaderView = updatedHeader
headerRows = 1 } else {
headerRows = 2
}
self.tableView.reloadSectionHeaders()
}
If you want to do some animating instead, what you would do is store reference to your headerView in a variable of your view controller and inside your scrollViewDidScroll animate it using UIView.animate{...}
hope this helps man.

Header view of UICollectionView keeps duplicating on scroll - How to create fixed (non-sticky), single collection view header?

I wanted to implement a view hierarchy like the following so that the ENTIRE view would be scrollable:
UIScrollView
Image View
Collection view
But a lot of people on here have said that it is better to use the header that comes along with the collection view. I've done that but now I have a new problem: as I scroll the collection view, any configurations I've done to the header cell in the viewForSupplementaryElementOfKind function is duplicating (Eg: If I programmatically create a new view in viewForSupplementaryElementOfKind function, this view will keep creating as I scroll)
I kind of get that this is happening because I'm dequeuing the header using dequeueReusableSupplementaryView. But I've tried searching on Apple docs and there are no other codes I can use to instantiate the header view without making it reusable.
Is there any way I can create a view controller as described above without using UICollectionView?
I've tried setting the number of sections to 1 hoping that it would only be reused ONCE but it doesn't work.
Edit: Also tried setting header size using UICollectionViewDelegateFlowLayouta and using UICollectionView instead of UIViewController and UICollectionViewDataSource etc.
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
switch kind {
case UICollectionView.elementKindSectionHeader:
guard
let headerView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: "DiscoverHeader",
for: indexPath) as? DiscoverHeader
else {
fatalError("Invalid view type")
}
// Rotating arrow image
headerView.arrowImg.transform = headerView.arrowImg.transform.rotated(by: CGFloat.pi/0.67)
return headerView
default:
assert(false, "Invalid element type")
}
}
Why not UIImageView and UICollectionView inside UIScrollView?
You can definitely create a UIScrollView and add an UIImageView and a UICollectionView in it.
But that won't work as expected. This is because you're embedding a scrollView(UICollectionView) inside another scrollView. Scrolling the collectionView vertically will hamper the scrolling of the outer scrollView.
Solution:
Try giving the height of the header view using UICollectionViewDelegateFlowLayout's method,
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.bounds.width, height: 100.0) //give the height as per your requirement...
}
Also, set the contentMode of imageView as Aspect Fit / Aspect Fill.

Make the last row's bottom be the tableview's bottom (if enough rows)

Is there any way to avoid the tableview keep scrolling when reaching the bottom cell?
So far I managed to do this:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == dataArray.count - 1 {
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
and that allows me only to stop the scroll when the user "rolled" the table to bottom. But after that the user can scroll again. Only if I could disable the "down" scrolling I could achieve it. Enabling it again as soon as it moved.
But it seems a lot of work just to do that, maybe I'm missing some property that fixes the last cell bottom to tableview's bottom.
Any ideas?
There are 3 things you need to do.
tableView.footerView = UIView()
tableView.bounces = false
And after the tableView has been reloaded.
tableViewHeight(your constraint).constant = tableView.contentSize.height
This will cause the tableView to only display the populated cells, you will restrict the height to be as high as the content and you can't bounce the content as you want it to stop at last cell.
Remember that an UITableView is a subclass of UIScrollView
Starting here, you have 2 options :
1 - Disable the bounces effect : tableView.bounces = false
2 - Prevent scroll further than the bottom of your tableView :
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height {
scrollView.contentOffset.y = scrollView.contentSize.height - scrollView.bounds.size.height
}
}
You can do by disabling it directly from Storyboard:
Disable the Bounce on scroll to Untick
Also if u want to disable the lines coming at bottom when data is less you can use:
tableView.footerView = UIView()

UICollectionView sticky header not working iOS 10

I am having an issue with a sticky header in my collection view not working in iOS 10. I have a collectionView that has a header in the second section only - implemented by setting the size in the first section as CGSize.zero and the size in the second section as the appropriate size:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
if section == 0 {
return CGSize.zero
}
return CGSize(width: collectionView.width, height: 80.0)
}
I make the sticky header sticky with these lines in viewDidLoad:
theCollectionView.delegate = self
theCollectionView.dataSource = self
if let flowLayout = theCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.sectionHeadersPinToVisibleBounds = true
}
Everything works great in iOS 11. When you scroll the collection view up, the header sticks to the top.
However, in iOS 10, this does not work. The header sticks - but not to the top of the screen - it sticks to its initial location and the collection view cells in the second section can be seen scrolling underneath it. There is a gap at the top of the screen. I've attached two screenshots illustrating my problem. Any help would be much appreciated!
Fixed by changing the top auto layout constraint in my storyboard. It was set from the Collection View top to the Safe Area top. Changed from the Collection View top to the Superview top.