gestureRecognizerShouldBegin: Unrecognized selector sent to instance - ios

I am implementing a Collection View with a Calendar From based on Sapporo from nghialv ( https://github.com/nghialv/Sapporo ) for a complete different purpose. I want to define pulses which are going to be sent in six different channels in a 10 second frame. Therefore I have a PulseCollectionView of Seconds vs. Channels.
I want to implement cell dragging after long press using this tutorial: http://karmadust.com/drag-and-rearrange-uicollectionviews-through-layouts/.
My problem is that I am getting an error when I do long press on a cell:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSConcreteMapTable gestureRecognizerShouldBegin:]: unrecognized selector sent to instance 0x7b6bea30'
I have tried to find the faulty line through the debugger, but it seems I cannot find it. It breaks before even getting to gestureRecognizerShouldBegin
This is my Layout File:
import UIKit
import Sapporo
let SecondsMaximum : CGFloat = 10
let ChannelsMaximum : CGFloat = 6
let HorizontalSpacing : CGFloat = 10
let VerticalSpacing : CGFloat = 4
//let WidthPerSecond : CGFloat = 100
let SecondHeaderHeight : CGFloat = 40
let ChannelHeaderWidth : CGFloat = 100
class PulseLayout: SALayout, UIGestureRecognizerDelegate {
override func collectionViewContentSize() -> CGSize {
let contentHeight = collectionView!.bounds.size.height
let contentWidth = collectionView!.bounds.size.width
return CGSizeMake(contentWidth, contentHeight)
}
override func awakeFromNib() {
super.awakeFromNib()
self.setupGestureRecognizer()
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
// Cells
let visibleIndexPaths = indexPathsOfItemsInRect(rect)
layoutAttributes += visibleIndexPaths.map {
self.layoutAttributesForItemAtIndexPath($0)
}
// Supplementary views
let secondHeaderViewIndexPaths = indexPathsOfSecondHeaderViewsInRect(rect)
layoutAttributes += secondHeaderViewIndexPaths.map {
self.layoutAttributesForSupplementaryViewOfKind(PulseHeaderType.Second.rawValue, atIndexPath: $0)
}
let channelHeaderViewIndexPaths = indexPathsOfChannelHeaderViewsInRect(rect)
layoutAttributes += channelHeaderViewIndexPaths.map {
self.layoutAttributesForSupplementaryViewOfKind(PulseHeaderType.Channel.rawValue, atIndexPath: $0)
}
// Decoration Views
let verticalGridViewIndexPaths = indexPathsOfVerticalGridLineViewsInRect(rect)
layoutAttributes += verticalGridViewIndexPaths.map {
self.layoutAttributesForDecorationViewOfKind(GridLineType.Vertical.rawValue, atIndexPath: $0)
}
let horizontalGridViewIndexPaths = indexPathsOfHorizontalGridLineViewsInRect(rect)
layoutAttributes += horizontalGridViewIndexPaths.map {
self.layoutAttributesForDecorationViewOfKind(GridLineType.Horizontal.rawValue, atIndexPath: $0)
}
return layoutAttributes
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
var attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
if let event = (getCellModel(indexPath) as? PulseEventCellModel)?.event {
attributes.frame = frameForEvent(event)
}
return attributes
}
override func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
var attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, withIndexPath: indexPath)
let totalHeight = collectionViewContentSize().height
let totalWidth = collectionViewContentSize().width
if elementKind == PulseHeaderType.Second.rawValue {
let availableWidth = totalWidth - ChannelHeaderWidth
let widthPerSecond = availableWidth / SecondsMaximum
attributes.frame = CGRectMake(ChannelHeaderWidth + widthPerSecond * CGFloat(indexPath.item), 0, widthPerSecond, SecondHeaderHeight)
attributes.zIndex = -10
} else if elementKind == PulseHeaderType.Channel.rawValue {
let availableHeight = totalHeight - SecondHeaderHeight
let heightPerChannel = availableHeight / ChannelsMaximum
attributes.frame = CGRectMake(0, SecondHeaderHeight + (heightPerChannel * CGFloat(indexPath.item)), ChannelHeaderWidth, heightPerChannel)
attributes.zIndex = -10
}
return attributes
}
override func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
var attributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind, withIndexPath: indexPath)
let totalHeight = collectionViewContentSize().height
let totalWidth = collectionViewContentSize().width
if elementKind == GridLineType.Vertical.rawValue {
let availableWidth = totalWidth - ChannelHeaderWidth
let widthPerSecond = availableWidth / SecondsMaximum
attributes.frame = CGRectMake(ChannelHeaderWidth + widthPerSecond * CGFloat(indexPath.item), 0.0, 1, totalHeight)
attributes.zIndex = -11
} else if elementKind == GridLineType.Horizontal.rawValue {
let availableHeight = totalHeight - SecondHeaderHeight
let heightPerChannel = availableHeight / ChannelsMaximum
attributes.frame = CGRectMake(0.0, SecondHeaderHeight + (heightPerChannel * CGFloat(indexPath.item)), totalWidth, 1)
attributes.zIndex = -11
}
return attributes
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
struct Bundle {
var offset : CGPoint = CGPointZero
var sourceCell : UICollectionViewCell
var representationImageView : UIView
var currentIndexPath : NSIndexPath
var canvas : UIView
}
var bundle : Bundle?
var canvas : UIView? {
didSet {
if canvas != nil {
// self.calculateBorders()
}
}
}
func setupGestureRecognizer() {
if let collectionView = self.collectionView {
let longPressGestureRecogniser = UILongPressGestureRecognizer(target: self, action: "handleGesture:")
longPressGestureRecogniser.minimumPressDuration = 0.2
longPressGestureRecogniser.delegate = self
collectionView.addGestureRecognizer(longPressGestureRecogniser)
if self.canvas == nil {
self.canvas = self.collectionView!.superview
}
}
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let ca = self.collectionView!.superview {
if let cv = self.collectionView {
let pointPressedInCanvas = gestureRecognizer.locationInView(ca)
for cell in cv.visibleCells() as! [PulseEventCell] {
let cellInCanvasFrame = ca.convertRect(cell.frame, fromView: cv)
if CGRectContainsPoint(cellInCanvasFrame, pointPressedInCanvas ) {
let representationImage = cell.snapshotViewAfterScreenUpdates(true)
representationImage.frame = cellInCanvasFrame
let offset = CGPointMake(pointPressedInCanvas.x - cellInCanvasFrame.origin.x, pointPressedInCanvas.y - cellInCanvasFrame.origin.y)
let indexPath : NSIndexPath = cv.indexPathForCell(cell as UICollectionViewCell)!
self.bundle = Bundle(offset: offset, sourceCell: cell, representationImageView:representationImage, currentIndexPath: indexPath, canvas: ca)
break
}
}
}
}
return (self.bundle != nil)
}
func handleGesture(gesture: UILongPressGestureRecognizer) -> Void {
if let bundle = self.bundle {
let dragPointOnCanvas = gesture.locationInView(self.collectionView!.superview)
if gesture.state == UIGestureRecognizerState.Began {
bundle.sourceCell.hidden = true
self.canvas?.addSubview(bundle.representationImageView)
UIView.animateWithDuration(0.5, animations: { () -> Void in
bundle.representationImageView.alpha = 0.8
});
}
if gesture.state == UIGestureRecognizerState.Changed {
// Update the representation image
var imageViewFrame = bundle.representationImageView.frame
var point = CGPointZero
point.x = dragPointOnCanvas.x - bundle.offset.x
point.y = dragPointOnCanvas.y - bundle.offset.y
imageViewFrame.origin = point
bundle.representationImageView.frame = imageViewFrame
let dragPointOnCollectionView = gesture.locationInView(self.collectionView)
if let indexPath : NSIndexPath = self.collectionView?.indexPathForItemAtPoint(dragPointOnCollectionView) {
// self.checkForDraggingAtTheEdgeAndAnimatePaging(gesture)
if indexPath.isEqual(bundle.currentIndexPath) == false {
// If we have a collection view controller that implements the delegate we call the method first
/*if let delegate = self.collectionView!.delegate as UICollectionViewDelegate? {
delegate.moveDataItem(bundle.currentIndexPath, toIndexPath: indexPath)
}*/
self.collectionView!.moveItemAtIndexPath(bundle.currentIndexPath, toIndexPath: indexPath)
self.bundle!.currentIndexPath = indexPath
}
}
}
if gesture.state == UIGestureRecognizerState.Ended {
bundle.sourceCell.hidden = false
bundle.representationImageView.removeFromSuperview()
if let delegate = self.collectionView!.delegate as UICollectionViewDelegate? { // if we have a proper data source then we can reload and have the data displayed correctly
self.collectionView!.reloadData()
}
self.bundle = nil
}
}
}
}
extension PulseLayout {
func indexPathsOfEventsBetweenMinSecondIndex(minChannelIndex: Int, maxChannelIndex: Int, minStartSecond: Int, maxStartSecond: Int) -> [NSIndexPath] {
var indexPaths = [NSIndexPath]()
if let cellmodels = getCellModels(0) as? [PulseEventCellModel] {
for i in 0..<cellmodels.count {
let event = cellmodels[i].event
if event.channel >= minChannelIndex && event.channel <= maxChannelIndex && event.startSecond >= minStartSecond && event.startSecond <= maxStartSecond {
let indexPath = NSIndexPath(forItem: i, inSection: 0)
indexPaths.append(indexPath)
}
}
}
return indexPaths
}
func indexPathsOfItemsInRect(rect: CGRect) -> [NSIndexPath] {
let minVisibleChannel = channelIndexFromYCoordinate(CGRectGetMinY(rect))
let maxVisibleChannel = channelIndexFromYCoordinate(CGRectGetMaxY(rect))
let minVisibleSecond = secondIndexFromXCoordinate(CGRectGetMinX(rect))
let maxVisibleSecond = secondIndexFromXCoordinate(CGRectGetMaxX(rect))
return indexPathsOfEventsBetweenMinSecondIndex(minVisibleChannel, maxChannelIndex: maxVisibleChannel, minStartSecond: minVisibleSecond, maxStartSecond: maxVisibleSecond)
}
func channelIndexFromYCoordinate(yPosition: CGFloat) -> Int {
let contentHeight = collectionViewContentSize().height - SecondHeaderHeight
let HeightPerChannel = contentHeight / ChannelsMaximum
let channelIndex = max(0, Int((yPosition - SecondHeaderHeight) / HeightPerChannel))
return channelIndex
}
func secondIndexFromXCoordinate(xPosition: CGFloat) -> Int {
let contentWidth = collectionViewContentSize().width - ChannelHeaderWidth
let WidthPerSecond = contentWidth / SecondsMaximum
let secondIndex = max(0, Int((xPosition - ChannelHeaderWidth) / WidthPerSecond))
return secondIndex
}
func indexPathsOfSecondHeaderViewsInRect(rect: CGRect) -> [NSIndexPath] {
if CGRectGetMinY(rect) > SecondHeaderHeight {
return []
}
let minSecondIndex = secondIndexFromXCoordinate(CGRectGetMinX(rect))
let maxSecondIndex = secondIndexFromXCoordinate(CGRectGetMaxX(rect))
return (minSecondIndex...maxSecondIndex).map { index -> NSIndexPath in
NSIndexPath(forItem: index, inSection: 0)
}
}
func indexPathsOfChannelHeaderViewsInRect(rect: CGRect) -> [NSIndexPath] {
if CGRectGetMinX(rect) > ChannelHeaderWidth {
return []
}
let minChannelIndex = channelIndexFromYCoordinate(CGRectGetMinY(rect))
let maxChannelIndex = channelIndexFromYCoordinate(CGRectGetMaxY(rect))
return (minChannelIndex...maxChannelIndex).map { index -> NSIndexPath in
NSIndexPath(forItem: index, inSection: 0)
}
}
func indexPathsOfVerticalGridLineViewsInRect(rect: CGRect) -> [NSIndexPath] {
if CGRectGetMaxX(rect) < SecondHeaderHeight {
return []
}
let minSecondIndex = secondIndexFromXCoordinate(CGRectGetMinX(rect))
let maxSecondIndex = secondIndexFromXCoordinate(CGRectGetMaxX(rect))
return (minSecondIndex...maxSecondIndex).map { index -> NSIndexPath in
NSIndexPath(forItem: index, inSection: 0)
}
}
func indexPathsOfHorizontalGridLineViewsInRect(rect: CGRect) -> [NSIndexPath] {
if CGRectGetMaxY(rect) < ChannelHeaderWidth {
return []
}
let minChannelIndex = channelIndexFromYCoordinate(CGRectGetMinY(rect))
let maxChannelIndex = channelIndexFromYCoordinate(CGRectGetMaxY(rect))
return (minChannelIndex...maxChannelIndex).map { index -> NSIndexPath in
NSIndexPath(forItem: index, inSection: 0)
}
}
func frameForEvent(event: PulseEvent) -> CGRect {
let totalHeight = collectionViewContentSize().height - SecondHeaderHeight
let HeightPerChannel = totalHeight / ChannelsMaximum
let contentWidth = collectionViewContentSize().width - ChannelHeaderWidth
let WidthPerSecond = contentWidth / SecondsMaximum
var frame = CGRectZero
frame.origin.x = ChannelHeaderWidth + WidthPerSecond * CGFloat(event.startSecond)
frame.origin.y = SecondHeaderHeight + HeightPerChannel * CGFloat(event.channel)
frame.size.height = HeightPerChannel
frame.size.width = CGFloat(event.durationInSeconds) * WidthPerSecond
frame = CGRectInset(frame, 2.0, HeightPerChannel/8.0)
return frame
}
}
And this is my ViewController:
import UIKit
import Sapporo
enum PulseHeaderType: String {
case Second = "SecondHeaderView"
case Channel = "ChannelHeaderView"
}
enum GridLineType: String {
case Vertical = "VerticalGridLineView"
case Horizontal = "HorizontalGridLineView"
}
class PulseViewController: UIViewController {
#IBOutlet var collectionView: UICollectionView!
lazy var sapporo: Sapporo = { [unowned self] in
return Sapporo(collectionView: self.collectionView)
}()
override func viewDidLoad() {
super.viewDidLoad()
sapporo.delegate = self
sapporo.registerNibForClass(PulseEventCell)
sapporo.registerNibForSupplementaryClass(PulseHeaderView.self, kind: PulseHeaderType.Second.rawValue)
sapporo.registerNibForSupplementaryClass(PulseHeaderView.self, kind: PulseHeaderType.Channel.rawValue)
let layout = PulseLayout()
layout.registerClass(GridLineView.self, forDecorationViewOfKind: GridLineType.Vertical.rawValue)
layout.registerClass(GridLineView.self, forDecorationViewOfKind: GridLineType.Horizontal.rawValue)
sapporo.setLayout(layout)
let randomEvent = { () -> PulseEvent in
let randomID = arc4random_uniform(10000)
let title = "Event \(randomID)"
let randomChannel = Int(arc4random_uniform(6))
let randomStartSecond = Int(arc4random_uniform(8))
let randomDuration = Int(arc4random_uniform(2) + 1)
return PulseEvent(title: title, channel: randomChannel, startSecond: randomStartSecond, durationInSeconds: randomDuration)
}
let cellmodels = (0...20).map { _ -> PulseEventCellModel in
let event = randomEvent()
return PulseEventCellModel(event: event) { _ in
println("Selected event: \(event.title)")
}
}
sapporo[0].append(cellmodels)
sapporo.bump()
}
}
extension PulseViewController: SapporoDelegate {
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
if kind == PulseHeaderType.Second.rawValue {
let view = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: PulseHeaderView.reuseIdentifier, forIndexPath: indexPath) as! PulseHeaderView
view.titleLabel.text = "\(indexPath.item)"
return view
}
let view = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: PulseHeaderView.reuseIdentifier, forIndexPath: indexPath) as! PulseHeaderView
view.titleLabel.text = "Channel \(indexPath.item + 1)"
return view
}
}
Can you please help me spot what I am missing?
Regards,
C

