Is it possible to somehow track the state when a scroll is needed in a UITextView? Swift - swift

I working in collectionViewCell. I upload API data, and I want to check if the number of lines in the UITextView is more than 4, then I need a isScrollEnabled = true, if not, then = false. How to implement this?

You can try something like this to get number of lines
extension UITextView {
func numberOfLines() -> Int {
let layoutManager = self.layoutManager
let numberOfGlyphs = layoutManager.numberOfGlyphs
var lineRange: NSRange = NSMakeRange(0, 1)
var index = 0
var numberOfLines = 0
while index < numberOfGlyphs {
layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
index = NSMaxRange(lineRange)
numberOfLines += 1
}
return numberOfLines
}
}
To call it
let count = textView.numberOfLines()
print(count)

Related

Swift+UIImageView: switch between two animation frame sequences?

Example of code for animation:
func startAnimation(index: Int) {
var jumpImages = ["Jump_1","Jump_2","Jump_3","Jump_4","Jump_5","Jump_6","Jump_7","Jump_8","Jump_9","Jump_10"]
if index == 1 {
jumpImages = ["Jump_11","Jump_21","Jump_31","Jump_41","Jump_51","Jump_61","Jump_71","Jump_81","Jump_91","Jump_101"]
}
var images = [UIImage]()
for image in jumpImages{
images.append(UIImage(named: image)!)
}
self.imageView.frame.origin.y = self.imageView.frame.origin.y + 100.0
self.imageView.animationImages = images
self.imageView.animationDuration = 2.0
self.imageView.animationRepeatCount = 1
self.imageView.startAnimating()
}
startAnimation(index: 0)
...
startAnimation(index: 1)
NOTE: Both startAnimation calls are in the main thread but not in the same run loop.
In my case I want to change jumpImages to another set of images. I should cancel the previous animation and start the new one but it looks like I set the last image in jumpImages sequence instead.
How to solve this issue?
Okay, I think I'm understanding your problem now -- from your statement, "[...] but it looks like I set the last image in jumpImages sequence instead," I think you mean that when you call startAnimation(index: 1), you are seeing just the last frame of the index 0 animations, and none of the index 1 animations.
Assuming that is right, your problem here going to be a race condition.
When you call startAnimation() then second time with index 1, the first animation may or may not still be in progress.
The solution will be to call self.imageView.stopAnimating() before you change all the images and start the new animation. The best practice would be to check the imageView.isAnimating flag before you call it. Something like this:
func startAnimation(index: Int) {
var jumpImages = ["Jump_1","Jump_2","Jump_3","Jump_4","Jump_5","Jump_6","Jump_7","Jump_8","Jump_9","Jump_10"]
if index == 1 {
jumpImages = ["Jump_11","Jump_21","Jump_31","Jump_41","Jump_51","Jump_61","Jump_71","Jump_81","Jump_91","Jump_101"]
}
var images = [UIImage]()
for image in jumpImages{
images.append(UIImage(named: image)!)
}
if self.imageView.isAnimating {
self.imageView.stopAnimating()
}
self.imageView.frame.origin.y = self.imageView.frame.origin.y + 100.0
self.imageView.animationImages = images
self.imageView.animationDuration = 2.0
self.imageView.animationRepeatCount = 1
self.imageView.startAnimating()
}
startAnimation(index: 0)
...
startAnimation(index: 1)
Also, since you're in a function and not within a closure, you can remove all those references to self., which makes things a bit shorter:
func startAnimation(index: Int) {
var jumpImages = ["Jump_1","Jump_2","Jump_3","Jump_4","Jump_5","Jump_6","Jump_7","Jump_8","Jump_9","Jump_10"]
if index == 1 {
jumpImages = ["Jump_11","Jump_21","Jump_31","Jump_41","Jump_51","Jump_61","Jump_71","Jump_81","Jump_91","Jump_101"]
}
var images = [UIImage]()
for image in jumpImages{
images.append(UIImage(named: image)!)
}
if imageView.isAnimating {
imageView.stopAnimating()
}
imageView.frame.origin.y = imageView.frame.origin.y + 100.0
imageView.animationImages = images
imageView.animationDuration = 2.0
imageView.animationRepeatCount = 1
imageView.startAnimating()
}
startAnimation(index: 0)
...
startAnimation(index: 1)

How to display a set number of rows in small Table View with self-sizing cells

