UICollectionViewFlowLayout Cell with custom anchorPoint reused too soon - ios

I've subclassed a UICollectionViewFlowLayout to achieve a small scaling effect during horizontal scroll. Therefore I had to subclass a UICollectionViewCell as well as to change the layer.anchorPoint of the cell (my scaling is from the bottom left of the cell rather than from the default center). Now all fine and well except the fact that when I am scrolling horizontally , my cell is reused too soon (I still can see the half cell when it suddenly disappear ).
I have the feeling that collection view still bases its calculations for reusing the cell on the anchor point positioned in the center of the cell...
However , this is my collection view . You can see how the item getting bigger as it reaches the left side of the collection view. This is the scaling I wanted.
Now here I am scrolling to the left. You can see how the right item became bigger and the left is getting out of the screen.
And here you see that the left item didn't get off the screen but already dissapeared. Only the right item remeained visible :/
So what I want is , to make the left item disappear only when it's right boundaries reaching the very left of the screen.Simply saying , to dissapear only when I don't see it anymore.
And here is my code:
class SongsCollectionViewCell : UICollectionViewCell {
#IBOutlet weak var imgAlbumCover: UIImageView!
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes) {
super.applyLayoutAttributes(layoutAttributes)
//we must change the anchor point for propper cells positioning and scaling
self.layer.anchorPoint.x = 0
self.layer.anchorPoint.y = 1
}
}
Here is the layout itself :
class SongsCollectionViewLayout : UICollectionViewFlowLayout {
override func prepareLayout() {
collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
self.scrollDirection = UICollectionViewScrollDirection.Horizontal;
//size of the viewport
let size:CGSize = self.collectionView!.frame.size;
let itemWidth:CGFloat = size.width * 0.7//0.7//3.0;
self.itemSize = CGSizeMake(itemWidth, itemWidth);
self.sectionInset = UIEdgeInsetsMake(0,0,0,0);
self.minimumLineSpacing = 0
self.minimumInteritemSpacing = 0
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes:[UICollectionViewLayoutAttributes] = super.layoutAttributesForElementsInRect(rect)!
var visibleRect:CGRect = CGRect()
visibleRect.origin = self.collectionView!.contentOffset;
visibleRect.size = self.collectionView!.bounds.size;
for layoutAttributes in attributes {
if (CGRectIntersectsRect(layoutAttributes.frame, rect)) {
//we must align items to the bottom of the collection view on y axis
let frameHeight = self.collectionView!.bounds.size.height
layoutAttributes.center.y = frameHeight
layoutAttributes.center.x = layoutAttributes.center.x - self.itemSize.width/2
//find where ite, left x is
let itemLeftX:CGFloat = layoutAttributes.center.x
//distance of the item from the left of the viewport
let distanceFromTheLeft:CGFloat = itemLeftX - CGRectGetMinX(visibleRect)
let normalizedDistanceFromTheLeft = abs(distanceFromTheLeft) / self.collectionView!.frame.size.width
//item that is closer to the left is most visible one
layoutAttributes.alpha = 1 - normalizedDistanceFromTheLeft
layoutAttributes.zIndex = abs(Int(layoutAttributes.alpha)) * 10;
//scale items
let scale = min(layoutAttributes.alpha + 0.5, 1)
layoutAttributes.transform = CGAffineTransformMakeScale(scale, scale)
}
}
return attributes;
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// Snap cells to centre
var newOffset = CGPoint()
let layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout
let width = layout.itemSize.width + layout.minimumLineSpacing
var offset = proposedContentOffset.x + collectionView!.contentInset.left
if velocity.x > 0 {
//ceil returns next biggest number
offset = width * ceil(offset / width)
} else if velocity.x == 0 { //6
//rounds the argument
offset = width * round(offset / width)
} else if velocity.x < 0 { //7
//removes decimal part of argument
offset = width * floor(offset / width)
}
newOffset.x = offset - collectionView!.contentInset.left
newOffset.y = proposedContentOffset.y //y will always be the same...
return newOffset
}
}