I solved this problem a while ago.:
struct Bundle
func setupGestureRecognizer()
func gestureRecognizerShouldBegin
func handleGesture(gesture: UILongPressGestureRecognizer) -> Void
should all go in the ViewController instead of the Layout File.

Related

Create a UICollectionView with a reversed flow layout

I want to create a view that resembles a chat view, where the newest message appears at the bottom of the screen. I would like the UICollectionViewCells to be 'pinned' to the bottom of the screen - the opposite of what the default flow layout does. How can I do this?
I've got this code from another stackoverflow answer, but it doesn't work with variable height items (which it needs to):
import Foundation
import UIKit
class InvertedStackLayout: UICollectionViewLayout {
let cellHeight: CGFloat = 100 // Your cell height here...
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttrs = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numberOfSectionItems = numberOfItemsInSection(section) {
for item in 0 ..< numberOfSectionItems {
let indexPath = IndexPath(item: item, section: section)
let layoutAttr = layoutAttributesForItem(at: indexPath)
if let layoutAttr = layoutAttr, layoutAttr.frame.intersects(rect) {
layoutAttrs.append(layoutAttr)
}
}
}
}
}
return layoutAttrs
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let contentSize = self.collectionViewContentSize
layoutAttr.frame = CGRect(
x: 0, y: contentSize.height - CGFloat(indexPath.item + 1) * cellHeight,
width: contentSize.width, height: cellHeight)
return layoutAttr
}
func numberOfItemsInSection(_ section: Int) -> Int? {
if let collectionView = self.collectionView,
let numSectionItems = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: section)
{
return numSectionItems
}
return 0
}
override var collectionViewContentSize: CGSize {
get {
var height: CGFloat = 0
var bounds: CGRect = .zero
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numItems = numberOfItemsInSection(section) {
height += CGFloat(numItems) * cellHeight
}
}
bounds = collectionView.bounds
}
return CGSize(width: bounds.width, height: max(height, bounds.height))
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if let oldBounds = self.collectionView?.bounds,
oldBounds.width != newBounds.width || oldBounds.height != newBounds.height
{
return true
}
return false
}
}
Flip your collectionView with a CGAffineTransform so that it starts at the bottom.
collectionView.transform = CGAffineTransform(scaleX: 1, y: -1)
The only problem now is that all your cells display upside down. You then apply the transform to each cell to flip it back upright.
class InvertedCollectionViewFlowLayout: UICollectionViewFlowLayout {
// inverting the transform in the layout, rather than directly on the cell,
// is the only way I've found to prevent cells from flipping during animated
// cell updates
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = super.layoutAttributesForItem(at: indexPath)
attrs?.transform = CGAffineTransform(scaleX: 1, y: -1)
return attrs
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attrsList = super.layoutAttributesForElements(in: rect)
if let list = attrsList {
for i in 0..<list.count {
list[i].transform = CGAffineTransform(scaleX: 1, y: -1)
}
}
return attrsList
}
}
UICollectionView with a reversed flow layout and dynamic cell and header height.
import Foundation
import UIKit
class InvertedFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard super.layoutAttributesForElements(in: rect) != nil else { return nil }
var attributesArrayNew = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
if let attributeCell = layoutAttributesForItem(at: indexPathCurrent) {
if attributeCell.frame.intersects(rect) {
attributesArrayNew.append(attributeCell)
}
}
}
}
for section in 0 ..< collectionView.numberOfSections {
let indexPathCurrent = IndexPath(item: 0, section: section)
if let attributeKind = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: indexPathCurrent) {
attributesArrayNew.append(attributeKind)
}
}
}
return attributesArrayNew
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributeKind = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath)
if let collectionView = self.collectionView {
var fullHeight: CGFloat = 0.0
for section in 0 ..< indexPath.section + 1 {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
fullHeight += cellHeight(indexPathCurrent) + minimumLineSpacing
}
}
attributeKind.frame = CGRect(x: 0, y: collectionViewContentSize.height - fullHeight - CGFloat(indexPath.section + 1) * headerHeight(indexPath.section) - sectionInset.bottom + minimumLineSpacing/2, width: collectionViewContentSize.width, height: headerHeight(indexPath.section))
}
return attributeKind
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributeCell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
if let collectionView = self.collectionView {
var fullHeight: CGFloat = 0.0
for section in 0 ..< indexPath.section + 1 {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
fullHeight += cellHeight(indexPathCurrent) + minimumLineSpacing
if section == indexPath.section && item == indexPath.item {
break
}
}
}
attributeCell.frame = CGRect(x: 0, y: collectionViewContentSize.height - fullHeight + minimumLineSpacing - CGFloat(indexPath.section) * headerHeight(indexPath.section) - sectionInset.bottom, width: collectionViewContentSize.width, height: cellHeight(indexPath) )
}
return attributeCell
}
override var collectionViewContentSize: CGSize {
get {
var height: CGFloat = 0.0
var bounds = CGRect.zero
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
height += cellHeight(indexPathCurrent) + minimumLineSpacing
}
}
height += sectionInset.bottom + CGFloat(collectionView.numberOfSections) * headerHeight(0)
bounds = collectionView.bounds
}
return CGSize(width: bounds.width, height: max(height, bounds.height))
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if let oldBounds = self.collectionView?.bounds,
oldBounds.width != newBounds.width || oldBounds.height != newBounds.height {
return true
}
return false
}
func cellHeight(_ indexPath: IndexPath) -> CGFloat {
if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
let size = delegateFlowLayout.collectionView!(collectionView, layout: self, sizeForItemAt: indexPath)
return size.height
}
return 0
}
func headerHeight(_ section: Int) -> CGFloat {
if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
let size = delegateFlowLayout.collectionView!(collectionView, layout: self, referenceSizeForHeaderInSection: section)
return size.height
}
return 0
}
}
you could rotate(transform) collectionView 180 degrees so top will be at the bottom and rotate(transform) each cell 180 to make them look normal.

