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.
Related
Note:
I am looking for alternate solution, Since I have tried changing lots of Sprite Kit properties values, and came upto conclusion that it is not possible with those changes. So I ain't posting any code. You can try the mentioned library for more information.
Original Question:
I am using BLBubbleFilters library to create Bubbles.
This library used magnetic property to pull align items to the centre of the screen. And each time the screen opens the positioning of each node is different.
What have I tried so far?
I have tried playing around with the library and its physics attributes(e.g. Magnetic property) to achieve my goal.
The goal is:
Place 'Family' node over and above all nodes, and rest of the nodes below it.
What I thought is to that, each node will have a property for example "priority(Integer)". That is, the Priority 0 nodes will be placed above all nodes, priority 1 nodes below 0 priority nodes and so on.
Since SpriteKit deals with Physics properties, is there is any such property or way to achieve my goal?
See, how the family bubble is at the Top.
I had to do something like that and I did By creating a custom SelectInterestsCollectionViewFlowLayOut and I added the bubbles in the UICollectionViewCells.
The result was this.
Every cell is a white square and inside I have an image view with corner radius 1/2 * height.
The difference is that in my case I had the same bubble size for all my bubbles.
My code is:
enum InterestBubbleType {
case A
case B
case C
case D
case E
func getYPosition(cellSideSize: CGFloat) -> CGFloat {
let spaceFromTopForTwoCellsOnTop: CGFloat = cellSideSize / 2
switch self {
case .A:
return cellSideSize
case .B:
return cellSideSize * 2
case .C:
return spaceFromTopForTwoCellsOnTop
case .D:
return cellSideSize + spaceFromTopForTwoCellsOnTop
case .E:
return 0
}
}
}
class SelectInterestsCollectionViewFlowLayOut: UICollectionViewFlowLayout {
var cache: [UICollectionViewLayoutAttributes] = []
var layoutAttributes: [UICollectionViewLayoutAttributes] = []
var cellSideSize: CGFloat!
var xPosition: CGFloat = 0
var itemPositions: [InterestBubbleType] = [.A,.C,.D,.E,.A,.B,.C,.D,.E,.A,.B,.C,.D,.A,.B,.C,.D,.E,.A,.B]
var positionsToIncreaseXPosition = [0,2,5,7,10,12,14,16]
override func prepare() {
cache = []
super.prepare()
//Prepare constants
self.xPosition = 0
cellSideSize = self.collectionView!.height() / 3
self.configureCasheForTwentyInterests()
}
private func configureCasheForTwentyInterests() {
for i in 0...(self.itemPositions.count - 1) {
let indexPath = IndexPath(item: i, section: 0)
let xPosition = self.xPosition
let frame = CGRect(x: xPosition, y: self.itemPositions[i].getYPosition(cellSideSize: self.cellSideSize), width: self.cellSideSize, height: self.cellSideSize)
let atribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
atribute.frame = frame
cache.append(atribute)
if self.positionsToIncreaseXPosition.contains(i) {
self.xPosition += self.cellSideSize
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
self.layoutAttributes = []
for uiCollectionViewLayoutAttribute in self.cache {
self.layoutAttributes.append(uiCollectionViewLayoutAttribute)
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutAttributes[indexPath.row]
}
override var collectionViewContentSize: CGSize {
let totalWidth = cellSideSize * 9
return CGSize(width: totalWidth, height: self.collectionView!.height())
}
}
Now how it works is that in my collection view there are 5 different y positions that a bubble can be inside the collection view and they are A,B,C,D,E and I also keep in mind when you need to move 1 more x position which is 1 * collectionViewWidth size.
You can configure the code to achive your own result.
In my case I had a fixed number of bubbles and a fixed view to show them. The reason I chose to do it with collection view was because if the number change you can just add positions to change x position in positionsToIncreaseXPosition and it will work.
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
I have a UIButton that is 36x36 and I want to add padding (in essence like CSS) so that the touchable area is the recommended 44x44.
I've tried adding edge inserts through the Interface Builder and also with the code below but nothing I've tried increases the touchable area.
resetButton.contentEdgeInsets.top = 50
resetButton.contentEdgeInsets.left = 50
Does an edge inset only work with buttons that include text?
Update:
I've tried setting contentEdgeInsets, imageEdgeInsets, and titleEdgeInsets to no avail.
I could expand touchable area by editing the actual image and increasing the border around the icon. (This wouldn't be the preferred way but I'll probably go this route if I can't find another solution.)
I overwrote in Swift this answer and it works:
import UIKit
import ObjectiveC
private var KEY_HIT_TEST_EDGE_INSETS: String = "HitTestEdgeInsets"
extension UIButton {
public func setHitTestEdgeInsets(inout hitTestEdgeInsets: UIEdgeInsets) {
let value = NSValue(&hitTestEdgeInsets, withObjCType:NSValue(UIEdgeInsets: UIEdgeInsetsZero).objCType)
objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
private func hitTestEdgeInsets() -> UIEdgeInsets {
let value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS)
if (value != nil) {
var edgeInsets: UIEdgeInsets = UIEdgeInsetsZero
value.getValue(&edgeInsets)
return edgeInsets
} else {
return UIEdgeInsetsZero
}
}
override public func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
if UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets(), UIEdgeInsetsZero) || !self.enabled || self.hidden {
return super.pointInside(point, withEvent: event)
}
let relativeFrame = self.bounds
let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets());
return CGRectContainsPoint(hitFrame, point)
}
}
Use:
var insets: UIEdgeInsets = UIEdgeInsetsMake(-20, -20, -20, -20)
button.setHitTestEdgeInsets(&insets)
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
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.