Answering my own question.
So as I suspected , the layout was taking an old center into account that is why I had to correct the center of the cell right after changing the anchor point . So my custom cell now looks like this :
class SongsCollectionViewCell : UICollectionViewCell {
#IBOutlet weak var imgAlbumCover: UIImageView!
override func prepareForReuse() {
imgAlbumCover.hnk_cancelSetImage()
imgAlbumCover.image = nil
}
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes) {
super.applyLayoutAttributes(layoutAttributes)
//we must change the anchor point for propper cells positioning and scaling
self.layer.anchorPoint.x = 0
self.layer.anchorPoint.y = 1
//we need to adjust a center now
self.center.x = self.center.x - layoutAttributes.size.width/2
}
}
Hope it helps someone

Related

Autoresize UICollectionView cells, Align cell to top

I've a UICollectionView, with multiple sections and rows.
Header and Footer views wherever necessary, of fixed size.
Cells that are autoresizable
The cell view are designed like :
Green colour - ImageView
Orange colour - Labels with numberOfLines = 0.
The cell should expand it's size according to label numberOfLines.
I've achieved this using this code in MyCustomCell :
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
super.apply(layoutAttributes)
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
let targetSize = CGSize(width: Constants.screenWidth/3.5, height: 0)
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
The cells are autoresizing but the contentView (in cyan colour) are Centre aligned both vertically and horizontally.
I need to make it vertically align to Top.
I had the alignment problem with headers and footers too. For that i've subclassed UICollectionViewFlowLayout
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext {
let context: UICollectionViewLayoutInvalidationContext = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes)
let indexPath = preferredAttributes.indexPath
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter, at: [indexPath])
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader, at: [indexPath])
return context
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var topMargin = sectionInset.top
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.y >= maxY {
leftMargin = sectionInset.left
topMargin = sectionInset.top
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
Here is an image to illustrate current situation. The cyan are contentView of cells. I've to make it align to Top.
EDIT:
So i realised that UICollectionViewFlowLayout code was creating more bugs than fixing a problem. I added layoutAttributesForElements to left align my cell in case there is only one cell. What it actually did was align all of my cells to the left.
Modified the code as
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
if attributes?.count == 1 {
if let currentAttribute = attributes?.first {
currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
}
}
return attributes
}
}
Now my UICollectionViewCell are properly aligned to horizontal centre with exception to if only one cell which will be left aligned.
Still no solution for vertical alignment.
So I worked around this for quite a bit. No answers from stack overflow help in any way. So i played with custom UICollectionViewFlowLayout method layoutAttributesForElements.
This method will call for every section, and will have layoutAttributesForElements for rect. This will give an array of UICollectionViewLayoutAttributes which one can modify as per the need.
I was having problem figuring out the y coordinate which I would like to change for my cells.
So first with a loop in attributes from layoutAttributesForElements I got the header and saved it's height in a variable. Then changed the rest of cell's y coordinate with the headerHeight value.
Not sure if this is the right way or not, nor did any performance testing. For now everything seems fine without any lag or jerk.
Here is full code or custom UICollectionViewFlowLayout.
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attr = super.layoutAttributesForElements(in: rect)
var attributes = [UICollectionViewLayoutAttributes]()
for itemAttributes in attr! {
let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
// manipulate itemAttributesCopy
attributes.append(itemAttributesCopy)
}
if attributes.count == 1 {
if let currentAttribute = attributes.first {
currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
}
} else {
var sectionHeight: CGFloat = 0
attributes.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .supplementaryView else {
return
}
if layoutAttribute.representedElementKind == UICollectionView.elementKindSectionHeader {
sectionHeight = layoutAttribute.frame.size.height
}
}
attributes.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.x == 0 {
sectionHeight = max(layoutAttribute.frame.minY, sectionHeight)
}
layoutAttribute.frame = CGRect(origin: CGPoint(x: layoutAttribute.frame.origin.x, y: sectionHeight), size: layoutAttribute.frame.size)
}
}
return attributes
}
}
NOTE: You need to copy the attributes in an array first to work with. Else xcode will yell Logging only once for UICollectionViewFlowLayout cache mismatched frame in console.
If any new/perfect solution is there please let me know.
CHEERS!!!