CustomCollectionViewLayout not working on Swift 3

I have implemented UICollectionView and it can scroll horizontal and vertical with sticky rows and columns using below example:
Example
Github
Apps works find on Swift2 but after upgrade Swift3 CustomCollectionViewLayout giving me error
class CustomCollectionViewLayout: UICollectionViewLayout {
let numberOfColumns = 8
var itemAttributes : NSMutableArray!
var itemsSize : NSMutableArray!
var contentSize : CGSize!
override func prepare() {
if self.collectionView?.numberOfSections == 0 {
return
}
if (self.itemAttributes != nil && self.itemAttributes.count > 0) {
for section in 0..<self.collectionView!.numberOfSections {
let numberOfItems : Int = self.collectionView!.numberOfItems(inSection: section)
for index in 0..<numberOfItems {
if section != 0 && index != 0 {
continue
}
let attributes : UICollectionViewLayoutAttributes = self.layoutAttributesForItem(at: IndexPath(item: index, section: section))
if section == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
}
}
return
}
if (self.itemsSize == nil || self.itemsSize.count != numberOfColumns) {
self.calculateItemsSize()
}
var column = 0
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var contentWidth : CGFloat = 0
var contentHeight : CGFloat = 0
for section in 0..<self.collectionView!.numberOfSections {
let sectionAttributes = NSMutableArray()
for index in 0..<numberOfColumns {
let itemSize = (self.itemsSize[index] as AnyObject).cgSizeValue
let indexPath = IndexPath(item: index, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: (itemSize?.width)!, height: (itemSize?.height)!).integral
if section == 0 && index == 0 {
attributes.zIndex = 1024;
} else if section == 0 || index == 0 {
attributes.zIndex = 1023
}
if section == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
sectionAttributes.add(attributes)
xOffset += (itemSize?.width)!
column += 1
if column == numberOfColumns {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += (itemSize?.height)!
}
}
if (self.itemAttributes == nil) {
self.itemAttributes = NSMutableArray(capacity: self.collectionView!.numberOfSections)
}
self.itemAttributes .add(sectionAttributes)
}
let attributes : UICollectionViewLayoutAttributes = (self.itemAttributes.lastObject as AnyObject).lastObject as! UICollectionViewLayoutAttributes
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
self.contentSize = CGSize(width: contentWidth, height: contentHeight)
}
override var collectionViewContentSize : CGSize {
return self.contentSize
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes! {
return self.itemAttributes[indexPath.section][indexPath.row] as! UICollectionViewLayoutAttributes
// Error in above return line and error is Type 'Any' has no subscript members
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = [UICollectionViewLayoutAttributes]()
if self.itemAttributes != nil {
for section in self.itemAttributes {
let filteredArray = (section as AnyObject).filtered(
using: NSPredicate(block: { (evaluatedObject, bindings) -> Bool in
return rect.intersects(evaluatedObject.frame)
// Error in above return line and error is Value of type 'Any?' has no member 'frame'
})
) as! [UICollectionViewLayoutAttributes]
attributes.append(contentsOf: filteredArray)
}
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
func sizeForItemWithColumnIndex(_ columnIndex: Int) -> CGSize {
var text : String = ""
switch (columnIndex) {
case 0:
text = "Col 0"
case 1:
text = "Col 1"
case 2:
text = "Col 2"
case 3:
text = "Col 3"
case 4:
text = "Col 4"
case 5:
text = "Col 5"
case 6:
text = "Col 6"
default:
text = "Col 7"
}
let size : CGSize = (text as NSString).size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 17.0)])
let width : CGFloat = size.width + 25
return CGSize(width: width, height: 30)
}
func calculateItemsSize() {
self.itemsSize = NSMutableArray(capacity: numberOfColumns)
for index in 0..<numberOfColumns {
self.itemsSize.add(NSValue(cgSize: self.sizeForItemWithColumnIndex(index)))
}
}
}
ContentCollectionViewCell
class ContentCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var contentLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
DateCollectionViewCell
class DateCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var dateLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
CollectionViewController
class CollectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
let dateCellIdentifier = "DateCellIdentifier"
let contentCellIdentifier = "ContentCellIdentifier"
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView .register(UINib(nibName: "DateCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: dateCellIdentifier)
self.collectionView .register(UINib(nibName: "ContentCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: contentCellIdentifier)
}
// MARK - UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 8
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print(indexPath.description)
if indexPath.section == 0 {
if indexPath.row == 0 {
let dateCell : DateCollectionViewCell = collectionView .dequeueReusableCell(withReuseIdentifier: dateCellIdentifier, for: indexPath) as! DateCollectionViewCell
dateCell.backgroundColor = UIColor.white
dateCell.dateLabel.font = UIFont.systemFont(ofSize: 13)
dateCell.dateLabel.textColor = UIColor.black
dateCell.dateLabel.text = "Donor ID"
return dateCell
} else {
let contentCell : ContentCollectionViewCell = collectionView .dequeueReusableCell(withReuseIdentifier: contentCellIdentifier, for: indexPath) as! ContentCollectionViewCell
contentCell.contentLabel.font = UIFont.systemFont(ofSize: 13)
contentCell.contentLabel.textColor = UIColor.black
contentCell.contentLabel.text = "Section"
if indexPath.section % 2 != 0 {
contentCell.backgroundColor = UIColor(white: 242/255.0, alpha: 1.0)
} else {
contentCell.backgroundColor = UIColor.white
}
return contentCell
}
} else {
if indexPath.row == 0 {
let dateCell : DateCollectionViewCell = collectionView .dequeueReusableCell(withReuseIdentifier: dateCellIdentifier, for: indexPath) as! DateCollectionViewCell
dateCell.dateLabel.font = UIFont.systemFont(ofSize: 13)
dateCell.dateLabel.textColor = UIColor.black
dateCell.dateLabel.text = String(indexPath.section)
if indexPath.section % 2 != 0 {
dateCell.backgroundColor = UIColor(white: 242/255.0, alpha: 1.0)
} else {
dateCell.backgroundColor = UIColor.white
}
return dateCell
} else {
let contentCell : ContentCollectionViewCell = collectionView .dequeueReusableCell(withReuseIdentifier: contentCellIdentifier, for: indexPath) as! ContentCollectionViewCell
contentCell.contentLabel.font = UIFont.systemFont(ofSize: 13)
contentCell.contentLabel.textColor = UIColor.black
contentCell.contentLabel.text = "Section:\(indexPath.section.description) / Row: \(indexPath.row.description)"
if indexPath.section % 2 != 0 {
contentCell.backgroundColor = UIColor(white: 242/255.0, alpha: 1.0)
} else {
contentCell.backgroundColor = UIColor.white
}
return contentCell
}
}
}
}
Thanks
Hello replace your CustomCollectionViewLayout with this
CustomCollectionViewLayout.swift
class CustomCollectionViewLayout: UICollectionViewLayout {
let numberOfColumns = 8
var itemAttributes : NSMutableArray!
var itemsSize : NSMutableArray!
var contentSize : CGSize!
override func prepare() {
if self.collectionView?.numberOfSections == 0 {
return
}
if (self.itemAttributes != nil && self.itemAttributes.count > 0) {
for section in 0..<self.collectionView!.numberOfSections {
let numberOfItems : Int = self.collectionView!.numberOfItems(inSection: section)
for index in 0..<numberOfItems {
if section != 0 && index != 0 {
continue
}
let attributes : UICollectionViewLayoutAttributes = self.layoutAttributesForItem(at: IndexPath(item: index, section: section))
if section == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
}
}
return
}
if (self.itemsSize == nil || self.itemsSize.count != numberOfColumns) {
self.calculateItemsSize()
}
var column = 0
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var contentWidth : CGFloat = 0
var contentHeight : CGFloat = 0
for section in 0..<self.collectionView!.numberOfSections {
let sectionAttributes = NSMutableArray()
for index in 0..<numberOfColumns {
let itemSize = (self.itemsSize[index] as AnyObject).cgSizeValue
let indexPath = IndexPath(item: index, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: (itemSize?.width)!, height: (itemSize?.height)!).integral
if section == 0 && index == 0 {
attributes.zIndex = 1024;
} else if section == 0 || index == 0 {
attributes.zIndex = 1023
}
if section == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
sectionAttributes.add(attributes)
xOffset += (itemSize?.width)!
column += 1
if column == numberOfColumns {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += (itemSize?.height)!
}
}
if (self.itemAttributes == nil) {
self.itemAttributes = NSMutableArray(capacity: self.collectionView!.numberOfSections)
}
self.itemAttributes .add(sectionAttributes)
}
let attributes : UICollectionViewLayoutAttributes = (self.itemAttributes.lastObject as AnyObject).lastObject as! UICollectionViewLayoutAttributes
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
self.contentSize = CGSize(width: contentWidth, height: contentHeight)
}
override var collectionViewContentSize : CGSize {
return self.contentSize
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes! {
//return self.itemAttributes[indexPath.section][indexPath.row] as! UICollectionViewLayoutAttributes
let arr = self.itemAttributes[indexPath.section] as! NSMutableArray
return arr[indexPath.row] as! UICollectionViewLayoutAttributes
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = [UICollectionViewLayoutAttributes]()
if self.itemAttributes != nil {
for section in self.itemAttributes {
let filteredArray = (section as! NSMutableArray).filtered(
using: NSPredicate(block: { (evaluatedObject , bindings) -> Bool in
let a = evaluatedObject as! UICollectionViewLayoutAttributes
return rect.intersects(a.frame)
})
) as! [UICollectionViewLayoutAttributes]
attributes.append(contentsOf: filteredArray)
}
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
func sizeForItemWithColumnIndex(_ columnIndex: Int) -> CGSize {
var text : String = ""
switch (columnIndex) {
case 0:
text = "Col 0"
case 1:
text = "Col 1"
case 2:
text = "Col 2"
case 3:
text = "Col 3"
case 4:
text = "Col 4"
case 5:
text = "Col 5"
case 6:
text = "Col 6"
default:
text = "Col 7"
}
let size : CGSize = (text as NSString).size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 17.0)])
let width : CGFloat = size.width + 25
return CGSize(width: width, height: 30)
}
func calculateItemsSize() {
self.itemsSize = NSMutableArray(capacity: numberOfColumns)
for index in 0..<numberOfColumns {
self.itemsSize.add(NSValue(cgSize: self.sizeForItemWithColumnIndex(index)))
}
}
}
I'm using https://github.com/bartjacobs/StickyHeadersCollectionView
For Swift 4 with xCode 9.2 my problem was that StickyHeadersCollectionViewFlowLayout stopped working when Swift 3 #objc inference was set to Default or Off (setting it to On the problem was gone) but since Swift 3 #objc inference is something that should be resolved I took a deeper look and found out that I only needed to use #objc on UICollectionViewDelegate methods (for me, to be more precise was just the last one):
// MARK: - Collection View Delegate Flow Layout Methods
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.width, height: self.gridCellHeight)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets.zero
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0.0
}
#objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
if useStickyHeader {
return CGSize(width: collectionView.bounds.width, height: self.gridCellHeight)
} else {
return CGSize.zero
}
}