I have a custom table view class, which is configured to set the table view height to display only 3 rows. This means if there are 20 rows, table view will be sized to display first 3 rows, and allowing user to scroll.
This code of mine works only if I set the static rowHeight
class CannedRepliesTableView: UITableView {
/// The max visible rows visible in the autocomplete table before the user has to scroll throught them
let maxVisibleRows = 3
open override var intrinsicContentSize: CGSize {
let rows = numberOfRows(inSection: 0) < maxVisibleRows ? numberOfRows(inSection: 0) : maxVisibleRows
return CGSize(width: super.intrinsicContentSize.width, height: (CGFloat(rows) * rowHeight))
}
}
If I set UITableViewAutomaticDimension to the rowHeight, table view is not properly resized. Is there a solution to this?
This would be one way to improve on what you currently have. I don't have experience accessing intrinsicContentSize for this calculations and didn't test this locally (other than syntax), but if previously it worked, this should as well.
Basically you're creating an array with maxVisibleRows number of indexPaths. If you have less, then fetchedIndexesCount prevents an indexOutOfBounds crash. Once you have the array, you iterate for each corresponding cell and fetching its size, finally summing it up.
class CannedRepliesTableView: UITableView {
var focussedSection = 0
let maxVisibleRows = 3
open override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: calculateHeight())
}
private func calculateHeight() -> CGFloat {
guard let indexPaths = startIndexes(firstCount: 3) else {
// Your table view doesn't have any rows. Feel free to return a non optional and remove this check if confident there's always at least a row
return 0
}
return indexPaths.compactMap({ cellForRow(at: $0)?.intrinsicContentSize.height }).reduce(0, +)
}
private func startIndexes(firstCount x: Int) -> [IndexPath]? {
let rowsCount = numberOfRows(inSection: focussedSection)
let fetchedIndexesCount = min(x, rowsCount)
guard fetchedIndexesCount > 0 else {
return nil
}
var result = [IndexPath]()
for i in 0..<fetchedIndexesCount {
result.append(IndexPath(row: i, section: focussedSection))
}
return result
}
}

swift cursor position with emojis

The standard method I use to get the cursor position in a UITextfield does not seem to work with some emojis. The following code queries a textField for cursor position after insertion of two characters, first an emoji and then a letter character. When the emoji is inserted into the textField the function returns a value of 2 for the cursor position instead of the expected result of 1. Any ideas into what I am doing wrong or how to correct this. Thanks
Here is the code from an xcode playground:
class MyViewController : UIViewController {
override func loadView() {
//setup view
let view = UIView()
view.backgroundColor = .white
let textField = UITextField()
textField.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
textField.textColor = .black
view.addSubview(textField)
self.view = view
//check cursor position
var str = "šŸŠ"
textField.insertText(str)
print("cursor position after '\(str)' insertion is \(getCursorPosition(textField))")
textField.text = ""
str = "A"
textField.insertText(str)
print("cursor position after '\(str)' insertion is \(getCursorPosition(textField))")
}
func getCursorPosition(_ textField: UITextField) -> Int {
if let selectedRange = textField.selectedTextRange {
let cursorPosition = textField.offset(from: textField.beginningOfDocument, to: selectedRange.end)
return cursorPosition
}
return -1
}
}
the code return the following output:
cursor position after 'šŸŠ' insertion is 2
cursor position after 'A' insertion is 1
I'm trying to use the cursor position to split the text string into two pieces -- the text that occurs before the cursor and the text that occurs after the cursor. To do this I use the cursor position as an index for a character array I have created using the map function as follows. The cursor position leads to an incorrect array index with an emoji
var textBeforeCursor = String()
var textAfterCursor = String()
let array = textField.text!.map { String($0) }
let cursorPosition = getCursorPosition(textField)
for index in 0..<cursorPosition {
textBeforeCursor += array[index]
}
Your issue is that the NSRange value returned by UITextField selectedTextRange and the offset need to be properly converted to a Swift String.Index.
func getCursorPosition(_ textField: UITextField) -> String.Index? {
if let selectedRange = textField.selectedTextRange {
let cursorPosition = textField.offset(from: textField.beginningOfDocument, to: selectedRange.end)
let positionRange = NSRange(location: 0, length: cursorPosition)
let stringOffset = Range(positionRange, in: textField.text!)!
return stringOffset.upperBound
}
return nil
}
Once you have that String.Index you can split the string.
if let index = getCursorPosition(textField) {
let textBeforeCursor = textField.text![..<index]
let textAfterCursor = textField.text![index...]
}