How can I apply a CGAffineTransform to a UICollectionViewCell while the UICollectionView is scrolling

I am creating a kind of horizontally scrolling menu with multiple items that the user can scroll through.(see picture at the bottom for a preview of what I mean)
The UICollectionView offset already always centers on one of its items. What I want to do is that when an item is the next one to approach the center, I want to apply a transformation to make it larger. This is the code that I'm using to achieve this (the logic doesn't handle animating out of the center or scrolling the other direction yet):
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let scrolledCollectionView = scrollView as? UICollectionView,
let flowLayout = scrolledCollectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
let itemWidth = flowLayout.itemSize.width
let collectionViewCenter = collectionView.bounds.width * 0.5 + scrolledCollectionView.contentOffset.x
let itemToEnlarge = Int((scrolledCollectionView.contentOffset.x + (itemWidth * 0.5)) / (itemWidth + flowLayout.minimumInteritemSpacing))
let itemEnlargeIndexpath = IndexPath(row: itemToEnlarge, section: 0)
guard let cellToAnimate = collectionView.cellForItem(at: itemEnlargeIndexpath) else { return }
let diff = cellToAnimate.center.x - collectionViewCenter
var transformationVolume: CGFloat = 1
if diff == 0 {
transformationVolume += 0.2
} else {
transformationVolume += (0.2 / diff)
}
DispatchQueue.main.async {
cellToAnimate.transform = CGAffineTransform(scaleX: transformationVolume, y: transformationVolume)
}
}
The problem that I'm having is that the transformation is only applied once the collectionview has stopped scrolling. Does anyone know if there's a way to apply the transformation dynamically? So that if you scroll the item towards the center little by little, the transformation is applied incrementally.

How can you make a custom scroll indicator using Swift?

