I'm trying to create a sticky supplementary header for a specific section, which stays on top all the time when the specific section touches on the navigation and won't overlay with other section headers. The solutions I found so far is working till 900 collectionView.contentOffset.y. I followed with this code StickyLayout.swift.
And I have to set sectionHeadersPinToVisibleBounds = false because I want to stick only one header on top while scrolling up/down depending on the position. Other section headers should not push out the sticky header.
As I'm doing this in Swift, it would be great to have an example in Swift.
This caused the header's layout attribute to not be included in the attributes array when you iterated over in the for loop, resulting in the layout position no longer being adjusted to its "sticky" position at the top of the screen.
Adding these lines right before the for loop to add the sticky header's layout attributes to the attributes array if they are not there:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
guard let cellLayoutAttributesInRect = super.layoutAttributesForElements(in: rect) else { return nil }
// add the sticky header's layout attribute to the attributes array if they are not there
if let stickyHeaderIndexPath = stickyHeaderIndexPath,
let stickyAttribute = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: stickyHeaderIndexPath),
!layoutAttributes.contains(stickyAttribute) {
layoutAttributes.append(stickyAttribute)
}
return layoutAttributes
}
Related
I am trying to implement a collection view, in which items have:
automatic height based on constraints
the full available width of the collection view.
I'm aware that this is pretty easy to accomplish UICollectionViewCompositionalLayout, but I'm looking to solve it for iOS 11+. I've decided to implement a custom UICollectionViewFlowLayout:
class SingleColumnFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView,
let layoutAttributes = super.layoutAttributesForElements(in: rect) else { return [] }
layoutAttributes
.filter { $0.representedElementCategory == .cell }
.forEach { attributes in
let availableWidth = collectionView.bounds
.inset(by: collectionView.contentInset)
.width
attributes.frame.origin.x = sectionInset.left
attributes.frame.size.width = availableWidth
}
return layoutAttributes
}
}
The result isn't quite what I have imagined:
The cell I'm using is pretty simple:
Interestingly if I add a fixed width constraint to the label, it works correctly, so my theory is that
for some reason the collection view fails to infer the width of the label correctly
for that reason, it thinks that it can fit multiple items in the same row
because of this, it calculates incorrect y values for some of the items.
I would like to make this work without fixed-width labels, so my question would be: am I missing anything obvious? Is there a way to fix this?
For anyone interested, I've uploaded the entire project to GitHub.
As it turns out, the issue was caused by the fixed trailing constraint of the label. The label's intrinsic width was smaller (due to short text), and since the entire cell was constrained horizontally, the cell width also became small. I fixed it by changing the trailing constraint from 'equal' to 'greater than'.
I have the header (header : UICollectionReusableView) in my UICollectionView. The header has 3 UIView (UIView1, UIView2, UIView3). When I set collectionLayout.sectionHeadersPinToVisibleBounds = true, the header will in top when I scroll the collection. And I want when the header stick in top of UICollectionView, I will hide UIView1 and UIView2. How can I do that?
UICollectionView is a subclass of UIScrollView. This means that is you assign a delegate to it then this delegate may listen to scrollViewDidScroll(_ scrollView: UIScrollView) method which will be called every time the offset changes in your collection view.
On this event you will need to get all of your header views which I assume you may get by calling visibleSupplementaryViews on your collection view and check its class type.
If the received view is indeed your header you will need to check its frame in comparison to your collection view and see if its position is on top. To do so you may convert frame to your coordinates:
func isHeader(headerView: UIView, onTopOfCollectionView collectionView: UICollectionView) -> Bool {
guard let collectionSuperView = collectionView.superview else {
return false // Collection view is not in view hierarchy. This will most likely never happen
}
let convertedFrame = headerView.convert(headerView.bounds, to: collectionSuperView) // Convert frame of the view to whatever is the superview of collection view
return convertedFrame.origin.y <= collectionView.frame.origin.y // Compare frame origins
}
So I guess the whole thing would be something like:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.visibleSupplementaryViews(<#My identifier here#>).forEach { view in
if let header = view as? MyHeaderView {
header.hideSecondaryViews = isHeader(headerView: header, onTopOfCollectionView: collectionView)
}
}
}
I hope this gets you on the right track.
I am having trouble getting my UITableView to appear full height in my Stack View.
My view tree looks as follows:
- View
- Scroll View
- Stack View
- Table View
- Image View
- Map View
The table view is dynamically populated with data, which works fine. The issue is that only one row is visible at a time and I have to scroll through the list. What I would like to see happen is for the table view to take as much vertical room as it needs to display all the cells.
I did try adjusting table height as follows, but that just ends up with table that no longer scrolls, though even if it did work I would rather have something more dynamic:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.detailsTableView.frame.size.height = 200
}
I am suspecting that it is probably an aspect of the 'stack view' that needs adjusting, but I am not sure at this point. Can anyone suggest an appropriate way?
I had been encountering the same issue and realized you need a self sizing table view. I stumbled on this answer and created a subclass like #MuHAOS suggested. I did not encounter any issues.
final class IntrinsicTableView: UITableView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
A UIStackView will compress views wherever it can, to counteract this set a height anchor and width anchor to the UITableView or a priority for its height and width. Here is a working example of how we can be in charge of the dimensions of a table within a stack view.
An extension to instantiate and centrally position the UIStackView
First of all I've written a UIStackView extension so that I don't need to include all the code inside the view controller. Your positioning and setup will be different because you are placing your stack view inside a scroll view, but separating this code out means you can make your own adjustments.
extension UIStackView {
convenience init(axis:UILayoutConstraintAxis, spacing:CGFloat) {
self.init()
self.axis = axis
self.spacing = spacing
self.translatesAutoresizingMaskIntoConstraints = false
}
func anchorStackView(toView view:UIView, anchorX:NSLayoutXAxisAnchor, equalAnchorX:NSLayoutXAxisAnchor, anchorY:NSLayoutYAxisAnchor, equalAnchorY:NSLayoutYAxisAnchor) {
view.addSubview(self)
anchorX.constraintEqualToAnchor(equalAnchorX).active = true
anchorY.constraintEqualToAnchor(equalAnchorY).active = true
}
}
We don't set a size for the UIStackView only a position, it is the things contained within it that determine its size. Also note the setting of translatesAutoresizingMaskIntoConstraints to false in the UIStackView extension. (It is only required that we set this property for the stack view, its subviews simply inherit the behaviour.)
UITableView class with data source code
Next I've created a basic table class for demo purposes.
class MyTable: UITableView, UITableViewDataSource {
let data = ["January","February","March","April","May","June","July","August","September","October","November","December"]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SauceCell", forIndexPath: indexPath)
cell.textLabel?.text = data[indexPath.row]
return cell
}
}
Setup of stack view and table in view controller
Finally, the important stuff. As soon as we add our table to the stack view all the frame information is disregarded. So we need the final two lines of code to set the width and height for the table in terms that Auto Layout can understand.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let table = MyTable(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
table.registerClass(UITableViewCell.self, forCellReuseIdentifier: "SauceCell")
table.dataSource = table
let stack = UIStackView(axis: .Vertical, spacing: 10)
stack.anchorStackView(toView: view, anchorX: stack.centerXAnchor, equalAnchorX: view.centerXAnchor, anchorY: stack.centerYAnchor, equalAnchorY: view.centerYAnchor)
stack.addArrangedSubview(table)
table.widthAnchor.constraintEqualToAnchor(view.widthAnchor, multiplier: 1).active = true
table.heightAnchor.constraintEqualToAnchor(view.heightAnchor, multiplier: 0.5).active = true
}
}
Note that we use addArrangedSubview: not addSubview: when adding views to the stack view.
(I've written blogposts about UIStackView as well as others about Auto Layout in general that might help too.)
Context: I am using an UICollectionView for a photoview. Every picture is a cell with one UIImage. Images can have different sizes and I want them to fill the whole screen.
So I wrote a class who determines the frame of every single UICollectionCell and let a subclass of UICollectionViewFlowLayout ask that class for the right frame for every item.
My implementation of the UICollectionViewFlowLayoutClass:
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]
for attributes in attributesToReturn ?? [] {
if attributes.representedElementCategory == .Cell {
let frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame
attributes.size = frame.size
attributes.frame = frame
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let frame = mazeManager.sizeForItemAtIndex(indexPath, collectionView: collectionView!)
curAttributes.size = frame.size
curAttributes.frame = frame
return curAttributes
}
So the frame asks my MazeManager to give back a frame. The returned frames seem to be correct and they all fit in the UICollectionView.
When I open my app everything looks fine, even when I scroll. But when I scroll to a specific position (this position feels random because it depends on the images I test with, but with the same set of images the positions are the same) cells disappear from my view. When I scroll back they return.
I've checked if the cells where not hidden, but they never are.
On some other threads with similar issues the answer is to implement the collectionViewContentSize so I did:
override func collectionViewContentSize() -> CGSize {
let size = mazeManager.collectionViewContentSize
return size.height < collectionView!.frame.size.height ? collectionView!.frame.size : size
}
The number of items is not static and it grows while reaching the end of the view. So what happens here is:
The manager determines the Origin.y + the Size.Height of the last item (+ 10 points to be sure), the width is the width of the UICollectionView.
Still all the frames of the cells are within the sizes returned by this method. But still some cell disappear or never appear at all.
When I scroll further through the UICollectionView I see other cells which are positioned on the right place. So there are gaps in my view, but the flow continues. (When a gap appears there are no calls for the items at the missing idexpaths in the collectionViewDelegate. For example the CollectionView asks for items: 1,2,3,4,5,6, 12,13,14).
The console prints nothing about wrong positioning, I've checked and everything is within the ContentSize. So I'm almost out of options. Can anybody explain what's happening in my case?
Thank you.
Edit:
While I was looking for a solution, I already found the mentioned post (UICollectionView's cell disappearing) and I already did 3 of the 4 steps.
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
And I just added the scroll direction in the initializer:
override init(){
super.init()
scrollDirection = .Vertical
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.scrollDirection = .Vertical
}
Unfortunately this doesn't fix my issue, so it doesn't seem a duplicate to me.
I don't know why, but it works when I add the following line in my init methods:
self.itemSize = CGSize(width: 165,height: 165)
165 is the average height for my cells (I need to make this less static). The size I specified here seems to be ignored, because the sizes I see on my screen are all calculated in the layoutAttributesForItemAtIndexPath.
So without this property set the view behaves strange, I still don't know the reason, but I'm glad it works. (If anyone knows the reason, I would like to hear it)
What is the best method to provide a footer for a UICollectionView that "sticks" to the bottom of the screen bounds on a given page? Assume the UICollectionView is full screen and only has one section.
Currently I am providing a UICollectionReusableView object in collectionView:viewForSupplementaryElementOfKind:atIndexPath:, but (as one might expect) when the content of my collection exceeds this screen's bounds, the footer is pushed off-screen.
I'm guessing the key is a Decoration View - but I can't find any good code (non-IB) examples on how these work, and Apple's documentation is in my opinion unclear on this particular subject.
Update re: Decoration Views
After building out and experimenting with a Decoration View (using this tutorial), I hit some limitations - namely that there aren't really any callbacks between the Decoration View object and your UICollectionViewController controller object (the Decoration View is managed by a UICollectionViewLayout object, not the UICollectionViewController object). It seems Apple was very serious about Decoration Views being limited to visual adornments, and not data-driven (although you could obviously hack around this).
So, the "right" solution still eludes me, but in the mean time I just created a static UIView object and am just managing that from my UICollectionViewController object. It works OK, but feels wrong.
Update re: Sticky HEADERS
Over the last few months, I've worked on similar issues across various projects, and did recently find a solution for sticky HEADERS. I assume the same would apply to footers, but I haven't tested it.
Details about headers here:
How to make Supplementary View float in UICollectionView as Section Headers do in UITableView plain style
The implementation is pretty heavy, but it seems to work well in most circumstances.
If there is no further activity on this question soon, I will close as a duplicate and point to the article above.
Basically what you need to do is provide a custom UICollectionViewLayout subclass that invalidates itself when the bounds change (when the view scrolls the bounds change). And then provides UICollectionViewLayoutAttributes for the supplementary view where the center is updated to hug the current bounds of the collection view (top, bottom, left, right, whatever).
I added project on github that demonstrates this strategy.
UPDATE: as of iOS 9, UICollectionViewFlowLayout has two very handy properties that simplify this task drastically. See sectionHeadersPinToVisibleBounds and sectionFootersPinToVisibleBounds.
ok.. so I have tried updating code from below link. and it works.
https://teamtreehouse.com/community/add-sticky-footer-to-uicollectionview-in-swift
class StickyFooter : UICollectionViewFlowLayout {
var footerIsFound : Bool = false
var UICollectionAttributes : [UICollectionViewLayoutAttributes]?
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
UICollectionAttributes = super.layoutAttributesForElements(in: rect)
for attributes in UICollectionAttributes! {
if let type = attributes.representedElementKind {
if type == UICollectionElementKindSectionFooter
{
footerIsFound = true
updateFooter(attributes: attributes)
}
}
}
if (!self.footerIsFound) {
let newItem = self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionFooter, at : NSIndexPath(row: self.UICollectionAttributes!.count, section: 0) as IndexPath)
UICollectionAttributes?.append(newItem!)
}
return UICollectionAttributes
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
{
let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath)
attributes.size = CGSize(width: self.collectionView!.bounds.size.width, height: 75)
if elementKind.isEqualToString(find: UICollectionElementKindSectionFooter)
{
updateFooter(attributes: attributes)
}
return attributes
}
func updateFooter(attributes : UICollectionViewLayoutAttributes){
let currentBounds = self.collectionView?.bounds
attributes.zIndex = 1024
attributes.isHidden = false
let yOffset = currentBounds!.origin.y + currentBounds!.size.height - attributes.size.height/2.0
attributes.center = CGPoint(x: currentBounds!.midX, y: yOffset)
}}
UICollectionViewController (like UITableVC) is a "shortcut", these 2 classes are just overriding UIViewController, create the collectionView for you.
You can easily do that and add your sticky view on your own.