Why does Xcode show warning 'Will never be executed' on my 'if' clause?

I want to show a note/label on my tableview background when/if there is no data to load, or data is being fetched etc.
I can't see what am I doing wrong here. Xcode is showing a warning "Will never be executed" on this line of code: if mostUpTodateNewsItemsFromRealm?.count < 1 {
Here is the method.
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// create a lable ready to display
let statusLabel: UILabel = UILabel(frame: CGRectMake(0, 0, self.tableView.bounds.size.width, self.tableView.bounds.size.height))
statusLabel.textColor = globalTintColor
statusLabel.textAlignment = NSTextAlignment.Center
self.tableView.backgroundView = statusLabel
self.tableView.backgroundView?.backgroundColor = colourOfAllPickerBackgrounds
// 1) Check if we have tried to fetch news
if NSUserDefaults.standardUserDefaults().valueForKey("haveTriedToFetchNewsForThisChurch") as! Bool == false {
statusLabel.text = "Busy loading news..."
} else {
// If have tried to fetch news items = true
// 2) check church has channels
let numberOfChannelsSubscribedToIs = 0
if let upToDateSubsInfo = upToDateChannelAndSubsInfo {
let numberOfChannelsSubscribedToIs = 0
for subInfo in upToDateSubsInfo {
if subInfo.subscribed == true {
numberOfChannelsSubscribedToIs + 1
}
}
}
if numberOfChannelsSubscribedToIs < 1 {
// if no channels
// show messsage saying you aren't subscribed to any channels.
statusLabel.text = "Your church hasn't setup any news channels yet."
} else {
// 3) if we have tried to fetch news AND the church DOES have channels
// check if we have any news items to show
if mostUpTodateNewsItemsFromRealm?.count < 1 {
// If no news items
statusLabel.text = "Your church hasn't broadcast and news yet."
} else {
// if have tried to fetch AND church has channels AND there ARE news items
// remove the background image so doesn't show when items load.
self.tableView.backgroundView = nil
}
}
}
// in all circumstances there will be one section
return 1
}
Your code first created a constant:
let numberOfChannelsSubscribedToIs = 0
And then you check whether it is less than 1:
if numberOfChannelsSubscribedToIs < 1
Since it is a constant, it will never change. This means that the if clause will always be executed. Thus, the else clause will never be executed.
So first you need to make this constant variable:
var numberOfChannelsSubscribedToIs = 0
Then just change this:
if subInfo.subscribed == true {
numberOfChannelsSubscribedToIs + 1
}
to this:
if subInfo.subscribed == true {
numberOfChannelsSubscribedToIs += 1
}
This way, numberOFChannelSubscribedToIs can be some number other than 0. And the else clause can be executed.
var and let are very different!

How do you determine spacing between cells in UICollectionView flowLayout