I have a UITableView that has the potential to have a lot of rows (importing contacts) and I'm looking to implement a custom scroll indicator similar to how Snapchat has. The two basic goals are as follows:
When you scroll the tableview, the scroll indicator moves down/up the correct amount (and does not exceed the top or bottom of the tableview).
When you drag the scroll indicator, it not only follows your finger, but scrolls the tableview to correct amount.
I've been trying to use different calculations for the scroll amount, but I can't seem to get it to be correct. I've been doing work on a sample app (just containing names in a tableview) and I can get it to work okay on there but when I transfer the code to a larger tableview, it doesn't work properly.
I've created a custom view for my scroll indicator which is basically just a view with a label on it that changes based on the letter of the name in the cell:
class IndicatorView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var letterLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("IndicatorView", owner: self, options: nil)
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView)
contentView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
contentView.layer.cornerRadius = 5
}
}
Here are the methods I'm using...
scrollViewWillEndDragging:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
//Get velocity and store in global variable
self.velocity = velocity.y
}
scrollViewDidScroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollBar.becomeFirstResponder()
let offset: CGPoint = scrollView.contentOffset
var frame = self.scrollBar.frame
var numberOfRows = 0
for i in 0...friendsTableView.numberOfSections - 1 {
let rows = friendsTableView.numberOfRows(inSection: i)
numberOfRows += rows
}
//Calculate the amount of scroll
let percentage = (44*CGFloat(numberOfRows) + 40)/friendsTableView.frame.height
UIView.animate(withDuration: 0.0) {
//Set the y value to new percentage
frame.origin.y = 30 + offset.y + (offset.y/percentage)
//Set label to correct letter
let currentPoint = CGPoint(x: 30, y: frame.origin.y)
if let indexPath = self.friendsTableView.indexPathForRow(at: currentPoint) {
let letter = self.sectionHeaders[indexPath.section]
self.scrollBar.letterLabel.text = letter
}
//Only expand the scroll indicator if the user scrolls fast enough
if abs(self.velocity) > 1.5 {
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 50
frame.origin.x = self.screenWidth - 50
}
//Set the new y value of the scroll indicator
self.scrollBar.frame = frame
}
}
Custom Pan Gesture Recognizer:
#objc func customDrag(_ sender: UIPanGestureRecognizer) {
self.friendsTableView.bringSubview(toFront: scrollBar)
//Only expand fully if the state is not ended
if sender.state != .ended {
var frame = self.scrollBar.frame
//Expand the view fully
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 100
frame.origin.x = self.screenWidth - 100
self.scrollBar.frame = frame
//Get translation and track the content offset
let translation = sender.translation(in: friendsTableView)
let originalContentOffset = friendsTableView.contentOffset
var numberOfRows = 0
for i in 0...friendsTableView.numberOfSections - 1 {
let rows = friendsTableView.numberOfRows(inSection: i)
numberOfRows += rows
}
//Calculate the percentage of scroll and add the translation to the original content offset
let percentage = (44*CGFloat(numberOfRows) + 50)/friendsTableView.frame.height
let vertTranslation = (percentage*translation.y) + originalContentOffset.y
//Set the content offset and the center of the scroll indicator
friendsTableView.contentOffset = CGPoint(x: 0, y: (vertTranslation))
self.scrollBar.center = CGPoint(x: self.screenWidth-50, y: self.scrollBar.center.y + (translation.y/percentage))
sender.setTranslation(CGPoint.zero, in: friendsTableView)
} else {
//once the state is ended, collapse back to the smaller size
var frame = self.scrollBar.frame
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 50
frame.origin.x = self.screenWidth - 50
self.scrollBar.frame = frame
}
}
I found the basics for the code that does the percentage calculation on another StackOverflow post but admittedly, I'm not 100% sure what it is doing/why it works in my demo app and not the real one. I've applied different number and calculations but still can't seem to get it right.
Any help or insight would be much appreciated.

How to set UICollectionView bottom padding and scrolling size