Can't update UICollectionView with custom layout after retrieving new data from web

In my application there is a collection view, that must update (load new posts at bottom), when i reach last - 10 item.
I am using Alamofire to get new posts.
Also, my collection view has custom layout.
Strange thing: when application loads up for the first time, collection view updates properly and I can see first portion of posts, but when I scroll and call a method to load more, it loads posts, appends it to data source, but collection view doesn't get update, even if I try to update it on main queue.
My code:
FeedViewController.swift:
private let reuseIdentifier = "Cell"
class FeedViewController: UICollectionViewController {
var posts : [GPost] = []
var offsetForPosts = 0
let limitForPosts = 50
override func viewDidLoad() {
super.viewDidLoad()
if let layout = collectionView?.collectionViewLayout as? FeedLayout {
layout.delegate = self
}
self.collectionView?.registerNib(UINib(nibName: "GCell", bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)
let initialPath : URLStringConvertible = "\(DEFAULT_PATH)api_key=\(API_KEY)&offset=\(offsetForPosts)&limit=\(self.limitForPosts)"
self.loadPostsForPath(initialPath) { (_posts) in
self.posts.appendContentsOf(_posts)
print(self.posts.count)
dispatch_async(dispatch_get_main_queue(), {
self.collectionView?.reloadData()
})
}
}
func loadPostsForPath(path: URLStringConvertible, completion: (posts: [GPost]) -> ()) {
Alamofire.request(.GET, path, parameters: nil, encoding: .JSON, headers: nil).responseJSON { (response) in
switch response.result {
case .Success(let data):
let JSONData = JSON(data)
var result = [GPost]()
for (_, value) in JSONData["data"] {
let post = GPost(id: value["id"].stringValue,
url: NSURL(string: value["images"]["fixed_width_downsampled"]["url"].stringValue)!,
slug: value["slug"].stringValue,
imageWidth: CGFloat(value["images"]["original"]["width"].floatValue),
imageHeight: CGFloat(value["images"]["original"]["height"].floatValue))
result.append(post)
}
completion(posts: result)
break
case .Failure(let error):
print("Error loading posts: \(error)")
break
}
}
}
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if indexPath.item == posts.count - 10 {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), {
self.offsetForPosts += 50
let path : URLStringConvertible = "\(DEFAULT_PATH)api_key=\(API_KEY)&offset=\(self.offsetForPosts)&limit=\(self.limitForPosts)"
self.loadPostsForPath(path, completion: { (_posts) in
self.posts.appendContentsOf(_posts)
print(self.posts.count)
dispatch_async(dispatch_get_main_queue(), {
self.collectionView?.reloadData()
self.collectionView?.collectionViewLayout.invalidateLayout()
})
})
})
}
}
}
extension FeedViewController : FeedLayoutDelegate {
func collectionView(collectionView: UICollectionView, heightForPostAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
let post = posts[indexPath.item]
return width * post.getProportion() + 30
}
}
FeedLayout.swift:
protocol FeedLayoutDelegate {
func collectionView(collectionView: UICollectionView, heightForPostAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat
}
class FeedLayout: UICollectionViewLayout {
var delegate : FeedLayoutDelegate!
var numberOfColumns = 2
var cellPadding : CGFloat = 0.0
private var cache = [UICollectionViewLayoutAttributes]()
private var contentHeight : CGFloat = 0.0
private var contentWidth : CGFloat {
let insets = collectionView!.contentInset
return CGRectGetWidth(collectionView!.bounds) - (insets.left + insets.right)
}
override func prepareLayout() {
if cache.isEmpty {
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset = [CGFloat]()
for column in 0..<numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset = [CGFloat](count: numberOfColumns, repeatedValue: 0)
for item in 0..<collectionView!.numberOfItemsInSection(0) {
let indexPath = NSIndexPath(forItem: item, inSection: 0)
let width = columnWidth - cellPadding * 2
let height = cellPadding + delegate.collectionView(collectionView!, heightForPostAtIndexPath: indexPath, withWidth: width) + cellPadding
let frame = CGRectMake(xOffset[column], yOffset[column], columnWidth, height)
let insetFrame = CGRectInset(frame, cellPadding, cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, CGRectGetMaxY(frame))
yOffset[column] = yOffset[column] + height
column = column >= (numberOfColumns - 1) ? 0 : column + 1
}
}
}
override func collectionViewContentSize() -> CGSize {
return CGSizeMake(contentWidth, contentHeight)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if CGRectIntersectsRect(attributes.frame, rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}

CollectionView not displaying cells

I'm trying to add UIDynamics springy-ness to a Horizontal UICollectionViewFlowLayout in Swift.
My view controller is pretty basic, just shows 30 stock items that are green:
import Foundation
import UIKit
class MainViewController: UIViewController {
private var collectionView: UICollectionView?
private let cellIdentifier = "Cell"
init() {
super.init(nibName: nil, bundle: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let layout = SpringyCollectionViewLayout()
let collectionView = UICollectionView(frame: view.frame, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.backgroundColor = UIColor.whiteColor()
collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
collectionView.decelerationRate = 0.9
view.addSubview(collectionView)
self.collectionView = collectionView
view.backgroundColor = UIColor.whiteColor()
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
extension MainViewController : UICollectionViewDataSource {
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 30
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath)
cell.clipsToBounds = true
cell.backgroundColor = UIColor.greenColor()
return cell
}
}
extension MainViewController : UICollectionViewDelegate {
}
My layout subclass looks like this:
import Foundation
import UIKit
let kLength = 0.3
let kDamping = 5.5
let kFrequence = 1.3
let kResistence = 1000
class SpringyCollectionViewLayout: UICollectionViewFlowLayout {
var dynamicAnimator: UIDynamicAnimator!
var visibleIndexPathsSet: NSMutableSet!
var latestDelta = CGFloat()
override init() {
super.init()
sectionInset = UIEdgeInsetsMake(0, 20, 0, 20)
itemSize = CGSizeMake(UIScreen.mainScreen().bounds.width * 0.5, UIScreen.mainScreen().bounds.height * 0.8)
minimumLineSpacing = 20
scrollDirection = .Horizontal
self.dynamicAnimator = UIDynamicAnimator(collectionViewLayout: self)
self.visibleIndexPathsSet = NSMutableSet()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareLayout() {
super.prepareLayout()
// Need to overflow our actual visible rect slightly to avoid flickering.
let visibleRect = CGRectInset(self.collectionView!.bounds, -100, -100)
let itemsInVisibleRectArray: NSArray = super.layoutAttributesForElementsInRect(visibleRect)!
let itemsIndexPathsInVisibleRectSet: NSSet = NSSet(array: itemsInVisibleRectArray.valueForKey("indexPath") as! [AnyObject])
// Step 1: Remove any behaviours that are no longer visible.
let noLongerVisibleBehaviours = (self.dynamicAnimator.behaviors as NSArray).filteredArrayUsingPredicate(NSPredicate(block: {behaviour, bindings in
let currentlyVisible: Bool = itemsIndexPathsInVisibleRectSet.member((behaviour as! UIAttachmentBehavior).items.first!
) != nil
return !currentlyVisible
}))
for (_, obj) in noLongerVisibleBehaviours.enumerate() {
self.dynamicAnimator.removeBehavior(obj as! UIDynamicBehavior)
self.visibleIndexPathsSet.removeObject((obj as! UIAttachmentBehavior).items.first!)
}
// Step 2: Add any newly visible behaviours.
// A "newly visible" item is one that is in the itemsInVisibleRect(Set|Array) but not in the visibleIndexPathsSet
let newlyVisibleItems = itemsInVisibleRectArray.filteredArrayUsingPredicate(NSPredicate(block: {item, bindings in
let currentlyVisible: Bool = self.visibleIndexPathsSet.member(item.indexPath) != nil
return !currentlyVisible
}))
let touchLocation: CGPoint = self.collectionView!.panGestureRecognizer.locationInView(self.collectionView)
for (_, item) in newlyVisibleItems.enumerate() {
let springBehaviour: UIAttachmentBehavior = UIAttachmentBehavior(item: item as! UIDynamicItem, attachedToAnchor: item.center)
springBehaviour.length = CGFloat(kLength)
springBehaviour.damping = CGFloat(kDamping)
springBehaviour.frequency = CGFloat(kFrequence)
// If our touchLocation is not (0,0), we'll need to adjust our item's center "in flight"
if (!CGPointEqualToPoint(CGPointZero, touchLocation)) {
let yDistanceFromTouch = fabsf(Float(touchLocation.y - springBehaviour.anchorPoint.y))
let xDistanceFromTouch = fabsf(Float(touchLocation.x - springBehaviour.anchorPoint.x))
let scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / Float(kResistence)
let item = springBehaviour.items.first as! UICollectionViewLayoutAttributes
var center = item.center
if self.latestDelta < 0 {
center.x += max(self.latestDelta, self.latestDelta * CGFloat(scrollResistance))
} else {
center.x += min(self.latestDelta, self.latestDelta * CGFloat(scrollResistance))
}
item.center = center
}
self.dynamicAnimator.addBehavior(springBehaviour)
self.visibleIndexPathsSet.addObject(item.indexPath)
}
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return self.dynamicAnimator.itemsInRect(rect) as? [UICollectionViewLayoutAttributes]
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return self.dynamicAnimator.layoutAttributesForCellAtIndexPath(indexPath)
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
let scrollView = self.collectionView!
let delta: CGFloat = newBounds.origin.x - scrollView.bounds.origin.x
self.latestDelta = delta
let touchLocation: CGPoint = self.collectionView!.panGestureRecognizer.locationInView(self.collectionView)
for (_, springBehaviour) in self.dynamicAnimator.behaviors.enumerate() {
let springBehav = (springBehaviour as! UIAttachmentBehavior)
let yDistanceFromTouch = fabsf(Float(touchLocation.y - springBehav.anchorPoint.y))
let xDistanceFromTouch = fabsf(Float(touchLocation.x - springBehav.anchorPoint.x))
let scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / Float(kResistence)
let item = springBehav.items.first as! UICollectionViewLayoutAttributes
var center = item.center
if self.latestDelta < 0 {
center.x += max(self.latestDelta, self.latestDelta * CGFloat(scrollResistance))
} else {
center.x += min(self.latestDelta, self.latestDelta * CGFloat(scrollResistance))
}
item.center = center;
self.dynamicAnimator.updateItemUsingCurrentState(item)
}
return false
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat(MAXFLOAT)
let center = proposedContentOffset.x + (CGRectGetWidth(self.collectionView!.bounds) / 2.0)
let proposedRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView!.bounds.size.width, self.collectionView!.bounds.size.height)
let array:NSArray = self.layoutAttributesForElementsInRect(proposedRect)!
for layoutAttributes : AnyObject in array {
if let _layoutAttributes = layoutAttributes as? UICollectionViewLayoutAttributes {
if _layoutAttributes.representedElementCategory != UICollectionElementCategory.Cell {
continue
}
let itemVerticalCenter:CGFloat = layoutAttributes.center.x
let _center = fabsf(Float(itemVerticalCenter) - Float(center))
let _offsetAdjustment = fabsf(Float(offsetAdjustment))
if (_center < _offsetAdjustment) {
offsetAdjustment = (itemVerticalCenter - center)
}
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y)
}
}
The collection view renders with the correct contentSize, but the cells are not there:
If I remove the override of layoutAttributesForElementsInRect that returns the result of itemsInRect from the UIDynamicAnimator, the cells will display - although without the dynamic behaviors:
Any idea of what I'm missing? I've done the same thing before in Objective-C and iOS 8 - when I compare the code it looks similar.
I've debugged prepareLayout and it does find cells in the visible rect, and behaviors are indeed added to the animator.

how to calculate interval between collectionView sections

How to calculate interval between UICollectionView Section or Header frame. I have custom PhotoViewFlowLayout it works pretty well when scrolling through the collection view, but problem in the headerView my headerView display wrong place.
override func prepareLayout() {
super.prepareLayout()
let columnWidth:CGFloat = contentWidth / numberOfColumns
if cache.isEmpty {
var column = 0
var row = 0
if let numberOfSections = collectionView?.numberOfSections() {
for index in 0 ..< numberOfSections {
let indexPath = NSIndexPath(forItem: 0, inSection: index)
stickyHeaderIndexPaths.append(indexPath)
for item in 0 ..< collectionView!.numberOfItemsInSection(0) {
let indexPath = NSIndexPath(forItem: item, inSection: 0)
...
let frame = CGRect(x: cell_x, y: cell_y, width: cell_size, height: cell_size)
let insetFrame = CGRectInset(frame, cellPadding, cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
}
}
}
contentHeight = CGFloat(row) * CGFloat(columnWidth - cellPadding*2)
}
}
override func collectionViewContentSize() -> CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if CGRectIntersectsRect(attributes.frame, rect) {
layoutAttributes.append(attributes)
}
}
for indexPath in stickyHeaderIndexPaths {
let headerAttribute = layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
print(headerAttribute)
layoutAttributes.append(headerAttribute!)
}
return layoutAttributes
}
override func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttribute = super.layoutAttributesForSupplementaryViewOfKind(elementKind, atIndexPath: indexPath)
let contentOffset = collectionView?.contentOffset ?? CGPointZero
var nextHeaderOrigin = CGPointZero
var nextHeaderIndex : Int = 0
if (indexPath.section + 1 < collectionView?.numberOfSections()) {
nextHeaderIndex = indexPath.section + 1
let nextIndexPath = NSIndexPath(forItem: 0, inSection: nextHeaderIndex)
let nextHeaderFrame = super.layoutAttributesForSupplementaryViewOfKind(elementKind, atIndexPath: nextIndexPath)!.frame
nextHeaderOrigin = nextHeaderFrame.origin
}
else {
return layoutAttribute;
}
var headerFrame = layoutAttribute!.frame
if (scrollDirection == UICollectionViewScrollDirection.Vertical) {
let nextStickyCellY = nextHeaderOrigin.y - headerFrame.size.height
let currentStickyCellY = max(contentOffset.y, headerFrame.origin.y)
headerFrame.origin.y = min(currentStickyCellY, nextStickyCellY)
}
else {
let nextStickyCellX = nextHeaderOrigin.x - headerFrame.size.width
let currentStickyCellX = max(contentOffset.x, headerFrame.origin.x)
headerFrame.origin.x = min(currentStickyCellX, nextStickyCellX)
}
layoutAttribute!.zIndex = stickyHeaderZIndex
layoutAttribute!.frame = headerFrame
return layoutAttribute
}
image:

Resources