I have a UICollectionView with a flow layout and each cell is a square. How do I determine the spacing between each cells on each row? I can't seem to find the appropriate settings for this. I see there's a min spacing attributes on the nib file for a collection view, but I set this to 0 and the cells doesn't even stick.
Any other idea?
Update: Swift version of this answer: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift
Taking #matt's lead I modified his code to insure that items are ALWAYS left aligned. I found that if an item ended up on a line by itself, it would be centered by the flow layout. I made the following changes to address this issue.
This situation would only ever occur if you have cells that vary in width, which could result in a layout like the following. The last line always left aligns due to the behavior of UICollectionViewFlowLayout, the issue lies in items that are by themselves in any line but the last one.
With #matt's code I was seeing.
In that example we see that cells get centered if they end up on the line by themselves. The code below insures your collection view would look like this.
#import "CWDLeftAlignedCollectionViewFlowLayout.h"
const NSInteger kMaxCellSpacing = 9;
#implementation CWDLeftAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
if (nil == attributes.representedElementKind) {
NSIndexPath* indexPath = attributes.indexPath;
attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
}
}
return attributesToReturn;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* currentItemAttributes =
[super layoutAttributesForItemAtIndexPath:indexPath];
UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];
CGRect currentFrame = currentItemAttributes.frame;
if (indexPath.item == 0) { // first item of section
currentFrame.origin.x = sectionInset.left; // first item of the section should always be left aligned
currentItemAttributes.frame = currentFrame;
return currentItemAttributes;
}
NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
CGFloat previousFrameRightPoint = CGRectGetMaxX(previousFrame) + kMaxCellSpacing;
CGRect strecthedCurrentFrame = CGRectMake(0,
currentFrame.origin.y,
self.collectionView.frame.size.width,
currentFrame.size.height);
if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
// the approach here is to take the current frame, left align it to the edge of the view
// then stretch it the width of the collection view, if it intersects with the previous frame then that means it
// is on the same line, otherwise it is on it's own new line
currentFrame.origin.x = sectionInset.left; // first item on the line should always be left aligned
currentItemAttributes.frame = currentFrame;
return currentItemAttributes;
}
currentFrame.origin.x = previousFrameRightPoint;
currentItemAttributes.frame = currentFrame;
return currentItemAttributes;
}
#end
To get a maximum interitem spacing, subclass UICollectionViewFlowLayout and override layoutAttributesForElementsInRect: and layoutAttributesForItemAtIndexPath:.
For example, a common problem is this: the rows of a collection view are right-and-left justified, except for the last line which is left-justified. Let's say we want all the lines to be left-justified, so that the space between them is, let's say, 10 points. Here's an easy way (in your UICollectionViewFlowLayout subclass):
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray* arr = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* atts in arr) {
if (nil == atts.representedElementKind) {
NSIndexPath* ip = atts.indexPath;
atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame;
}
}
return arr;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* atts =
[super layoutAttributesForItemAtIndexPath:indexPath];
if (indexPath.item == 0) // degenerate case 1, first item of section
return atts;
NSIndexPath* ipPrev =
[NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame;
CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10;
if (atts.frame.origin.x <= rightPrev) // degenerate case 2, first item of line
return atts;
CGRect f = atts.frame;
f.origin.x = rightPrev;
atts.frame = f;
return atts;
}
The reason this is so easy is that we aren't really performing the heavy lifting of the layout; we are leveraging the layout work that UICollectionViewFlowLayout has already done for us. It has already decided how many items go in each line; we're just reading those lines and shoving the items together, if you see what I mean.
There are a few things to consider:
Try changing the minimum spacing in IB, but leave the cursor in that field. Notice that Xcode doesn't immediately mark the document as changed. When you click in a different field, though, Xcode does notice that the document is changed and marks it so in the file navigator. So, be sure to tab or click over to a different field after making a change.
Save your storyboard/xib file after making a change, and be sure to rebuild the app. It's not hard to miss that step, and then you're left scratching your head wondering why your changes didn't seem to have any effect.
UICollectionViewFlowLayout has a minimumInteritemSpacing property, which is what you're setting in IB. But the collection's delegate can also have a method to determine the inter-item spacing. That method trump's the layout's property, so if you implement it in your delegate your layout's property won't be used.
Remember that the spacing there is a minimum spacing. The layout will use that number (whether it comes from the property or from the delegate method) as the smallest allowable space, but it may use a larger space if it has space leftover on the line. So if, for example, you set the minimum spacing to 0, you may still see a few pixels between items. If you want more control over exactly how the items are spaced you should probably use a different layout (possibly one of your own creation).
A little bit of maths does the trick more easily. The code wrote by Chris Wagner is horrible because it calls the layout attributes of each previous items. So the more you scroll, the more it's slow...
Just use modulo like this (I'm using my minimumInteritemSpacing value as a max value too):
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
NSInteger numberOfItemsPerLine = floor([self collectionViewContentSize].width / [self itemSize].width);
if (indexPath.item % numberOfItemsPerLine != 0)
{
NSInteger cellIndexInLine = (indexPath.item % numberOfItemsPerLine);
CGRect itemFrame = [currentItemAttributes frame];
itemFrame.origin.x = ([self itemSize].width * cellIndexInLine) + ([self minimumInteritemSpacing] * cellIndexInLine);
currentItemAttributes.frame = itemFrame;
}
return currentItemAttributes;
}
An easy way to left-justify is to modify layoutAttributesForElementsInRect: in your subclass of UICollectionViewFlowLayout:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
CGRect prevFrame = CGRectMake(-FLT_MAX, -FLT_MAX, 0, 0);
for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes)
{
//fix blur
CGRect theFrame = CGRectIntegral(layoutAttributes.frame);
//left justify
if(prevFrame.origin.x > -FLT_MAX &&
prevFrame.origin.y >= theFrame.origin.y &&
prevFrame.origin.y <= theFrame.origin.y) //workaround for float == warning
{
theFrame.origin.x = prevFrame.origin.x +
prevFrame.size.width +
EXACT_SPACE_BETWEEN_ITEMS;
}
prevFrame = theFrame;
layoutAttributes.frame = theFrame;
}
return allLayoutAttributes;
}
Clean Swift solution, from an history of evolution:
there was matt answer
there was Chris Wagner lone items fix
there was mokagio sectionInset and minimumInteritemSpacing improvement
there was fanpyi Swift version
now here is a simplified and clean version of mine:
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,
collectionView != nil else {
// should never happen
return nil
}
// if the current frame, once stretched to the full row intersects the previous frame then they are on the same row
if indexPath.item != 0,
let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.origin.y, width: .infinity, height: previousFrame.size.height)) {
// the next item on a line
currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
} else {
// the first item on a line
currentItemAttributes.frame.origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left
}
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
}
}
Usage: the spacing between items is determined by delegate's collectionView (_:layout:minimumInteritemSpacingForSectionAt:).
I put it on github, https://github.com/Coeur/UICollectionViewLeftAlignedLayout, where I actually added a feature of supporting both scroll directions (horizontal and vertical).
The swift version of Chris solution.
class PazLeftAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout {
var maxCellSpacing = 14.0
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
if var attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? Array<UICollectionViewLayoutAttributes> {
for attributes in attributesToReturn {
if attributes.representedElementKind == nil {
let indexPath = attributes.indexPath
attributes.frame = self.layoutAttributesForItemAtIndexPath(indexPath).frame;
}
}
return attributesToReturn;
}
return super.layoutAttributesForElementsInRect(rect)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
if let collectionViewFlowLayout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
let sectionInset = collectionViewFlowLayout.sectionInset
if (indexPath.item == 0) { // first item of section
var frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
let previousIndexPath = NSIndexPath(forItem:indexPath.item-1, inSection:indexPath.section)
let previousFrame = self.layoutAttributesForItemAtIndexPath(previousIndexPath).frame;
let previousFrameRightPoint = Double(previousFrame.origin.x) + Double(previousFrame.size.width) + self.maxCellSpacing
let currentFrame = currentItemAttributes.frame
var width : CGFloat = 0.0
if let collectionViewWidth = self.collectionView?.frame.size.width {
width = collectionViewWidth
}
let strecthedCurrentFrame = CGRectMake(0,
currentFrame.origin.y,
width,
currentFrame.size.height);
if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
// the approach here is to take the current frame, left align it to the edge of the view
// then stretch it the width of the collection view, if it intersects with the previous frame then that means it
// is on the same line, otherwise it is on it's own new line
var frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
var frame = currentItemAttributes.frame;
frame.origin.x = CGFloat(previousFrameRightPoint)
currentItemAttributes.frame = frame;
}
return currentItemAttributes;
}
}
To use it do the following:
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.collectionViewLayout = self.layout
}
var layout : PazLeftAlignedCollectionViewFlowLayout {
var layout = PazLeftAlignedCollectionViewFlowLayout()
layout.itemSize = CGSizeMake(220.0, 230.0)
layout.minimumLineSpacing = 12.0
return layout
}
The "problem" with UICollectionViewFlowLayout is that it applies a justified align to the cells: The first cell in a row is left-aligned, the last cell in a row is right-aligned and all other cells in between are evenly distributed with an equal spacing that's greater than the minimumInteritemSpacing.
There are already many great answers to this post that solve this problem by subclassing UICollectionViewFlowLayout. As a result you get a layout that aligns the cells left. Another valid solution to distribute the cells with a constant spacing is to align the cells right.
AlignedCollectionViewFlowLayout
I've created a UICollectionViewFlowLayout subclass as well that follows a similar idea as suggested by matt and Chris Wagner that can either align the cells
ā¬…ļøŽ left:
or āž”ļøŽ right:
You can simply download it from here, add the layout file to your project and set AlignedCollectionViewFlowLayout as your collection view's layout class:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout
How it works (for left-aligned cells):
+---------+----------------------------------------------------------------+---------+
| | | |
| | +------------+ | |
| | | | | |
| section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
| inset | |intersection| | | line rect | inset |
| |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| |
| (left) | | | current item | (right) |
| | +------------+ | |
| | previous item | |
+---------+----------------------------------------------------------------+---------+
The concept here is to check if the current cell with index i and the previous cell with the index iāˆ’1 occupy the same line.
If they don't the cell with index i is the left most cell in the line.
ā†’ Move the cell to the left edge of the collection view (without changing its vertical position).
If they do, the cell with index i is not the left most cell in the line.
ā†’ Get the previous cell's frame (with the index iāˆ’1) and move the current cell next to it.
For right-aligned cells...
... you do the same vice-versa, i.e. you check the next cell with the index i+1 instead.
You can do it in two ways.
Firstly, do some modification in layoutAttributesForItem,
get the current attributes's layout via the previous layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame.
layoutAttributesForItem(at:): This method provides on demand layout information to the collection view. You need to override it and return the layout attributes for the item at the requested indexPath.
Secondly, new some attributes via UICollectionViewLayoutAttributes(forCellWith: indexPath),layout them wherever you want.
And the math is a little larger, because it performs the heavy lifting of the layout.
layoutAttributesForElements(in:): In this method you need to return the layout attributes for all the items inside the given rectangle. You return the attributes to the collection view as an array of UICollectionViewLayoutAttributes.
You can check My repo
you can put items left aligned
you can put items right aligned
you can put items right aligned and items reversed
A cleaner swift version for people interested, based on Chris Wagner's answer:
class AlignLeftFlowLayout: UICollectionViewFlowLayout {
var maximumCellSpacing = CGFloat(9.0)
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]
for attributes in attributesToReturn ?? [] {
if attributes.representedElementKind == nil {
attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let sectionInset = (self.collectionView?.collectionViewLayout as UICollectionViewFlowLayout).sectionInset
if indexPath.item == 0 {
let f = curAttributes.frame
curAttributes.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
return curAttributes
}
let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath).frame
let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing
let curFrame = curAttributes.frame
let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)
if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
curAttributes.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
} else {
curAttributes.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
}
return curAttributes
}
}
Here it is for NSCollectionViewFlowLayout
class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout {
var maximumCellSpacing = CGFloat(2.0)
override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
for attributes in attributesToReturn ?? [] {
if attributes.representedElementKind == nil {
attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath!)!.frame
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {
let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let sectionInset = (self.collectionView?.collectionViewLayout as! NSCollectionViewFlowLayout).sectionInset
if indexPath.item == 0 {
let f = curAttributes!.frame
curAttributes!.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
return curAttributes
}
let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath)!.frame
let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing
let curFrame = curAttributes!.frame
let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)
if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
curAttributes!.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
} else {
curAttributes!.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
}
return curAttributes
}
}
a swift version base on mokagio:https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift
class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
if let attributesToReturn = attributesToReturn {
for attributes in attributesToReturn {
if attributes.representedElementKind == nil {
let indexpath = attributes.indexPath
if let attr = layoutAttributesForItemAtIndexPath(indexpath) {
attributes.frame = attr.frame
}
}
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
if let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath){
let sectionInset = self.evaluatedSectionInsetForItemAtIndex(indexPath.section)
let isFirstItemInSection = indexPath.item == 0;
let layoutWidth = CGRectGetWidth(self.collectionView!.frame) - sectionInset.left - sectionInset.right;
if (isFirstItemInSection) {
currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
return currentItemAttributes
}
let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
let previousFrame = layoutAttributesForItemAtIndexPath(previousIndexPath)?.frame ?? CGRectZero
let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width
let currentFrame = currentItemAttributes.frame;
let strecthedCurrentFrame = CGRectMake(sectionInset.left,
currentFrame.origin.y,
layoutWidth,
currentFrame.size.height)
// if the current frame, once left aligned to the left and stretched to the full collection view
// widht intersects the previous frame then they are on the same line
let isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)
if (isFirstItemInRow) {
// make sure the first item on a line is left aligned
currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
return currentItemAttributes
}
var frame = currentItemAttributes.frame;
frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacingForSectionAtIndex(indexPath.section)
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
return nil
}
func evaluatedMinimumInteritemSpacingForSectionAtIndex(sectionIndex:Int) -> CGFloat {
if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
if delegate.respondsToSelector("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:") {
return delegate.collectionView!(self.collectionView!, layout: self, minimumInteritemSpacingForSectionAtIndex: sectionIndex)
}
}
return self.minimumInteritemSpacing
}
func evaluatedSectionInsetForItemAtIndex(index: Int) ->UIEdgeInsets {
if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
if delegate.respondsToSelector("collectionView:layout:insetForSectionAtIndex:") {
return delegate.collectionView!(self.collectionView!, layout: self, insetForSectionAtIndex: index)
}
}
return self.sectionInset
}
}