Hello I have working uicollectionview custom layout and i have issuesfor bottom padding and scrolling sizes. Under below picture you can see;
Also top side first cell always stays on top when scrolling ( Left side cell must be always stay right now no problem there , but top side cell must be scroll when scrolling )
I try to set in viewDidLoad under below codes but dont work
self.automaticallyAdjustsScrollViewInsets = false
self.collectionView.contentInset = UIEdgeInsetsMake(20, 20, 120, 100);
My custom collectionview layout file
import UIKit
public var CELL_HEIGHT = CGFloat(22)
protocol CustomCollectionViewDelegateLayout: NSObjectProtocol {
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, widthForItemAtIndexPath indexPath: NSIndexPath) -> CGFloat
}
class CustomCollectionViewLayout: UICollectionViewLayout {
// Used for calculating each cells CGRect on screen.
// CGRect will define the Origin and Size of the cell.
let STATUS_BAR = UIApplication.sharedApplication().statusBarFrame.height
// Dictionary to hold the UICollectionViewLayoutAttributes for
// each cell. The layout attribtues will define the cell's size
// and position (x, y, and z index). I have found this process
// to be one of the heavier parts of the layout. I recommend
// holding onto this data after it has been calculated in either
// a dictionary or data store of some kind for a smooth performance.
var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
// Defines the size of the area the user can move around in
// within the collection view.
var contentSize = CGSize.zero
// Used to determine if a data source update has occured.
// Note: The data source would be responsible for updating
// this value if an update was performed.
var dataSourceDidUpdate = true
weak var delegate: CustomCollectionViewDelegateLayout?
override func collectionViewContentSize() -> CGSize {
return self.contentSize
}
override func prepareLayout() {
// Only update header cells.
if !dataSourceDidUpdate {
// Determine current content offsets.
let xOffset = collectionView!.contentOffset.x
let yOffset = collectionView!.contentOffset.y
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Confirm the section has items.
if collectionView?.numberOfItemsInSection(section) > 0 {
// Update all items in the first row.
if section == 0 {
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
// Build indexPath to get attributes from dictionary.
let indexPath = NSIndexPath(forItem: item, inSection: section)
// Update y-position to follow user.
if let attrs = cellAttrsDictionary[indexPath] {
var frame = attrs.frame
// Also update x-position for corner cell.
if item == 0 {
frame.origin.x = xOffset
}
frame.origin.y = yOffset
attrs.frame = frame
}
}
// For all other sections, we only need to update
// the x-position for the fist item.
} else {
// Build indexPath to get attributes from dictionary.
let indexPath = NSIndexPath(forItem: 0, inSection: section)
// Update y-position to follow user.
if let attrs = cellAttrsDictionary[indexPath] {
var frame = attrs.frame
frame.origin.x = xOffset
attrs.frame = frame
}
}
}
}
}
// Do not run attribute generation code
// unless data source has been updated.
return
}
// Acknowledge data source change, and disable for next time.
dataSourceDidUpdate = false
var maxWidthInASection = CGFloat(0)
// Cycle through each section of the data source.
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Cycle through each item in the section.
if collectionView?.numberOfItemsInSection(section) > 0 {
var prevCellAttributes: UICollectionViewLayoutAttributes?
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
let cellIndex = NSIndexPath(forItem: item, inSection: section)
guard let width = delegate?.collectionView(collectionView!, layout: self, widthForItemAtIndexPath: cellIndex) else {
print("Please comform to CustomCollectionViewDelegateLayout protocol")
return
}
// Build the UICollectionVieLayoutAttributes for the cell.
var xPos = CGFloat(0)
let yPos = CGFloat(section) * CELL_HEIGHT
if let prevCellAttributes = prevCellAttributes {
xPos = CGRectGetMaxX(prevCellAttributes.frame)
}
let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: cellIndex)
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: width, height: CELL_HEIGHT)
// Determine zIndex based on cell type.
if section == 0 && item == 0 {
cellAttributes.zIndex = 4
} else if section == 0 {
cellAttributes.zIndex = 3
} else if item == 0 {
cellAttributes.zIndex = 2
} else {
cellAttributes.zIndex = 1
}
// Save the attributes.
cellAttrsDictionary[cellIndex] = cellAttributes
prevCellAttributes = cellAttributes
let maxX = CGRectGetMaxX(cellAttributes.frame)
if maxWidthInASection < maxX {
maxWidthInASection = maxX
}
}
}
}
}
// Update content size.
let contentWidth = maxWidthInASection
let contentHeight = Double(collectionView!.numberOfSections())
self.contentSize = CGSize(width: Double(contentWidth), height: contentHeight)
self.contentSize.height = CGFloat(Double(collectionView!.numberOfSections()))
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Create an array to hold all elements found in our current view.
var attributesInRect = [UICollectionViewLayoutAttributes]()
// Check each element to see if it should be returned.
for cellAttributes in cellAttrsDictionary.values.elements {
if CGRectIntersectsRect(rect, cellAttributes.frame) {
attributesInRect.append(cellAttributes)
}
}
// Return list of elements.
return attributesInRect
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttrsDictionary[indexPath]!
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
}
I try change collecionview properties in storyboard but doesnt works also i attached current collectionview object properties picture under below
Also i selected zoom 4 but zoom feature doesnt work ? why ? it must be work also zoom feature.
I know this is too late to answer, but this could be useful to other users.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 0)
}
This what worked for me:
messagesCollectionView.contentInset.top = 100
messagesCollectionView.contentInset.bottom = 80
This can be also done on IB under section insets...

