I'm using a UICollectionView with a Flow Layout. I've set the header, which is a UICollectionReusableView to behave like so;
layout?.sectionHeadersPinToVisibleBounds = true
...
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "header", for: indexPath as IndexPath)
header.layer.zPosition = -1
return header
}
This gives the desired effect that when scrolling the cells up, the header stays pinned but goes behind the regular cells.
However, if I try to click a UICollectionViewCell that is scrolled toward the top, i.e. so it's technically covering the UICollectionReusableView, the UICollectionViewCell's didSelectItemAt tap event no longer fires until I scroll it back down away from where the header is. In other words, the UICollectionReusableView is blocking tap gestures, even though it's zPosition is set to -1 and isn't visible.
Has anyone ever had this issue and how did you fix it?
Adding this to your Section Header view class:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return false
}
will pass all touches through to the next receiver below - in this case, your collection view cell. If you need an interactive element (such as a button) in the Section Header, you can do:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let ptInSub = theButton.convert(point, from: theButton.superview)
if theButton.bounds.contains(ptInSub) {
return true
}
return false
}
This could give you what you want, although... if the Cell View is covering the Button on the Section Header, and you tap the cell where the button is, the button will take the tap. Should be able to get around that with another contains(point) or two...
I ended up adding a Parallax effect to my UICollectionReusableView header. There are a few Parallax libraries out there, but it's actually really simple - I achieved it using the following code;
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if header != nil {
let scrollDiff = scrollView.contentOffset.y - self.previousScrollOffset
let absoluteTop: CGFloat = 0;
let absoluteBottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height;
let isScrollingDown = scrollDiff > 0 && scrollView.contentOffset.y > absoluteTop
let isScrollingUp = scrollDiff < 0 && scrollView.contentOffset.y < absoluteBottom
var newHeight = self.headerHeight
if isScrollingDown {
newHeight = max(self.minHeaderHeight, self.headerHeight - abs(scrollDiff))
} else if isScrollingUp {
newHeight = min(self.maxHeaderHeight, self.headerHeight + abs(scrollDiff))
}
if newHeight != self.headerHeight {
self.headerHeight = newHeight
self.collectionView?.contentOffset = CGPoint(x: (self.collectionView?.contentOffset.x)!, y: self.previousScrollOffset)
self.collectionView?.collectionViewLayout.invalidateLayout()
}
self.previousScrollOffset = scrollView.contentOffset.y
}
}
And then to alter the height of the header (which is called when you invalidateLayout());
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.bounds.width, height:self.headerHeight)
}
The result is that no overlapping ever occurs, but it still gives the desired effect that I was aiming to achieve in the first place.
Related
Here UPCarouselFlowLayout is used for carousel scroll. As of now, user must tap a cell in order to trigger collection view didSelectItemAtIndexPath. Is there a way to select the center cell once the scrolling ended automatically?
here is the code i used to carousel:
let layout = UPCarouselFlowLayout()
layout.itemSize = CGSize(width: 211, height: 75)
layout.scrollDirection = .horizontal
layout.spacingMode = UPCarouselFlowLayoutSpacingMode.fixed(spacing: 10)
layout.spacingMode = UPCarouselFlowLayoutSpacingMode.overlap(visibleOffset: 65)
carCollection.collectionViewLayout = layout
here the code used for collection view:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return carCategory.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! carCollectionViewCell
cell.carName.text = carCategory[indexPath.row]
cell.carImage.image = UIImage(named: carCategoryImage[indexPath.row])
cell.carMeters.text = carCategoryMeter[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("selected index::\(indexPath.row)")
}
If you look at ViewController.swift from the Demo included with ** UPCarouselFlowLayout**, you will see the function scrollViewDidEndDecelerating. That is triggered when the scroll stops moving and a cell become the "center" cell.
In that function, the variable currentPage is set, and that's where the labels below the collection view are changed.
So, that's one place to try what you want to do.
Add the two lines as shown here... when the scroll stops, you create an IndexPath and manually call didSelectItemAt:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let layout = self.collectionView.collectionViewLayout as! UPCarouselFlowLayout
let pageSide = (layout.scrollDirection == .horizontal) ? self.pageSize.width : self.pageSize.height
let offset = (layout.scrollDirection == .horizontal) ? scrollView.contentOffset.x : scrollView.contentOffset.y
currentPage = Int(floor((offset - pageSide / 2) / pageSide) + 1)
// add these two lines
let indexPath = IndexPath(item: currentPage, section: 0)
collectionView(self.collectionView, didSelectItemAt: indexPath)
}
You will almost certainly want to add some error checking and additional functionality (like only calling didSelect if the cell actually changed, as opposed to just sliding it a little but remaining on the current cell), but this is a starting point.
I'm trying to create a complex sticky UICollectionView header:
it must be resizable based on specific scroll criteria, e.g. when a user scrolls past a certain y position, the header will resize.
it must automatically resize while the collection view content is scrolling
it must respond to touch events on the header itself (it can't be a background view that "appears" like a header)
Now this has proven to be quite the challenge, but I've made some progress. To simplify the problem, I'm starting by just creating a header that will resize on scroll and keep its bounds when the touch ends.
To get started, I've created a collection view and supplementary view header that looks like this in its simplest form:
private let defaultHeaderViewHeight: CGFloat = 250.0
private var beginHeaderViewHeight: CGFloat = defaultHeaderViewHeight
private var currentHeaderViewHeight: CGFloat = defaultHeaderViewHeight
private var headerView: HeaderView? {
get {
guard let headerView = collectionView?.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: IndexPath(row: 0, section: 0)) as? HeaderView else { return nil }
return headerView
}
}
// Resizing logic
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
// Define the header size. This is called each time the layout is invalidated, and the beginHeaderViewHeight value is different each time
print("Header ref size delegate called, setting height to \(beginHeaderViewHeight)")
return CGSize(width: collectionView.frame.size.width, height: beginHeaderViewHeight)
// ^ super important!
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
currentHeaderViewHeight = max(0, -(collectionView?.contentOffset.y)! + beginHeaderViewHeight)
headerView?.frame.size.height = currentHeaderViewHeight
headerView?.frame.origin.y = collectionView?.contentOffset.y ?? 0
print("Did scroll\t\t\tcurrentHeaderViewHeight: \(currentHeaderViewHeight), content offset: \(scrollView.contentOffset.y)")
}
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
print("Will end dragging\t\theader view frame height: \(headerView?.frame.size.height), content offset: \(scrollView.contentOffset.y)")
scrollView.bounds.origin = CGPoint(x: 0, y: 0)
self.collectionView?.collectionViewLayout.invalidateLayout()
print("Will end dragging\t\theader view frame height: \(headerView?.frame.size.height), content offset: \(scrollView.contentOffset.y)")
beginHeaderViewHeight = currentHeaderViewHeight
}
// Basic UICollectionView setup
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.register(UINib(nibName: "HeaderView", bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header View")
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 50
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
This kinda works, but not entirely. When I scroll "up" (touch tracks down), it behaves as I'd expect - when the touch ends, the header resizes and sticks in place. Here's a log extract to validate:
Did scroll currentHeaderViewHeight: 447.333333333333, content offset: -70.3333333333333
Did scroll currentHeaderViewHeight: 449.0, content offset: -72.0
Did scroll currentHeaderViewHeight: 451.0, content offset: -74.0
Did scroll currentHeaderViewHeight: 451.666666666667, content offset: -74.6666666666667
Will end dragging header view frame height: Optional(451.666666666667), content offset: -74.6666666666667
Will end dragging header view frame height: Optional(451.666666666667), content offset: 0.0
Did end dragging currentHeaderViewHeight: 451.666666666667, content offset: 0.0
Header ref size delegate called, setting height to 451.666666666667
But when I scroll "down" (touch tracks up), it gets really goofed: it appears that scrollViewDidScroll is being called, even though I'm not setting the content offset anywhere:
Did scroll currentHeaderViewHeight: 330.666666666667, content offset: 121.0
Did scroll currentHeaderViewHeight: 330.333333333333, content offset: 121.333333333333
Did scroll currentHeaderViewHeight: 330.0, content offset: 121.666666666667
Did scroll currentHeaderViewHeight: 328.333333333333, content offset: 123.333333333333
Will end dragging header view frame height: Optional(328.333333333333), content offset: 123.333333333333
Will end dragging header view frame height: Optional(328.333333333333), content offset: 0.0
Did end dragging currentHeaderViewHeight: 328.333333333333, content offset: 0.0
Header ref size delegate called, setting height to 328.333333333333
// WHY ARE THESE LAST TWO LINES CALLED?
Did scroll currentHeaderViewHeight: 205.0, content offset: 123.333333333333
Did end deceler.. currentHeaderViewHeight: 205.0, content offset: 123.333333333333
So the tl;dr here: why on earth does it work when I scroll in one direction and not the other?
Bonus points: If there's a cleaner, simpler way to do all of this, feel free to share!
I am using UICollectionView which has WebView. I have created CollectionView programmatically with UICollectionViewFlowLayout, like this
collectionViewLayout.sectionInset = UIEdgeInsetsZero
collectionViewLayout.minimumLineSpacing = 0
collectionViewLayout.minimumInteritemSpacing = 0
collectionViewLayout.scrollDirection = .Horizontal
collectionView1 = UICollectionView(frame: screenBounds, collectionViewLayout: collectionViewLayout)
when scrollView of webView reached to bottom, There is an API call which fetch some Html data and shows in WebView and here i am reloading collectionView here in main thread, but for first 2 times it doesnt call any method in UICollectionVIewDataSource or Delegate,but on third time i calls all function, I am not sure what cause this incorrect behaviour till now collectionView1 has only one cell.
i have set the UIWebViewDelegate like this
public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CustomReaderCell
cell.webView.scrollView.delegate = self
}
function where i m calling API
extension CustomReaderViewController : UIScrollViewDelegate
{
public func scrollViewDidScroll(scrollView: UIScrollView)
{
if (scrollView.contentOffset.y == (scrollView.contentSize.height - scrollView.frame.size.height) )
{
// Reached To Bottom
// Calling API here
dispatch_async(dispatch_get_main_queue(), {
self.callAPI()
})
}
}
}
And when receive the result, reloading the collectionView
dispatch_async(dispatch_get_main_queue()) {
self.collectionView1.reloadData()
}
I set up a UICollectionView that has a following settings:
collectionView fits screen bounds
only vertical scroll is applied
most of cells fit to content's width
some of cells can change their heights on user interaction dynamically (animated)
It's pretty much like a UITableView, which works fine in most cases, except one specific situation when the animation doesn't apply.
Among stacked cells in collectionView, say one of the upper cells expands its height. Then the lower cell must be moving downwards to keep the distance. If this moving cell's target frame is out of collectionView's bounds, then no animation applies and the cell disappears.
Opposite case works the same way; if the lower cell's source frame is out of screen bounds (currently outside of the bounds) and the upper cell should shrink, no animation applies and it just appear on target frame.
This seems appropriate in memory management logic controlled by UICollectionView, but at the same time nothing natural to show users that some of contents just appear or disappear out of blue. I had tested this with UITableView and the same thing happens.
Is there a workaround for this issue?
You should add some code or at least a gif of your UI problem.
I tried to replicate your problem using a basic UICollectionViewLayout subclass :
protocol CollectionViewLayoutDelegate: AnyObject {
func heightForItem(at indexPath: IndexPath) -> CGFloat
}
class CollectionViewLayout: UICollectionViewLayout {
weak var delegate: CollectionViewLayoutDelegate?
private var itemAttributes: [UICollectionViewLayoutAttributes] = []
override func prepare() {
super.prepare()
itemAttributes = generateItemAttributes()
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return collectionView?.contentOffset ?? .zero
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return itemAttributes.first { $0.indexPath == indexPath }
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return itemAttributes.filter { $0.frame.intersects(rect) }
}
private func generateItemAttributes() -> [UICollectionViewLayoutAttributes] {
var offset: CGFloat = 0
return (0..<numberOfItems()).map { index in
let indexPath = IndexPath(item: index, section: 0)
let frame = CGRect(
x: 0,
y: offset,
width: collectionView?.bounds.width ?? 0,
height: delegate?.heightForItem(at: indexPath) ?? 0
)
offset = frame.maxY
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
return attributes
}
}
}
In a simple UIViewController, I reloaded the first cell each time a cell is selected:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
updatedIndexPath = IndexPath(item: 0, section: 0)
collectionView.reloadItems(at: [updatedIndexPath])
}
In that case, I faced an animation like this:
How to fix it ?
I think you could try to tweak the attributes returned by super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) computing its correct frame and play with the z-index.
But you could also simply try to invalidate all the layout like so:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
layout = CollectionViewLayout()
layout.delegate = self
collectionView.setCollectionViewLayout(layout, animated: true)
}
and override:
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return collectionView?.contentOffset ?? .zero
}
to avoid a wrong target content offset computation when the layout is invalidated.
my View hierarchy is something like:
TableView-->tableViewCell-->CollectionView-->CollectionViewCell-->imageView
and in my tableViewCell I have some other items (textView, labels and a UIPageControl) so now am trying to change the currentPage of PageControl according to the item of CollectionViewCell ( same as carousel ) but I don't know why UIPageControl is not changing its position this is what I tried:
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
// here scrollView is my collectionView
if scrollView != tableView {
pagerControll.currentPage = Int(scrollView.contentOffset.x / self.view.frame.size.width)
}
}
I also tried this (for remembering the current position of Item in a cell after scrolling through the tableView)
override func tableView(tableView: UITableView,
willDisplayCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath) {
guard let cell = cell as? NotesTableViewCell else { return }
cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
cell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0
cell.pageControll.currentPage = calculateCurrentPage(storedOffsets[indexPath.row] ?? 0)
}
above am fetching the contentOffset of each Row from an array of (contentoffset) so that I can show the previous positions Of CollectionViewCell's items, when tableView reuses the cell its working fine for the items of my collectionView cell but not for my UIPageControl
func calculateCurrentPage(offSet : CGFloat) -> Int{
if offSet >= self.view.frame.size.width && offSet < (self.view.frame.size.width*2) {
return 1
}else if offSet >= (self.view.frame.size.width*2) {
return 2
}else {
return 0
}
}
What's wrong here? or how to do it?
UPDate Extra Codes:
//inside my tableViewCell
func setCollectionViewDataSourceDelegate
>
(dataSourceDelegate: D, forRow row: Int) {
collectionView.delegate = dataSourceDelegate
collectionView.dataSource = dataSourceDelegate
collectionView.tag = row
collectionView.reloadData()
}
//in TableView
extension NotesTableViewController: UICollectionViewDelegate, UICollectionViewDataSource , UICollectionViewDelegateFlowLayout {
func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
pagerControll.numberOfPages = attachedImgUrlDict[collectionView.tag]!.count
return attachedImgUrlDict[collectionView.tag]!.count
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let newSize = CGSizeMake(collectionView.frame.size.width , collectionView.frame.size.height)
return newSize
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, 0, 0, 0)
//t,l,b,r
}
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
if scrollView != tableView {
if scrollView.contentOffset.x >= 320 && scrollView.contentOffset.x < 640{
pagerControll.currentPage = 1
}else if scrollView.contentOffset.x >= 600 {
pagerControll.currentPage = 2
}
//pagerControll.currentPage = Int(scrollView.contentOffset.x / self.view.frame.size.width)
print(scrollView.contentOffset)
}
}
}
Check #1
You'll receive callbacks for scroll events for UICollectionView as soon as you set a UICollectionViewDelegate on the collectionView instance.
Seems like you might be missing setting up UICollectionViewDelegate in following call
cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
Can you verify you're receiving callbacks for UICollectionView scroll events?
Check #2
Say you are receiving callbacks properly now, Can you check your logic works properly with page index? A good thing would be to add a print statement that would show you the page index that you are calculating.
let pageIndex = Int(scrollView.contentOffset.x / self.view.frame.size.width)
print("pageIndex == \(pageIndex)")
Check #3
Say you are calculating it right, Can you check if cell.pageControl is populated with properly with UIPageControl instance you need to update.
Maybe you need to check your IBOutlet connections?
Check #4
Inside prepareForReuse() callback, you need to make sure that pageControl is set to some initial value like 0.
Adding a small delay while updating the pageIndex could work if you see inconsistencies like it is updating sometimes and sometimes it's not.
Hope it helps.