I am an android application developer and new to iOS programming and my very first challenge is to build a 2-way scrolling table in iOS. I am getting many solutions with UICollectionView inside UITableView. But in my case rows will scroll together, not independent of each other. There are more than 15 columns and 100+ rows with text data in the table.
I have achieved the same in Android by using a ListView inside a HorizontalScrollView. But yet to find any solution in iOS. Any help is greatly appreciated.
EDIT: I have added a couple of screens of the android app where the table is scrolled horizontally.
So you want this:
You should use a UICollectionView. You can't use UICollectionViewFlowLayout (the only layout that's provided in the public SDK) because it is designed to only scroll in one direction, so you need to implement a custom UICollectionViewLayout subclass that arranges the elements to scroll in both directions if needed.
For full details on building a custom UICollectionViewLayout subclass, you should watch these: videos from WWDC 2012:
Session 205: Introducing Collection Views
Session 219: Advanced Collection Views and Building Custom Layouts
Anyway, I'll just dump an example implementation of GridLayout here for you to start with. For each IndexPath, I use the section as the row number and the item as the column number.
class GridLayout: UICollectionViewLayout {
var cellHeight: CGFloat = 22
var cellWidths: [CGFloat] = [] {
didSet {
precondition(cellWidths.filter({ $0 <= 0 }).isEmpty)
invalidateCache()
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: totalWidth, height: totalHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// When bouncing, rect's origin can have a negative x or y, which is bad.
let newRect = rect.intersection(CGRect(x: 0, y: 0, width: totalWidth, height: totalHeight))
var poses = [UICollectionViewLayoutAttributes]()
let rows = rowsOverlapping(newRect)
let columns = columnsOverlapping(newRect)
for row in rows {
for column in columns {
let indexPath = IndexPath(item: column, section: row)
poses.append(pose(forCellAt: indexPath))
}
}
return poses
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return pose(forCellAt: indexPath)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return false
}
private struct CellSpan {
var minX: CGFloat
var maxX: CGFloat
}
private struct Cache {
var cellSpans: [CellSpan]
var totalWidth: CGFloat
}
private var _cache: Cache? = nil
private var cache: Cache {
if let cache = _cache { return cache }
var spans = [CellSpan]()
var x: CGFloat = 0
for width in cellWidths {
spans.append(CellSpan(minX: x, maxX: x + width))
x += width
}
let cache = Cache(cellSpans: spans, totalWidth: x)
_cache = cache
return cache
}
private var totalWidth: CGFloat { return cache.totalWidth }
private var cellSpans: [CellSpan] { return cache.cellSpans }
private var totalHeight: CGFloat {
return cellHeight * CGFloat(collectionView?.numberOfSections ?? 0)
}
private func invalidateCache() {
_cache = nil
invalidateLayout()
}
private func rowsOverlapping(_ rect: CGRect) -> Range<Int> {
let startRow = Int(floor(rect.minY / cellHeight))
let endRow = Int(ceil(rect.maxY / cellHeight))
return startRow ..< endRow
}
private func columnsOverlapping(_ rect: CGRect) -> Range<Int> {
let minX = rect.minX
let maxX = rect.maxX
if let start = cellSpans.firstIndex(where: { $0.maxX >= minX }), let end = cellSpans.lastIndex(where: { $0.minX <= maxX }) {
return start ..< end + 1
} else {
return 0 ..< 0
}
}
private func pose(forCellAt indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let pose = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let row = indexPath.section
let column = indexPath.item
pose.frame = CGRect(x: cellSpans[column].minX, y: CGFloat(row) * cellHeight, width: cellWidths[column], height: cellHeight)
return pose
}
}
To draw the separating lines, I added hairline views to each cell's background:
class GridCell: UICollectionViewCell {
static var reuseIdentifier: String { return "cell" }
override init(frame: CGRect) {
super.init(frame: frame)
label.frame = bounds.insetBy(dx: 2, dy: 2)
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.addSubview(label)
let backgroundView = UIView(frame: CGRect(origin: .zero, size: frame.size))
backgroundView.backgroundColor = .white
self.backgroundView = backgroundView
rightSeparator.backgroundColor = .gray
backgroundView.addSubview(rightSeparator)
bottomSeparator.backgroundColor = .gray
backgroundView.addSubview(bottomSeparator)
}
func setRecord(_ record: String) {
label.text = record
}
override func layoutSubviews() {
super.layoutSubviews()
let thickness = 1 / (window?.screen.scale ?? 1)
let size = bounds.size
rightSeparator.frame = CGRect(x: size.width - thickness, y: 0, width: thickness, height: size.height)
bottomSeparator.frame = CGRect(x: 0, y: size.height - thickness, width: size.width, height: thickness)
}
private let label = UILabel()
private let rightSeparator = UIView()
private let bottomSeparator = UIView()
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here's my demo view controller:
class ViewController: UIViewController {
var records: [[String]] = (0 ..< 20).map { row in
(0 ..< 6).map {
column in
"Row \(row) column \(column)"
}
}
var cellWidths: [CGFloat] = [ 180, 200, 180, 160, 200, 200 ]
override func viewDidLoad() {
super.viewDidLoad()
let layout = GridLayout()
layout.cellHeight = 44
layout.cellWidths = cellWidths
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.isDirectionalLockEnabled = true
collectionView.backgroundColor = UIColor(white: 0.95, alpha: 1)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.register(GridCell.self, forCellWithReuseIdentifier: GridCell.reuseIdentifier)
collectionView.dataSource = self
view.addSubview(collectionView)
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return records.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return records[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GridCell.reuseIdentifier, for: indexPath) as! GridCell
cell.setRecord(records[indexPath.section][indexPath.item])
return cell
}
}
Related
I have a horizontally scrolling uicollectionview. I dynamically change each cell's height based on the length of a subview uilabel text string. At the end, I set the uicollectionview height constraint to equal the longest cell height.
This result in a uicollectionview where each cell varies in height and prevents me from achieving my goal: get all cells' tops to align and be pinned to the top edge of the uicollectionview.
Is anyone able to lend a hand here? I understand I might have to create a custom layout for this, but have no clue where to start and could not find any references to instruct. Any guidance would be appreciated!
easy to do it with custom UICollectionViewLayout,
put it simple:
you just assign the frame of each item manually.
Notice the logic in func prepare()
The rest method are just common mode.
for variable item height, you can use a delegate to fetch the value.
enum CommonComponent: String {
case cell
case header
var kind: String {
switch self{
case .header:
return "UICollectionElementKindSectionHeader"
default:
return ""
}
}
}
protocol CustomLayoutProxy: AnyObject {
func getHeightFor(indexPath ip: IndexPath) -> CGFloat
}
class CustomLayout: UICollectionViewLayout {
weak var delegate: CustomLayoutProxy?
override public var collectionViewContentSize: CGSize {
CGSize(width: collectionViewWidth, height: contentHeight)
}
// MARK: - Properties
private var contentHeight = CGFloat.zero
private var cache = [CommonComponent: [IndexPath: UICollectionViewLayoutAttributes]]()
private var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
private var collectionViewHeight: CGFloat {
collectionView!.frame.height
}
private var collectionViewWidth: CGFloat {
collectionView!.frame.width
}
let maxItemSize = CGSize(width: 40, height: 40)
}
// MARK: - LAYOUT CORE PROCESS
extension CustomLayout{
override public func prepare() {
guard let collectionView = collectionView else {
return
}
prepareCache()
var cellX: CGFloat = 16
let countOne = collectionView.numberOfItems(inSection: 0)
let headerIP = IndexPath(item: 0, section: 0)
let headH: CGFloat = 58
let headerAttributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: CommonComponent.header.kind, with: headerIP)
headerAttributes.frame = CGRect(x: 0, y: contentHeight, width: UI.std.width, height: headH)
cache[.header]?[headerIP] = headerAttributes
contentHeight += headH
for item in 0 ..< countOne{
let cellIndexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: cellIndexPath)
var size = maxItemSize
if let h = delegate?.getHeightFor(indexPath: cellIndexPath){
size.height = h
}
attributes.frame = CGRect( x: cellX, y: contentHeight, width: size.width, height: size.height )
contentHeight += exceedUp(origin: &cellX, limit: collectionViewWidth)
cache[.cell]?[cellIndexPath] = attributes
}
contentHeight += (maxItemSize.height + 16)
}
private func prepareCache() {
contentHeight = 0
cache.removeAll(keepingCapacity: true)
cache[.cell] = [IndexPath: UICollectionViewLayoutAttributes]()
cache[.header] = [IndexPath: UICollectionViewLayoutAttributes]()
}
}
//MARK: - PROVIDING ATTRIBUTES TO THE COLLECTIONVIEW
extension CustomLayout{
public override func layoutAttributesForSupplementaryView(ofKind ComponentKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
switch ComponentKind {
case CommonComponent.header.kind:
return cache[.header]?[indexPath]
default:
return nil
}
}
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[.cell]?[indexPath]
}
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let _ = collectionView else { return nil }
visibleLayoutAttributes.removeAll(keepingCapacity: true)
for (_, infos) in cache {
for (_, attributes) in infos {
attributes.transform = .identity
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
}
return visibleLayoutAttributes
}
func exceedUp(origin x: inout CGFloat, limit widthX: CGFloat) -> CGFloat{
var exceedH: CGFloat = 0
let lineSpacing: CGFloat = 20
let interitemSpacing: CGFloat = 16
let contentEdge = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
if x + maxItemSize.width * 2 + interitemSpacing + contentEdge.right < widthX + 1{
x += maxItemSize.width + interitemSpacing
}
else{
x = contentEdge.left
exceedH = (maxItemSize.height + lineSpacing)
}
return exceedH
}
}
For more example , you can check my github repo
BoxDengJZ/UICollectionViewLeftAlignedLayout
coyingcat/CardCollection
I want exactly when tap particular cell it will show expand
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = -UIScreen.main.bounds.width/2.08
i want expand cell?
Friends any suggestion and needs helpful for us
Thanks
Usually we use UICollectionViewFlowLayout, once the item's size and spacing is given, the item's frame is settled.
To do
CollectionView stacked Cell for Overlapping Cell Like Wallet
You need to give the frame you want for every item in UICollectionView.
Finishing it by customizing UICollectionViewLayout, and use your own UICollectionViewLayout subclass.
The final effect is as the following image
The basic thing:
To do custom layout is easy:
override public func prepare() ,
to calculate the frames for every item,
And put the frame for item in its container , your custom UICollectionViewLayoutAttributes
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? ,
Retrieves layout information for an item at the specified index path with a corresponding cell.
to assign your prepared custom UICollectionViewLayoutAttributes for item layout
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? ,
Retrieves the layout attributes for all of the cells and views in the specified rectangle.
There is no supplementary views, and decoration views in this situation, just to handle items.
to assign your prepared custom UICollectionViewLayoutAttributes for UICollectionView layout
Key Point:
You have two kinds of layout state.
the initial state, and the unselected state is the same, when no card is selected.
The front card are partly hidden, and the last card is full shown.
The custom UICollectionViewLayoutAttributes, with isExpand to record if there is cell selected.
isExpand is used for an cell added UIPanGestureRecognizer
class CardLayoutAttributes: UICollectionViewLayoutAttributes {
var isExpand = false
override func copy(with zone: NSZone? = nil) -> Any {
let attribute = super.copy(with: zone) as! CardLayoutAttributes
attribute.isExpand = isExpand
return attribute
}
}
The frame calculation of state unselected is easy.
Set the frame of the first item,
then the second ...
y: titleHeight * CGFloat(index) , all OK
fileprivate func setNoSelect(attribute:CardLayoutAttributes) {
guard let collection = collectionView else {
return
}
let noneIdx = Int(collection.contentOffset.y/titleHeight)
if noneIdx < 0 {
return
}
attribute.isExpand = false
let index = attribute.zIndex
var currentFrame = CGRect(x: collection.frame.origin.x, y: titleHeight * CGFloat(index), width: cellSize.width, height: cellSize.height)
if index == noneIdx{
attribute.frame = CGRect(x: currentFrame.origin.x, y: collection.contentOffset.y, width: cellSize.width, height: cellSize.height)
}
else if index <= noneIdx, currentFrame.maxY > collection.contentOffset.y{
currentFrame.origin.y -= (currentFrame.maxY - collection.contentOffset.y )
attribute.frame = currentFrame
}
else {
attribute.frame = currentFrame
}
}
the selected state, to select an item, the item is expanded , others will make room for it.
The logic is to put the selected item in the middle, the y position matters, collection.contentOffset.y + offsetSelected,
The center item's frame is known , then calculate the two sides.
One side is items from (selectedIdx-1) to 0, calculate the frame of item.
The Other side is items from (selectedIdx+1) to the final index , also calculate item's frame.
fileprivate func calculate(for attributes: [CardLayoutAttributes], choose selectedIP: IndexPath) -> [CGRect]{
guard let collection = collectionView else {
return []
}
let noneIdx = Int(collection.contentOffset.y / titleHeight)
if noneIdx < 0 {
return []
}
let x = collection.frame.origin.x
var selectedIdx = 0
for attr in attributes{
if attr.indexPath == selectedIP{
break
}
selectedIdx += 1
}
var frames = [CGRect](repeating: .zero, count: attributes.count)
// Edit here
let offsetSelected: CGFloat = 100
let marginBottomSelected: CGFloat = 10
frames[selectedIdx] = CGRect(x: x, y: collection.contentOffset.y + offsetSelected, width: cellSize.width, height: cellSize.height)
if selectedIdx > 0{
for i in 0...(selectedIdx-1){
frames[selectedIdx - i - 1] = CGRect(x: x, y: frames[selectedIdx].origin.y - titleHeight * CGFloat(i + 1), width: cellSize.width, height: cellSize.height)
}
}
if selectedIdx < (attributes.count - 1){
for i in (selectedIdx + 1)...(attributes.count - 1){
frames[i] = CGRect(x: x, y: frames[selectedIdx].origin.y + marginBottomSelected + titleHeight * CGFloat(i - selectedIdx - 1) + cellSize.height, width: cellSize.width, height: cellSize.height)
}
}
return frames
}
When there is a item selected, you should refresh the custom layout.
To call invalidateLayout(),
Invalidates the current layout and triggers a layout update.
fileprivate var _selectPath: IndexPath? {
didSet {
self.collectionView!.isScrollEnabled = (_selectPath == nil)
}
}
public var selectPath: IndexPath? {
set {
_selectPath = (_selectPath == newValue) ? nil : newValue
self.collectionView?.performBatchUpdates({
self.invalidateLayout()
}, completion: nil)
} get {
return _selectPath
}
}
one more thing, the sample demo in github
Here is the overlapping the layout, minimumLineSpacing negative number
if click the card it should be expanded and scroll down and up card
should not disclose anymoew like wallet
class OverlappedCustomFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
// This allows us to make intersection and overlapping
// A negative number implies overlapping whereas positive implies space between the adjacent edges of two cells.
minimumLineSpacing = -100
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
for currentLayoutAttributes: UICollectionViewLayoutAttributes in layoutAttributes! {
// zIndex - Specifies the item’s position on the z-axis.
// Unlike a layer's zPosition, changing zIndex allows us to change not only layer position,
// but tapping/UI interaction logic too as it moves the whole item.
currentLayoutAttributes.zIndex = currentLayoutAttributes.indexPath.row + 1
}
return layoutAttributes
}
}
you can try the following idea
The layout part:
var selectedIdx: Int?
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
var originY: CGFloat = 0
let defaultOffsetY: CGFloat = 80
if let idx = selectedIdx{
// expanded layout
for i in 0..<cnt{
// frame the attribute
if i == idx + 1{
// to expand is to make room for the rest items
originY += 400
}
else{
originY += defaultOffsetY
}
}
}
else{
// default layout
for i in 0..<cnt{
// frame the attribute
originY += defaultOffsetY
}
}
return layoutAttributes
}
trigger the expand action
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// edit the state
layout.selectedIdx = indexPath.item
// trigger refreshing the collectionView's layout
collectionView.reloadData()
}
I have tried it's working fine, thanks to everyone
class CardLayout: UICollectionViewLayout {
var contentHeight: CGFloat = 0.0
var cachedAttributes = [UICollectionViewLayoutAttributes]()
var nextIndexPath: Int?
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var collectionViewContentSize: CGSize {
var size = super.collectionViewContentSize
let collection = collectionView!
size.width = collection.bounds.size.width
if let _ = FlowLayoutAttributes.shared.cellIndex{
size.height = contentHeight+UIScreen.main.bounds.width/2+38
}else{
size.height = contentHeight
}
print("Contend",contentHeight)
return size
}
func reloadData(){
self.cachedAttributes = [UICollectionViewLayoutAttributes]()
}
override func prepare() {
cachedAttributes.removeAll()
guard let numberOfItems = collectionView?.numberOfItems(inSection: 0) else {
return
}
for index in 0..<numberOfItems {
let layout = UICollectionViewLayoutAttributes(forCellWith: IndexPath(row: index, section: 0))
layout.frame = frameFor(index: index)
if let indexExpand = FlowLayoutAttributes.shared.cellIndex, indexExpand == index {
self.nextIndexPath = index+1
contentHeight = CGFloat(CGFloat(numberOfItems)*getCardSize())+UIScreen.main.bounds.width/2+38*2
}else{
contentHeight = CGFloat(CGFloat(numberOfItems)*getCardSize())+UIScreen.main.bounds.width/2+38
}
layout.zIndex = index
layout.isHidden = false
cachedAttributes.append(layout)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cachedAttributes {
if attributes.frame.intersects(rect) {
layoutAttributes.append(cachedAttributes[attributes.indexPath.item])
}
}
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cachedAttributes[indexPath.item]
}
func frameFor(index: Int) -> CGRect {
var frame = CGRect(origin: CGPoint(x: CGFloat(8), y:0), size: CGSize(width: UIScreen.main.bounds.width - CGFloat(8 + 8), height: CGFloat(UIScreen.main.bounds.width/2+38)))
var frameOrigin = frame.origin
if let indexExpand = FlowLayoutAttributes.shared.cellIndex{
if index > 0 {
if indexExpand < index {
let spacesHeight = CGFloat((getCardSize() * CGFloat(index)))+UIScreen.main.bounds.width/2+38-getCardSize()/2
frameOrigin.y = spacesHeight
}else{
frameOrigin.y = CGFloat((getCardSize() * CGFloat(index)))
}
}
}else{
if index > 0 {
frameOrigin.y = CGFloat((getCardSize() * CGFloat(index)))
}
}
frame.origin = frameOrigin
return frame
}
func getCardSize()-> CGFloat{
if UIDevice().userInterfaceIdiom == .phone {
switch UIScreen.main.nativeBounds.height {
case 1136:
print("iPhone 5 or 5S or 5C")
return 45.25
case 1334:
print("iPhone 6/6S/7/8")
return 45.25
case 1920, 2208:
print("iPhone 6+/6S+/7+/8+")
return 46
case 2436:
print("iPhone X/XS/11 Pro")
return 45.25
case 2688:
print("iPhone XS Max/11 Pro Max")
return 46
case 1792:
print("iPhone XR/ 11 ")
return 46
case 2532:
print("iPhone 12/ iPhone 12 Pro")
return 45.50
case 2778:
print("iPhone 12 Pro Max")
return 46.2
default:
return 46.2
}
}else{
return CGFloat(46.2)
}
}
}
I want to set shadow around a every section(group of cells) in iOS Swift.
Like this:
Easy to achieve this with UICollectionView ,
by subclass UICollectionViewFlowLayout with DecorationView
turn UITableViewCell into UICollectionViewCell ,
use decoration view to achieve the drop shadow of section part
class DecorationFlow: UICollectionViewFlowLayout {
private var cache = [IndexPath: UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
register(HistoDecorationV.self, forDecorationViewOfKind: HistoDecorationV.id)
let originX: CGFloat = 15
let widH: CGFloat = UI.std.width - originX * 2
guard let collect = collectionView else{
return
}
let sections = collect.numberOfSections
var originY: CGFloat = 0
for sect in 0..<sections{
let sectionFirst = IndexPath(item: 0, section: sect)
let attributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: HistoDecorationV.id, with: sectionFirst)
let itemCount = collect.numberOfItems(inSection: sect)
originY = originY + 80
let h: CGFloat = 50 * CGFloat(itemCount)
attributes.frame = CGRect(x: originX, y: originY, width: widH, height: h)
originY = originY + h + 15
cache[sectionFirst] = attributes
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
private func prepareCache() {
cache.removeAll(keepingCapacity: true)
cache = [IndexPath: UICollectionViewLayoutAttributes]()
}
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var array = super.layoutAttributesForElements(in: rect)
guard let collection = collectionView, collection.numberOfSections > 0 else{
return array
}
var z = 0
array?.forEach({
$0.zIndex = z + 100
z += 1
})
z = 0
for (_, attributes) in cache {
if attributes.frame.intersects(rect){
attributes.zIndex = z + 10
array?.append(attributes)
}
z += 1
}
return array
}
}
more code in github
The designer wants the following animation from a swipe gesture.
As it can be seen the user can swipe cards and see what each card has. At the same time, the user can see in the right side of the screen the following card and the last one in the left. Also, cards are changing their size while the user is moving the scroll.
I have already worked with page control views but I have no idea if this is possible with a page Control (which actually is the question of this post).
Also, I have already tried with a collectionView but when I swipe (actually is an horizontal scroll) the scroll has an uncomfortable inertia and also, I have no idea how to make the animation.
In this question a scrolled page control is implemented but now I just wondering if and animation like the gif provided is possible.
If the answer is yes, I would really appreciate if you can give tips of how I can make this possible.
Thanks in advance.
Based on the Denislava Shentova comment I found a good library that solves this issue.
For all people in the future and their work hours, I just took code from UPCarouselFlowLayout library and deleted some I didn't need.
Here is the code of a simple viewController that shows the following result:
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
// CollectionView variable:
var collectionView : UICollectionView?
// Variables asociated to collection view:
fileprivate var currentPage: Int = 0
fileprivate var pageSize: CGSize {
let layout = self.collectionView?.collectionViewLayout as! UPCarouselFlowLayout
var pageSize = layout.itemSize
pageSize.width += layout.minimumLineSpacing
return pageSize
}
fileprivate var colors: [UIColor] = [UIColor.black, UIColor.red, UIColor.green, UIColor.yellow]
override func viewDidLoad() {
super.viewDidLoad()
self.addCollectionView()
self.setupLayout()
}
func setupLayout(){
// This is just an utility custom class to calculate screen points
// to the screen based in a reference view. You can ignore this and write the points manually where is required.
let pointEstimator = RelativeLayoutUtilityClass(referenceFrameSize: self.view.frame.size)
self.collectionView?.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.collectionView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: pointEstimator.relativeHeight(multiplier: 0.1754)).isActive = true
self.collectionView?.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
self.collectionView?.heightAnchor.constraint(equalToConstant: pointEstimator.relativeHeight(multiplier: 0.6887)).isActive = true
self.currentPage = 0
}
func addCollectionView(){
// This is just an utility custom class to calculate screen points
// to the screen based in a reference view. You can ignore this and write the points manually where is required.
let pointEstimator = RelativeLayoutUtilityClass(referenceFrameSize: self.view.frame.size)
// This is where the magic is done. With the flow layout the views are set to make costum movements. See https://github.com/ink-spot/UPCarouselFlowLayout for more info
let layout = UPCarouselFlowLayout()
// This is used for setting the cell size (size of each view in this case)
// Here I'm writting 400 points of height and the 73.33% of the height view frame in points.
layout.itemSize = CGSize(width: pointEstimator.relativeWidth(multiplier: 0.73333), height: 400)
// Setting the scroll direction
layout.scrollDirection = .horizontal
// Collection view initialization, the collectionView must be
// initialized with a layout object.
self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
// This line if for able programmatic constrains.
self.collectionView?.translatesAutoresizingMaskIntoConstraints = false
// CollectionView delegates and dataSource:
self.collectionView?.delegate = self
self.collectionView?.dataSource = self
// Registering the class for the collection view cells
self.collectionView?.register(CardCell.self, forCellWithReuseIdentifier: "cellId")
// Spacing between cells:
let spacingLayout = self.collectionView?.collectionViewLayout as! UPCarouselFlowLayout
spacingLayout.spacingMode = UPCarouselFlowLayoutSpacingMode.overlap(visibleOffset: 20)
self.collectionView?.backgroundColor = UIColor.gray
self.view.addSubview(self.collectionView!)
}
// MARK: - Card Collection Delegate & DataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! CardCell
cell.customView.backgroundColor = colors[indexPath.row]
return cell
}
// MARK: - UIScrollViewDelegate
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)
}
}
class CardCell: UICollectionViewCell {
let customView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 12
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.customView)
self.customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
self.customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
self.customView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true
self.customView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
} // End of CardCell
class RelativeLayoutUtilityClass {
var heightFrame: CGFloat?
var widthFrame: CGFloat?
init(referenceFrameSize: CGSize){
heightFrame = referenceFrameSize.height
widthFrame = referenceFrameSize.width
}
func relativeHeight(multiplier: CGFloat) -> CGFloat{
return multiplier * self.heightFrame!
}
func relativeWidth(multiplier: CGFloat) -> CGFloat{
return multiplier * self.widthFrame!
}
}
Note that there are some other clases in this code but temporarily you can run the whole code in the ViewController.swift file. After you test, please split them into different files.
In order tu run this code, you need the following module. Make a file called UPCarouselFlowLayout.swift and paste all this code:
import UIKit
public enum UPCarouselFlowLayoutSpacingMode {
case fixed(spacing: CGFloat)
case overlap(visibleOffset: CGFloat)
}
open class UPCarouselFlowLayout: UICollectionViewFlowLayout {
fileprivate struct LayoutState {
var size: CGSize
var direction: UICollectionViewScrollDirection
func isEqual(_ otherState: LayoutState) -> Bool {
return self.size.equalTo(otherState.size) && self.direction == otherState.direction
}
}
#IBInspectable open var sideItemScale: CGFloat = 0.6
#IBInspectable open var sideItemAlpha: CGFloat = 0.6
open var spacingMode = UPCarouselFlowLayoutSpacingMode.fixed(spacing: 40)
fileprivate var state = LayoutState(size: CGSize.zero, direction: .horizontal)
override open func prepare() {
super.prepare()
let currentState = LayoutState(size: self.collectionView!.bounds.size, direction: self.scrollDirection)
if !self.state.isEqual(currentState) {
self.setupCollectionView()
self.updateLayout()
self.state = currentState
}
}
fileprivate func setupCollectionView() {
guard let collectionView = self.collectionView else { return }
if collectionView.decelerationRate != UIScrollViewDecelerationRateFast {
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
}
fileprivate func updateLayout() {
guard let collectionView = self.collectionView else { return }
let collectionSize = collectionView.bounds.size
let isHorizontal = (self.scrollDirection == .horizontal)
let yInset = (collectionSize.height - self.itemSize.height) / 2
let xInset = (collectionSize.width - self.itemSize.width) / 2
self.sectionInset = UIEdgeInsetsMake(yInset, xInset, yInset, xInset)
let side = isHorizontal ? self.itemSize.width : self.itemSize.height
let scaledItemOffset = (side - side*self.sideItemScale) / 2
switch self.spacingMode {
case .fixed(let spacing):
self.minimumLineSpacing = spacing - scaledItemOffset
case .overlap(let visibleOffset):
let fullSizeSideItemOverlap = visibleOffset + scaledItemOffset
let inset = isHorizontal ? xInset : yInset
self.minimumLineSpacing = inset - fullSizeSideItemOverlap
}
}
override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect),
let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
else { return nil }
return attributes.map({ self.transformLayoutAttributes($0) })
}
fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else { return attributes }
let isHorizontal = (self.scrollDirection == .horizontal)
let collectionCenter = isHorizontal ? collectionView.frame.size.width/2 : collectionView.frame.size.height/2
let offset = isHorizontal ? collectionView.contentOffset.x : collectionView.contentOffset.y
let normalizedCenter = (isHorizontal ? attributes.center.x : attributes.center.y) - offset
let maxDistance = (isHorizontal ? self.itemSize.width : self.itemSize.height) + self.minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance)/maxDistance
let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
attributes.zIndex = Int(alpha * 10)
return attributes
}
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView , !collectionView.isPagingEnabled,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let isHorizontal = (self.scrollDirection == .horizontal)
let midSide = (isHorizontal ? collectionView.bounds.size.width : collectionView.bounds.size.height) / 2
let proposedContentOffsetCenterOrigin = (isHorizontal ? proposedContentOffset.x : proposedContentOffset.y) + midSide
var targetContentOffset: CGPoint
if isHorizontal {
let closest = layoutAttributes.sorted { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
}
else {
let closest = layoutAttributes.sorted { abs($0.center.y - proposedContentOffsetCenterOrigin) < abs($1.center.y - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: proposedContentOffset.x, y: floor(closest.center.y - midSide))
}
return targetContentOffset
}
}
Again, this module was made by Paul Ulric, you can installed with cocoa.
I want to reorder cells in a Collection View with custom size for every cell.
In every cell of the Collection View there is a label with a word.
I set the dimension of every cell with this code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let word = textArray[indexPath.row]
let font = UIFont.systemFont(ofSize: 17)
let fontAttributes = [NSFontAttributeName: font]
var size = (word as NSString).size(attributes: fontAttributes)
size.width = size.width + 2
return size
}
I reorder the Collection View with this code:
override func viewDidLoad() {
super.viewDidLoad()
self.installsStandardGestureForInteractiveMovement = false
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
self.collectionView?.addGestureRecognizer(panGesture)
}
func handlePanGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case UIGestureRecognizerState.began :
guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
break
}
collectionView?.beginInteractiveMovementForItem(at: selectedIndexPath)
print("Interactive movement began")
case UIGestureRecognizerState.changed :
collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
print("Interactive movement changed")
case UIGestureRecognizerState.ended :
collectionView?.endInteractiveMovement()
print("Interactive movement ended")
default:
collectionView?.cancelInteractiveMovement()
print("Interactive movement canceled")
}
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// Swap values if sorce and destination
let change = textArray[sourceIndexPath.row]
textArray.remove(at: sourceIndexPath.row)
textArray.insert(change, at: destinationIndexPath.row)
// Reload data to recalculate dimensions for the cells
collectionView.reloadData()
}
The view looks like this:
The problem is that during the reordering, the cells maintain the dimensions of the original cell at a indexPath, so during the the reordering, the view looks like this:
At the moment I've fixed the problem reloading data at the end of the reordering, to recalculate the right dimensions.
How can I mantain the right dimensions for the cells also during the interactive movement and reorder custom size cells?
This has been bugging me all week so I sat down this evening to try and find a solution. I think what you need is a custom layout manager for your collection view, which can dynamically adjust the layout for each cell as the order is changed.
The following code obviously produces something a lot cruder than your layout above, but fundamentally achieves the behaviour you want: crucially moving to the new layout when the cells are reordered occurs "instantaneously" without any interim adjustments required.
The key to it all is the didSet function in the sourceData variable of the view controller. When this array's value is changed (via pressing the sort button - my crude approximation to your gesture recogniser), this automatically triggers a recalculation of the required cell dimensions which then also triggers the layout to clear itself down and recalculate and the collection view to reload the data.
If you have any questions on any of this, let me know. Hope it helps!
UPDATE: OK, I understand what you are trying to do now, and I think the attached updated code gets you there. Instead of using the in-built interaction methods, I think it is easier given the way I have implemented a custom layout manager to use delegation: when the pan gesture recognizer selects a cell, we create a subview based on that word which moves with the gesture. At the same time in the background we remove the word from the data source and refresh the layout. When the user selects a location to place the word, we reverse that process, telling the delegate to insert a word into the data source and refresh the layout. If the user drags the word outside the collection view or to a non-valid location, the word is simply put back where it began (use the cunning technique of storing the original index as the label's tag).
Hope that helps you out!
[Text courtesy of Wikipedia]
import UIKit
class ViewController: UIViewController, bespokeCollectionViewControllerDelegate {
let sourceText : String = "So Midas, king of Lydia, swelled at first with pride when he found he could transform everything he touched to gold; but when he beheld his food grow rigid and his drink harden into golden ice then he understood that this gift was a bane and in his loathing for gold, cursed his prayer"
var sourceData : [String]! {
didSet {
refresh()
}
}
var sortedCVController : UICollectionViewController!
var sortedLayout : bespokeCollectionViewLayout!
var sortButton : UIButton!
var sortDirection : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
sortedLayout = bespokeCollectionViewLayout(contentWidth: view.frame.width - 200)
sourceData = {
let components = sourceText.components(separatedBy: " ")
return components
}()
sortedCVController = bespokeCollectionViewController(sourceData: sourceData, collectionViewLayout: sortedLayout, frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: view.frame.width - 200, height: view.frame.height - 200)))
(sortedCVController as! bespokeCollectionViewController).delegate = self
sortedCVController.collectionView!.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: view.frame.width - 200, height: view.frame.height - 200))
sortButton = {
let sB : UIButton = UIButton(frame: CGRect(origin: CGPoint(x: 25, y: 100), size: CGSize(width: 50, height: 50)))
sB.setTitle("Sort", for: .normal)
sB.setTitleColor(UIColor.black, for: .normal)
sB.addTarget(self, action: #selector(sort), for: .touchUpInside)
sB.layer.borderColor = UIColor.black.cgColor
sB.layer.borderWidth = 1.0
return sB
}()
view.addSubview(sortedCVController.collectionView!)
view.addSubview(sortButton)
}
func refresh() -> Void {
let dimensions : [CGSize] = {
var d : [CGSize] = [CGSize]()
let font = UIFont.systemFont(ofSize: 17)
let fontAttributes = [NSFontAttributeName : font]
for item in sourceData {
let stringSize = ((item + " ") as NSString).size(attributes: fontAttributes)
d.append(CGSize(width: stringSize.width, height: stringSize.height))
}
return d
}()
if self.sortedLayout != nil {
sortedLayout.dimensions = dimensions
if let _ = sortedCVController {
(sortedCVController as! bespokeCollectionViewController).sourceData = sourceData
}
self.sortedLayout.cache.removeAll()
self.sortedLayout.prepare()
if let _ = self.sortedCVController {
self.sortedCVController.collectionView?.reloadData()
}
}
}
func sort() -> Void {
sourceData = sortDirection > 0 ? sourceData.sorted(by: { $0 > $1 }) : sourceData.sorted(by: { $0 < $1 })
sortDirection = sortDirection + 1 > 1 ? 0 : 1
}
func didMoveWord(atIndex: Int) {
sourceData.remove(at: atIndex)
}
func didPlaceWord(word: String, atIndex: Int) {
print(atIndex)
if atIndex >= sourceData.count {
sourceData.append(word)
}
else
{
sourceData.insert(word, at: atIndex)
}
}
func pleaseRefresh() {
refresh()
}
}
protocol bespokeCollectionViewControllerDelegate {
func didMoveWord(atIndex: Int) -> Void
func didPlaceWord(word: String, atIndex: Int) -> Void
func pleaseRefresh() -> Void
}
class bespokeCollectionViewController : UICollectionViewController {
var sourceData : [String]
var movingLabel : UILabel!
var initialOffset : CGPoint!
var delegate : bespokeCollectionViewControllerDelegate!
init(sourceData: [String], collectionViewLayout: bespokeCollectionViewLayout, frame: CGRect) {
self.sourceData = sourceData
super.init(collectionViewLayout: collectionViewLayout)
self.collectionView = UICollectionView(frame: frame, collectionViewLayout: collectionViewLayout)
self.collectionView?.backgroundColor = UIColor.white
self.collectionView?.layer.borderColor = UIColor.black.cgColor
self.collectionView?.layer.borderWidth = 1.0
self.installsStandardGestureForInteractiveMovement = false
let pangesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
self.collectionView?.addGestureRecognizer(pangesture)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func handlePanGesture(gesture: UIPanGestureRecognizer) {
guard let _ = delegate else { return }
switch gesture.state {
case UIGestureRecognizerState.began:
guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else { break }
guard let selectedCell : UICollectionViewCell = self.collectionView?.cellForItem(at: selectedIndexPath) else { break }
initialOffset = gesture.location(in: selectedCell)
let index : Int = {
var i : Int = 0
for sectionCount in 0..<selectedIndexPath.section {
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
}
i += selectedIndexPath.row
return i
}()
movingLabel = {
let mL : UILabel = UILabel()
mL.font = UIFont.systemFont(ofSize: 17)
mL.frame = selectedCell.frame
mL.textColor = UIColor.black
mL.text = sourceData[index]
mL.layer.borderColor = UIColor.black.cgColor
mL.layer.borderWidth = 1.0
mL.backgroundColor = UIColor.white
mL.tag = index
return mL
}()
self.collectionView?.addSubview(movingLabel)
delegate.didMoveWord(atIndex: index)
case UIGestureRecognizerState.changed:
if let _ = movingLabel {
movingLabel.frame.origin = CGPoint(x: gesture.location(in: self.collectionView).x - initialOffset.x, y: gesture.location(in: self.collectionView).y - initialOffset.y)
}
case UIGestureRecognizerState.ended:
print("Interactive movement ended")
if let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) {
guard let _ = movingLabel else { return }
let index : Int = {
var i : Int = 0
for sectionCount in 0..<selectedIndexPath.section {
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
}
i += selectedIndexPath.row
return i
}()
delegate.didPlaceWord(word: movingLabel.text!, atIndex: index)
UIView.animate(withDuration: 0.25, animations: {
self.movingLabel.alpha = 0
self.movingLabel.removeFromSuperview()
}, completion: { _ in
self.movingLabel = nil })
}
else
{
if let _ = movingLabel {
delegate.didPlaceWord(word: movingLabel.text!, atIndex: movingLabel.tag)
UIView.animate(withDuration: 0.25, animations: {
self.movingLabel.alpha = 0
self.movingLabel.removeFromSuperview()
}, completion: { _ in
self.movingLabel = nil })
}
}
default:
collectionView?.cancelInteractiveMovement()
print("Interactive movement canceled")
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
guard !(self.collectionViewLayout as! bespokeCollectionViewLayout).cache.isEmpty else { return 0 }
return (self.collectionViewLayout as! bespokeCollectionViewLayout).cache.last!.indexPath.section + 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard !(self.collectionViewLayout as! bespokeCollectionViewLayout).cache.isEmpty else { return 0 }
var n : Int = 0
for element in (self.collectionViewLayout as! bespokeCollectionViewLayout).cache {
if element.indexPath.section == section {
if element.indexPath.row > n {
n = element.indexPath.row
}
}
}
print("Section \(section) has \(n) elements")
return n + 1
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let change = sourceData[sourceIndexPath.row]
sourceData.remove(at: sourceIndexPath.row)
sourceData.insert(change, at: destinationIndexPath.row)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
// Clean
for subview in cell.subviews {
subview.removeFromSuperview()
}
let label : UILabel = {
let l : UILabel = UILabel()
l.font = UIFont.systemFont(ofSize: 17)
l.frame = CGRect(origin: CGPoint.zero, size: cell.frame.size)
l.textColor = UIColor.black
let index : Int = {
var i : Int = 0
for sectionCount in 0..<indexPath.section {
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
}
i += indexPath.row
return i
}()
l.text = sourceData[index]
return l
}()
cell.addSubview(label)
return cell
}
}
class bespokeCollectionViewLayout : UICollectionViewLayout {
var cache : [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
let contentWidth: CGFloat
var dimensions : [CGSize]!
init(contentWidth: CGFloat) {
self.contentWidth = contentWidth
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() -> Void {
guard self.dimensions != nil else { return }
if cache.isEmpty {
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var rowCount = 0
var wordCount : Int = 0
while wordCount < dimensions.count {
let nextRowCount : Int = {
var totalWidth : CGFloat = 0
var numberOfWordsInRow : Int = 0
while totalWidth < contentWidth && wordCount < dimensions.count {
if totalWidth + dimensions[wordCount].width >= contentWidth {
break
}
else
{
totalWidth += dimensions[wordCount].width
wordCount += 1
numberOfWordsInRow += 1
}
}
return numberOfWordsInRow
}()
var columnCount : Int = 0
for count in (wordCount - nextRowCount)..<wordCount {
let index : IndexPath = IndexPath(row: columnCount, section: rowCount)
let newAttribute : UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes(forCellWith: index)
let cellFrame : CGRect = CGRect(origin: CGPoint(x: xOffset, y: yOffset), size: dimensions[count])
newAttribute.frame = cellFrame
cache.append(newAttribute)
xOffset += dimensions[count].width
columnCount += 1
}
xOffset = 0
yOffset += dimensions[0].height
rowCount += 1
}
}
}
override var collectionViewContentSize: CGSize {
guard !cache.isEmpty else { return CGSize(width: 100, height: 100) }
return CGSize(width: self.contentWidth, height: cache.last!.frame.maxY)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
if cache.isEmpty {
self.prepare()
}
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}