targetContentOffsetForProposedContentOffset - wrong proposed offset

I have a custom UICollectionViewLayout and it implements targetContentOffsetForProposedContentOffset in order to set paging. The center item in the UICollectionView is the full "large" size, while each other item has a CGAffineTransformScale of the "shrunk" scale value.
My problem is that there appears to be an upper limit on the contentOffset so that I can only scroll to item 5 of 7, and it bounces back. Specifics after the code:
I'm setting the collectionViewContentSize() as follows:
#IBInspectable var shrunkScale: CGFloat = 0.5 // 0.5 in IB
#IBInspectable var largeSize: CGSize = CGSizeZero // 360x490 in IB
#IBInspectable var itemSpacing: CGFloat = 0 // 32 in IB
var widthPerAdditionalItem: CGFloat {
return largeSize.width * shrunkScale + itemSpacing
}
override func collectionViewContentSize() -> CGSize {
guard let collectionView = self.collectionView else {
return CGSizeZero
}
let count = CGFloat(collectionView.numberOfItemsInSection(0))
let width = largeSize.width + (count) * widthPerAdditionalItem
let height = collectionView.bounds.height
let size = CGSize(width: width, height: height)
return size
}
the targetOffset... methods reference a single helper method:
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint) -> CGPoint {
let closestPlace = round(proposedContentOffset.x / widthPerAdditionalItem)
guard let offsetX = offsetXForItemAtIndex(Int(closestPlace)) else {
return proposedContentOffset
}
print("Calculated: \(offsetX), Proposed: \(proposedContentOffset.x), ContentWidth: \(collectionView?.contentSize.width ?? 0 )")
return CGPoint(x: offsetX, y: proposedContentOffset.y)
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
return targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
func contentOffsetForItemAtIndexPath(indexPath: NSIndexPath) -> CGPoint? {
guard
let collectionView = self.collectionView,
let offsetX = offsetXForItemAtIndex(indexPath.item)
else {
return nil
}
print("Tap Offset: - \(offsetX) vs. \(collectionView.contentOffset.x)")
return CGPoint(x: offsetX, y: collectionView.contentOffset.y)
}
private func offsetXForItemAtIndex(index: Int) -> CGFloat? {
guard
let count = collectionView?.numberOfItemsInSection(0)
else {
return nil
}
let proposed = CGFloat(index) * widthPerAdditionalItem
let maximum = CGFloat(count) * widthPerAdditionalItem
// bound min = 0, max = count*width
return max( min(maximum, proposed), 0)
}
Here's What I get:
My content Width is 1844.0
I finish dragging the view at offset.x = 1187.5
The targetContentOffsetForProposedContentOffset receives a proposed offset.x = 820.0
I return the "paged" offset.x value of 848.0
The collectionView scrolls to offset.x of 820.0
What I am expecting:
My content Width is 1844.0
I finish dragging the view at offset.x = 1187.5
The targetContentOffsetForProposedContentOffset receives a
proposed offset.x = 1187.5
I return the "paged" offset.x value of 1272.0
The collectionView scrolls to offset.x of 1272.0
Debugging:
If I manually call setContentOffset with the calculated offset of 1272.0 then it scrolls to the correct position. But the instant I try to scroll it snaps back to 820.0
After sitting down and doing some math I figured out the following:
contentWidth = 1844
poposedContentOffset.x = 820
1844 - 820 = 1024 // <--- Width of the screen.
The content was being displayed from the center of the screen for the first item, to the center of the screen for the second item.
This means I needed to add half the collectionView's frame.width so the first item can be centered, and half again so the final item could be centered.
The collectionViewContentSize() now returns
let width = (count-1) * widthPerAdditionalItem + collectionView.bounds.width
let height = collectionView.bounds.height
let size = CGSize(width: width, height: height)

Resources