// The MIT License (MIT) // // Copyright (c) 2015 Joakim Gyllström // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import UIKit /** Provides a grid collection view layout */ @objc(BSGridCollectionViewLayout) public final class GridCollectionViewLayout: UICollectionViewLayout { /** Spacing between items (horizontal and vertical) */ public var itemSpacing: CGFloat = 0 { didSet { _itemSize = estimatedItemSize() } } /** Number of items per row */ public var itemsPerRow = 3 { didSet { _itemSize = estimatedItemSize() } } /** Item height ratio relative to it's width */ public var itemHeightRatio: CGFloat = 1 { didSet { _itemSize = estimatedItemSize() } } /** Size for each item */ public var itemSize: CGSize { get { return _itemSize } } var items = 0 var rows = 0 var _itemSize = CGSizeZero public override func prepareLayout() { // Set total number of items and rows items = estimatedNumberOfItems() rows = items / itemsPerRow + ((items % itemsPerRow > 0) ? 1 : 0) // Set item size _itemSize = estimatedItemSize() } /** See UICollectionViewLayout documentation */ public override func collectionViewContentSize() -> CGSize { guard let collectionView = collectionView where rows > 0 else { return CGSizeZero } let height = estimatedRowHeight() * CGFloat(rows) return CGSize(width: collectionView.bounds.width, height: height) } /** See UICollectionViewLayout documentation */ public override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return indexPathsInRect(rect).map { (indexPath) -> UICollectionViewLayoutAttributes in return self.layoutAttributesForItemAtIndexPath(indexPath)! // TODO: Fix forcefull unwrap } } /** See UICollectionViewLayout documentation */ public override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { let itemIndex = flatIndex(indexPath) // index among total number of items let rowIndex = itemIndex % itemsPerRow // index within it's row let row = itemIndex / itemsPerRow // which row for that item let x = (CGFloat(rowIndex) * itemSpacing) + (CGFloat(rowIndex) * itemSize.width) let y = (CGFloat(row) * itemSpacing) + (CGFloat(row) * itemSize.height) let width = _itemSize.width let height = _itemSize.height let attribute = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath) attribute.frame = CGRect(x: x, y: y, width: width, height: height) return attribute } /** See UICollectionViewLayout documentation */ public override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { return true } // No decoration or supplementary views /** See UICollectionViewLayout documentation */ public override func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { return nil } /** See UICollectionViewLayout documentation */ public override func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { return nil } } extension GridCollectionViewLayout { /** Calculates which index paths are within a given rect - parameter rect: The rect which we want index paths for - returns: An array of indexPaths for that rect */ func indexPathsInRect(rect: CGRect) -> [NSIndexPath] { // Make sure we have items/rows guard items > 0 && rows > 0 else { return [] } let rowHeight = estimatedRowHeight() let startRow = GridCollectionViewLayout.firstRowInRect(rect, withRowHeight: rowHeight) let endRow = GridCollectionViewLayout.lastRowInRect(rect, withRowHeight: rowHeight, max: rows) let startIndex = GridCollectionViewLayout.firstIndexInRow(startRow, withItemsPerRow: itemsPerRow) let endIndex = GridCollectionViewLayout.lastIndexInRow(endRow, withItemsPerRow: itemsPerRow, numberOfItems: items) let indexPaths = (startIndex...endIndex).map { indexPathFromFlatIndex($0) } return indexPaths } /** Calculates which row index would be first for a given rect. - parameter rect: The rect to check - parameter rowHeight: Height for a row - returns: First row index */ static func firstRowInRect(rect: CGRect, withRowHeight rowHeight: CGFloat) -> Int { if rect.origin.y / rowHeight < 0 { return 0 } else { return Int(rect.origin.y / rowHeight) } } /** Calculates which row index would be last for a given rect. - parameter rect: The rect to check - parameter rowHeight: Height for a row - returns: Last row index */ static func lastRowInRect(rect: CGRect, withRowHeight rowHeight: CGFloat, max: Int) -> Int { guard rect.size.height >= rowHeight else { return 0 } if (rect.origin.y + rect.height) / rowHeight > CGFloat(max) { return max - 1 } else { return Int(ceil((rect.origin.y + rect.height) / rowHeight)) - 1 } } /** Calculates which index would be the first for a given row. - parameter row: Row index - parameter itemsPerRow: How many items there can be in a row - returns: First index */ static func firstIndexInRow(row: Int, withItemsPerRow itemsPerRow: Int) -> Int { return row * itemsPerRow } /** Calculates which index would be the last for a given row. - parameter row: Row index - parameter itemsPerRow: How many items there can be in a row - parameter numberOfItems: The total number of items. - returns: Last index */ static func lastIndexInRow(row: Int, withItemsPerRow itemsPerRow: Int, numberOfItems: Int) -> Int { let maxIndex = (row + 1) * itemsPerRow - 1 let bounds = numberOfItems - 1 if maxIndex > bounds { return bounds } else { return maxIndex } } /** Takes an index path (which are 2 dimensional) and turns it into a 1 dimensional index - parameter indexPath: The index path we want to flatten - returns: A flat index */ func flatIndex(indexPath: NSIndexPath) -> Int { guard let collectionView = collectionView else { return 0 } return (0.. NSIndexPath { guard let collectionView = collectionView else { return NSIndexPath(forItem: 0, inSection: 0) } var item = index var section = 0 while(item >= collectionView.numberOfItemsInSection(section)) { item -= collectionView.numberOfItemsInSection(section) section += 1 } return NSIndexPath(forItem: item, inSection: section) } /** Estimated the size of the items - returns: Estimated item size */ func estimatedItemSize() -> CGSize { guard let collectionView = collectionView else { return CGSizeZero } let itemWidth = (collectionView.bounds.width - CGFloat(itemsPerRow - 1) * itemSpacing) / CGFloat(itemsPerRow) return CGSize(width: itemWidth, height: itemWidth * itemHeightRatio) } /** Estimated total number of items - returns: Total number of items */ func estimatedNumberOfItems() -> Int { guard let collectionView = collectionView else { return 0 } return (0.. CGFloat { return _itemSize.height+itemSpacing } }