I am trying to create a tag flow layout. To do this I followed a very nice tutorial https://codentrick.com/create-a-tag-flow-layout-with-uicollectionview/
I managed to create and customize the flow layout, but the cells are not aligned properly some times. Here is the screenshot from iphone 4S. I have two header sections also
As you can see in section 1, the tags are not placed correctly. I am not understanding what is causing this issue.
Here is my custom flow layout class
import UIKit
class FlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()
// unwrap super's attributes
guard let attributesForElementsInRect = super.layoutAttributesForElementsInRect(rect) else { return nil }
// modify attributes
var leftMargin: CGFloat = 8.0;
for attributes in attributesForElementsInRect {
let itemAttributesCopy = attributes.copy() as! UICollectionViewLayoutAttributes
print("attCopy",itemAttributesCopy.frame.size.width)
print("left",leftMargin)
print("collWidth",self.collectionView?.frame.size.width)
print("sectionInset",self.sectionInset.left)
if( itemAttributesCopy.frame.size.width + leftMargin > self.collectionView?.frame.size.width)
{
leftMargin = 8.0
}
if (itemAttributesCopy.frame.origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left
} else {
print("itemAttributeCopy",itemAttributesCopy.frame)
var newLeftAlignedFrame = itemAttributesCopy.frame
newLeftAlignedFrame.origin.x = leftMargin
itemAttributesCopy.frame = newLeftAlignedFrame
print("newFrame",newLeftAlignedFrame)
}
leftMargin += itemAttributesCopy.frame.size.width + 8
print("finalleftMargin",leftMargin)
newAttributesForElementsInRect.append(itemAttributesCopy)
}
return newAttributesForElementsInRect
}
}
So i need help in figuring out this issue.
Regards
Ranjit
Related
I want my cells to be displayed like this
X
Group 1
X X X
X X
Group 2
X X X
X X X
But, this is the output I'm getting. I'm not expecting the cell named Home to be center aligned and also not expecting the space in middle for 2nd row of Employee communication.
private class MenuDelegate : UICollectionViewDelegateFlowLayout
{
private const int interItemSpacing = 12;
private readonly double _itemWidth;
private readonly double _itemsPerRow;
private readonly List<MenuItem> _items;
public MenuDelegate(List<MenuItem> items)
{
_items = items;
_itemsPerRow = DisplayUtils.IsIPad ? 5 : 3;
_itemWidth = (PlatformConstants.MenuWidth - ((_itemsPerRow - 1) * interItemSpacing)) / _itemsPerRow;
}
public override CGSize GetSizeForItem(UICollectionView collectionView,
UICollectionViewLayout layout, NSIndexPath indexPath)
{
if (_items[indexPath.Row].Parent) {
return new CGSize(DisplayUtils.ScreenWidth, 44);
}
return new CGSize(_itemWidth, _itemWidth);
}
public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath)
{
HandleCellSelect(_items[indexPath.Row]);
}
public override nfloat GetMinimumInteritemSpacingForSection(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
return interItemSpacing;
}
public override nfloat GetMinimumLineSpacingForSection(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
return float.Epsilon;
}
}
Have you tried overriding LayoutAttributesForElementsInRect in custom UICollectionViewFlowLayout and setting the attributes. This happens as the UICollectionViewFlowLayout returns 2 attributes for a single cell.
public class CustomFlowLayout : UICollectionViewFlowLayout
{
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
UICollectionViewLayoutAttributes[] attributes = base.LayoutAttributesForElementsInRect(rect);
UICollectionViewLayoutAttributes[] newAttributes = new UICollectionViewLayoutAttributes[attributes.Length];
foreach (UICollectionViewLayoutAttributes attribute in attributes)
{
if ((attribute.Frame.X + attribute.Frame.Width<= this.CollectionViewContentSize.Width) &&
(attribute.Frame.Y + attribute.Frame.Height <= this.CollectionViewContentSize.Height))
{
newAttributes.Append(attribute);
}
}
return newAttributes;
}
}
For more details you can check here
I had a subclass UICollectionViewFlowLayout to achieve the desired result.
private class CustomFlowLayout : UICollectionViewFlowLayout
{
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
var attributes = base.LayoutAttributesForElementsInRect(rect);
var leftMargin = SectionInset.Left;
var maxY = 2.0f;
var horizontalSpacing = 6f; // spacing between the items.
foreach (var attribute in attributes) {
if (attribute.Frame.Y >= maxY || attribute.Frame.X == SectionInset.Left) {
leftMargin = SectionInset.Left;
}
if (attribute.Frame.X == SectionInset.Left)
leftMargin = SectionInset.Left;
else
attribute.Frame = new CGRect(leftMargin, attribute.Frame.Y, attribute.Frame.Width, attribute.Frame.Height);
leftMargin += attribute.Frame.Width + horizontalSpacing;
maxY = (float)Math.Max(attribute.Frame.GetMaxY(), maxY);
}
return attributes;
}
}
I'm trying to implement a smoother way for a use to add new collectionViewCells for myCollectionView (which only shows one cell at a time). I want it to be something like when the user swipes left and if the user is on the last cell myCollectionView inserts a new cell when the user is swiping so that the user swipes left "into" the cell. And also I only allow the user to scroll one cell at a time.
EDIT:
So I think it is a bit hard to describe it in words so here is a gif to show what I mean
So in the past few weeks I have been trying to implement this in a number of different ways, and the one that I have found the most success with is by using the scrollViewWillEndDragging delegate method and I have implemented it like this:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Getting the size of the cells
let flowLayout = myCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let cellWidth = flowLayout.itemSize.width
let cellPadding = 10.0 as! CGFloat
// Calculating which page "card" we should be on
let currentOffset = scrollView.contentOffset.x - cellWidth/2
print("currentOffset is \(currentOffset)")
let cardWidth = cellWidth + cellPadding
var page = Int(round((currentOffset)/(cardWidth) + 1))
print("current page number is: \(page)")
if (velocity.x < 0) {
page -= 1
}
if (velocity.x > 0) {
page += 1
}
print("Updated page number is: \(page)")
print("Previous page number is: \(self.previousPage)")
// Only allowing the user to scroll for one page!
if(page > self.previousPage) {
page = self.previousPage + 1
self.previousPage = page
}
else if (page == self.previousPage) {
page = self.previousPage
}
else {
page = self.previousPage - 1
self.previousPage = page
}
print("new page number is: " + String(page))
print("addedCards.count + 1 is: " + String(addedCards.count + 1))
if (page == addedCards.count) {
print("reloading data")
// Update data source
addedCards.append("card")
// Method 1
cardCollectionView.reloadData()
// Method 2
// let newIndexPath = NSIndexPath(forItem: addedCards.count - 1, inSection: 0)
// cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
}
// print("Centering on new cell")
// Center the cardCollectionView on the new page
let newOffset = CGFloat(page * Int((cellWidth + cellPadding)))
print("newOffset is: \(newOffset)")
targetContentOffset.memory.x = newOffset
}
Although I think I have nearly got the desired result there are still some concerns and also bugs that I have found.
My main concern is the fact that instead of inserting a single cell at the end of myCollectionView I'm reloading the whole table. The reason that I do this is because if I didn't then myCollectionView.contentOffset wouldn't be changed and as a result when a new cell is created, myCollectionView isn't centered on the newly created cell.
1. If the user scrolls very slowly and then stops, the new cell gets created but then myCollectionView gets stuck in between two cells it doesn't center in on the newly created cell.
2. When myCollectionView is in between the second last cell and the last cell as a result of 1., the next time the user swipes right, instead of creating one single cell, two cells are created.
I've also used different ways to implement this behaviour such as using scrollViewDidScroll, and various other but to no avail. Can anyone point me in the right direction as I am kind of lost.
Here is a link to download my project if you want to see the interaction:
My Example Project
These the old methods if you're interested:
To do this I have tried 2 ways,
The first being:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// page is the current cell that the user is on
// addedCards is the array that the data source works with
if (page == addedCards.count + 1) {
let placeholderFlashCardProxy = FlashCardProxy(phrase: nil, pronunciation: nil, definition: nil)
addedCards.append(placeholderFlashCardProxy)
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
cardCollectionView.reloadData()
}
}
The problem with using this method is that:
Sometimes I will get a crash as a result of: NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0.
When a new collectionCell is added it will sometimes display the input that the user has written from the previous cell (this might have sometimes to do with the dequeuing and re-use of cell, although again, I'm not sure and I would be grateful if someone answered it)
The inserting isn't smooth, I want the user to be able to swipe left at the last cell and "into" a new cell. As in if I am currently on the last cell, swiping left would put me automatically at the new cell, because right now when I swipe left a new cell is created by it doesn't center on the newly created cell
The second method that I am using is:
let swipeLeftGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipedLeftOnCell:")
swipeLeftGestureRecognizer.direction = .Left
myCollectionView.addGestureRecognizer(swipeLeftGestureRecognizer)
swipeLeftGestureRecognizer.delegate = self
Although the swipe gesture very rarely responds, but if I use a tap gesture, myCollectionView always responds which is very weird (I know again this is a question on its own)
My question is which is the better way to implement what I have described above? And if none are good what should I work with to create the desired results, I've been trying to do this for two days now and I was wondering if someone could point me in the right direction. Thanks!
I hope this helps out in some way :)
UPDATE
I updated the code to fix the issue scrolling in either direction.
The updated gist can be found here
New Updated Gist
Old Gist
First I'm gonna define some model to for a Card
class Card {
var someCardData : String?
}
Next, create a collection view cell, with a card view inside that we will apply the transform to
class CollectionViewCell : UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(cardView)
self.addSubview(cardlabel)
}
override func prepareForReuse() {
super.prepareForReuse()
cardView.alpha = 1.0
cardView.layer.transform = CATransform3DIdentity
}
override func layoutSubviews() {
super.layoutSubviews()
cardView.frame = CGRectMake(contentPadding,
contentPadding,
contentView.bounds.width - (contentPadding * 2.0),
contentView.bounds.height - (contentPadding * 2.0))
cardlabel.frame = cardView.frame
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
lazy var cardView : UIView = {
[unowned self] in
var view = UIView(frame: CGRectZero)
view.backgroundColor = UIColor.whiteColor()
return view
}()
lazy var cardlabel : UILabel = {
[unowned self] in
var label = UILabel(frame: CGRectZero)
label.backgroundColor = UIColor.whiteColor()
label.textAlignment = .Center
return label
}()
}
Next setup the view controller with a collection view. As you will see there is a CustomCollectionView class, which I will define near the end.
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var cards = [Card(), Card()]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.frame = CGRectMake(0, 0, self.view.bounds.width, tableViewHeight)
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, contentPadding)
}
lazy var collectionView : CollectionView = {
[unowned self] in
// MARK: Custom Flow Layout defined below
var layout = CustomCollectionViewFlowLayout()
layout.contentDelegate = self
var collectionView = CollectionView(frame: CGRectZero, collectionViewLayout : layout)
collectionView.clipsToBounds = true
collectionView.showsVerticalScrollIndicator = false
collectionView.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
}()
// MARK: UICollectionViewDelegate, UICollectionViewDataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cards.count
}
func collectionView(collectionView : UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAtIndexPath indexPath:NSIndexPath) -> CGSize {
return CGSizeMake(collectionView.bounds.width, tableViewHeight)
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cellIdentifier = "CollectionViewCell"
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! CollectionViewCell
cell.contentView.backgroundColor = UIColor.blueColor()
// UPDATE If the cell is not the initial index, and is equal the to animating index
// Prepare it's initial state
if flowLayout.animatingIndex == indexPath.row && indexPath.row != 0{
cell.cardView.alpha = 0.0
cell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, 0.0, 0.0, 0.0)
}
return cell
}
}
UPDATED - Now For the really tricky part. I'm gonna define the CustomCollectionViewFlowLayout. The protocol callback returns the next insert index calculated by the flow layout
protocol CollectionViewFlowLayoutDelegate : class {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath)
}
/**
* Custom FlowLayout
* Tracks the currently visible index and updates the proposed content offset
*/
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
weak var contentDelegate: CollectionViewFlowLayoutDelegate?
// Tracks the card to be animated
// TODO: - Adjusted if cards are deleted by one if cards are deleted
private var animatingIndex : Int = 0
// Tracks thje currently visible index
private var visibleIndex : Int = 0 {
didSet {
if visibleIndex > oldValue {
if visibleIndex > animatingIndex {
// Only increment the animating index forward
animatingIndex = visibleIndex
}
if visibleIndex + 1 > self.collectionView!.numberOfItemsInSection(0) - 1 {
let currentEntryIndex = NSIndexPath(forRow: visibleIndex + 1, inSection: 0)
contentDelegate?.flowLayout(self, insertIndex: currentEntryIndex)
}
} else if visibleIndex < oldValue && animatingIndex == oldValue {
// if we start panning to the left, and the animating index is the old value
// let set the animating index to the last card.
animatingIndex = oldValue + 1
}
}
}
override init() {
super.init()
self.minimumInteritemSpacing = 0.0
self.minimumLineSpacing = 0.0
self.scrollDirection = .Horizontal
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// The width offset threshold percentage from 0 - 1
let thresholdOffsetPrecentage : CGFloat = 0.5
// This is the flick velocity threshold
let velocityThreshold : CGFloat = 0.4
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5))
let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5))
let currentHorizontalOffset = collectionView!.contentOffset.x
// If you either traverse far enought in either direction,
// or flicked the scrollview over the horizontal velocity in either direction,
// adjust the visible index accordingly
if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold {
visibleIndex = max(0 , (visibleIndex - 1))
} else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold {
visibleIndex += 1
}
var _proposedContentOffset = proposedContentOffset
_proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex)
return _proposedContentOffset
}
}
And define the delegate methods in your view controller to insert a new card when the delegate tell it that it needs a new index
extension ViewController : CollectionViewFlowLayoutDelegate {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath) {
cards.append(Card())
collectionView.performBatchUpdates({
self.collectionView.insertItemsAtIndexPaths([index])
}) { (complete) in
}
}
And below is the custom Collection view that applies the animation while scrolling accordingly :)
class CollectionView : UICollectionView {
override var contentOffset: CGPoint {
didSet {
if self.tracking {
// When you are tracking the CustomCollectionViewFlowLayout does not update it's visible index until you let go
// So you should be adjusting the second to last cell on the screen
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 1, inSection: 0))
} else {
// Once the CollectionView is not tracking, the CustomCollectionViewFlowLayout calls
// targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:), and updates the visible index
// by adding 1, thus we need to continue the trasition on the second the last cell
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 2, inSection: 0))
}
}
}
/**
This method applies the transform accordingly to the cell at a specified index
- parameter atIndex: index of the cell to adjust
*/
func adjustTransitionForOffset(atIndex : NSIndexPath) {
if let lastCell = self.cellForItemAtIndexPath(atIndex) as? CollectionViewCell {
let progress = 1.0 - (lastCell.frame.minX - self.contentOffset.x) / lastCell.frame.width
lastCell.cardView.alpha = progress
lastCell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, progress, progress, 0.0)
}
}
}
I think u missed one point here in
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
the indexPath should be
NSIndexPath(forItem : addedCards.count - 1, inSection : 0) not addedCards.count
Thats why you were getting the error
NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0
I am trying to build a UICollectionViewas tag flow layout according to this topic: http://codentrick.com/create-a-tag-flow-layout-with-uicollectionview/
It worked until the button action but there is one issue. As you can see at the picture below, custom UICollectionViewCell's don't align correctly as like as at the topic. I did everything same but FlowLayout.swift
I want to make equal gaps between these cells. But they are aligning like this.
You can find FlowLayout.swift below.
Can someone tell me how can I fix it?
import Foundation
import UIKit
class FlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesForElementsInRect = super.layoutAttributesForElementsInRect(rect)
var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()
var leftMargin : CGFloat = 0.0
for attributes in attributesForElementsInRect! {
let refAttributes = attributes
// assign value if next row
if (refAttributes.frame.origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left
} else {
// set x position of attributes to current margin
var newLeftAlignedFrame = refAttributes.frame
newLeftAlignedFrame.origin.x = leftMargin
refAttributes.frame = newLeftAlignedFrame
}
// calculate new value for current Fmargin
leftMargin += refAttributes.frame.size.width + 8
newAttributesForElementsInRect.append(refAttributes)
}
return newAttributesForElementsInRect
}
}
I know this is an old thread but the problem still persists. The code here helped me, to solve my problem but I created a more swifty and robust version of it. Already tested with Swift 5.1 and iOS 13.
// A collection view flow layout in which all items get left aligned
class LeftAlignedFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let originalAttributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var leftMargin: CGFloat = 0.0
var lastY: Int = 0
return originalAttributes.map {
let changedAttribute = $0
// Check if start of a new row.
// Center Y should be equal for all items on the same row
if Int(changedAttribute.center.y.rounded()) != lastY {
leftMargin = sectionInset.left
}
changedAttribute.frame.origin.x = leftMargin
lastY = Int(changedAttribute.center.y.rounded())
leftMargin += changedAttribute.frame.width + minimumInteritemSpacing
return changedAttribute
}
}
}
Solved.
I changed the UICollectionView's layout flow to custom and set FlowLayout.swift as UICollectionView's class.
I'm currently in the process of building a social networking app that displays the user's posts in a table. Each post will be displayed in one cell of the table. Because I don't know how much the user will type for one post, I need to set the text view size according to the amount of text typed by the user. The problem is that my current implementation is not working and behaving strangely.
Here is my table code:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell : UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("HomeCell") as? UITableViewCell
let post : Post = tblData[indexPath.row]
//Get all custom cell components
let userLbl = cell?.viewWithTag(101) as UILabel
let timeLbl = cell?.viewWithTag(102) as UILabel
let postLbl = cell?.viewWithTag(103) as UITextView
let profilePic = cell?.viewWithTag(100) as UIImageView
let postPic = cell?.viewWithTag(104) as UIImageView
//Post lbl properties
let textHeight = textViewDidChange(postLbl)
postLbl.frame.size.height = CGFloat(textHeight)
//postLbl.selectable = false
//Individual height variables
let postInfoHeight = 66 as CGFloat
var postHeight = 8 + CGFloat(textHeight)
var imgHeight = 8 + postPic.frame.height as CGFloat
if post.postInformation == "" {
postLbl.removeFromSuperview()
postHeight = 0
}
if post.img == nil {
postPic.removeFromSuperview()
imgHeight = 0
}
//Change the autolayout constraints so it works properly
return postInfoHeight + postHeight + imgHeight
}
and here is the code that calculates the height of the text view:
func textViewDidChange(textView : UITextView) -> Float {
let content : NSString = textView.text
let oneLineSize = content.sizeWithAttributes(["NSFontSizeAttribute": UIFont.systemFontOfSize(14.0)])
let contentSize = CGSizeMake(textView.frame.width, oneLineSize.height * round(oneLineSize.width/textView.frame.width))
return Float(contentSize.height)
}
I can't understand why this isn't giving me the correct results. If anyone can spot an error or suggest how to solve this issue I'd really appreciate it. Thanks in advance!
Thanks to #jrisberg for the link to the other question. I decided to abandon using the text view and am now using a UILabel instead. The code in the other question is extremely old and uses a lot of deprecated methods, so I'll post some updated code that works on iOS 8 here. I deleted the textViewDidChange(textView : UITextView) method and changed my tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) method to this:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell : UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("HomeCell") as? UITableViewCell
let post : Post = tblData[indexPath.row]
//Get custom cell components
let postLbl = cell?.viewWithTag(103) as UILabel
let postPic = cell?.viewWithTag(104) as UIImageView
//Post lbl properties
let PADDING : Float = 8
let pString = post.postInformation as NSString?
let textRect = pString?.boundingRectWithSize(CGSizeMake(CGFloat(self.tableView.frame.size.width - CGFloat(PADDING * 3.0)), CGFloat(1000)), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName : UIFont.systemFontOfSize(14.0)], context: nil)
//Individual height variables
let postInfoHeight = 66 as CGFloat
var postHeight = textRect?.size.height
postHeight? += CGFloat(PADDING * 3)
var imgHeight = 8 + postPic.frame.height as CGFloat
if post.img == nil {
postPic.removeFromSuperview()
imgHeight = 0
}
//Change the autolayout constraints so it works properly
return postInfoHeight + postHeight! + imgHeight
}
This question already has answers here:
How do you determine spacing between cells in UICollectionView flowLayout
(12 answers)
Closed 5 years ago.
So, I'm trying to implement a tag list with UICollectionView. I'm following this tutorial: http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
The issue is flow layout in UICollectionView tries to space items on the same row evenly.
As a developer, I can only specify minimumInteritemSpacingForSectionAtIndex, it's really up to the UICollectionView to determine the actual item spacing.
But what I really want to achieve is like this:
Any ideas?
I've converted Milo's solution to Swift: https://github.com/Coeur/UICollectionViewLeftAlignedLayout/
It simply subclasses UICollectionViewFlowLayout.
import UIKit
/**
* Simple UICollectionViewFlowLayout that aligns the cells to the left rather than justify them
*
* Based on https://stackoverflow.com/questions/13017257/how-do-you-determine-spacing-between-cells-in-uicollectionview-flowlayout
*/
open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
}
open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
let collectionView = self.collectionView else {
// should never happen
return nil
}
let sectionInset = evaluatedSectionInsetForSection(at: indexPath.section)
guard indexPath.item != 0 else {
currentItemAttributes.leftAlignFrame(withSectionInset: sectionInset)
return currentItemAttributes
}
guard let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame else {
// should never happen
return nil
}
// if the current frame, once left aligned to the left and stretched to the full collection view
// width intersects the previous frame then they are on the same line
guard previousFrame.intersects(CGRect(x: sectionInset.left, y: currentItemAttributes.frame.origin.y, width: collectionView.frame.width - sectionInset.left - sectionInset.right, height: currentItemAttributes.frame.size.height)) else {
// make sure the first item on a line is left aligned
currentItemAttributes.leftAlignFrame(withSectionInset: sectionInset)
return currentItemAttributes
}
currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
return currentItemAttributes
}
func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
}
func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
}
}
extension UICollectionViewLayoutAttributes {
func leftAlignFrame(withSectionInset sectionInset: UIEdgeInsets) {
frame.origin.x = sectionInset.left
}
}
Apple has provided the UICollectionViewFlowLayout class for us developers, which should be enough to solve the 'typical use case' of collection views. However, I believe you're correct in your assessment that the default layout does not allow you to create this tag cloud effect. If you need something different from the normal flow layout, you'll have to write your own subclass of UICollectionViewLayout.
Apple covers this topic in their 2012 WWDC session titled, "Advanced Collection Views and Building Custom Layouts"
Some additional Apple docs: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html
At the risk of seeming biased, I also wrote a quick blog post running through the basic steps: http://bradbambara.wordpress.com/2014/05/24/getting-started-with-custom-uicollectionview-layouts/
Hope that helps.