iOS Uicollectionview - Default Flow, Fill Rows from Right to Left

iOS UICollectionView - default flow, fill rows from right to left

Assuming you are expecting ONLY three cells, and using a header view for this collection view, you will need to perform the following steps:

override UICollectionViewFlowLayout

@interface GSRightToLeftCollectionViewFlowLayout : UICollectionViewFlowLayout

(GS is for Groboot SmarTech :))

the .m should look like this:

#import "GSRightToLeftCollectionViewFlowLayout.h"

typedef enum {
// enum for comfortibility.
CellXPositionLeft = 1,
CellXpositionRight = 2,
CellXpositionCenter = 3,
CellXPositionNone = 4
} CellXPosition;

@interface GSRightToLeftCollectionViewFlowLayout ()

@property (nonatomic) BOOL cellFlag;
@property (nonatomic) float cellLeftX;
@property (nonatomic) float cellMiddleX;
@property (nonatomic) float cellRightX;

@end

@implementation GSRightToLeftCollectionViewFlowLayout

// when ever the bounds change, call layoutAttributesForElementsInRect:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *allItems = [[super layoutAttributesForElementsInRect:rect] mutableCopy];

for (UICollectionViewLayoutAttributes *attribute in allItems) {

// when we get an item, calculate it's English writing position,
// so that we can convert it to the Hebrew - Arabic position.
if (!self.cellFlag) {

[self calculateLocationsForCellsWithAttribute:attribute];

self.cellFlag = YES;
}

// if it's a header, do not change it's place.
if (attribute.representedElementKind == UICollectionElementKindSectionHeader) {

continue;
}

// check where the item should be placed.
CellXPosition position = [self attributeForPosition:attribute];

switch (position) {
case CellXPositionLeft:
attribute.frame = CGRectMake(self.cellLeftX, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);
break;
case CellXpositionRight:
attribute.frame = CGRectMake(self.cellRightX, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);
break;
case CellXpositionCenter:
attribute.frame = CGRectMake(self.cellMiddleX, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);
break;
case CellXPositionNone:
NSLog(@"warning");
break;
}
}

return allItems;
}

- (CellXPosition)attributeForPosition:(UICollectionViewLayoutAttributes *)attribute
{
CellXPosition cellXposition = CellXPositionNone;

// we will return an opposite answer
if (attribute.indexPath.row % 3 == 0) {

// if it's in the left side, move it to the right
cellXposition = CellXpositionRight;

} else if (attribute.indexPath.row % 3 == 1) {

cellXposition = CellXpositionCenter;

} else if (attribute.indexPath.row % 3) {

// if it's in the right side, move it to the left
cellXposition = CellXPositionLeft;
}

return cellXposition;
}

- (void)calculateLocationsForCellsWithAttribute:(UICollectionViewLayoutAttributes *)attribute
{
float cellWidth = self.itemSize.width;
float minimumX = self.sectionInset.left;
float maximumX = self.sectionInset.right;
float displayWidth = self.collectionView.contentSize.width - minimumX - maximumX;
// on iOS6, displayWidth will be 0 (don't know why), so insert an if (displayWidth == 0) and set manually the size.

self.cellLeftX = minimumX;
float space = (displayWidth - cellWidth * 3) / 2;
self.cellMiddleX = self.cellRightX + cellWidth + space;
self.cellRightX = self.cellMiddleX + cellWidth + space;
}

@end

In the Class which you display the CollectionView, you need to do this:

if you are using storyboards:
1. change the collectionView layout to custom (in the attribute inspector)
2. set it's class to GSRightToLeftCollectionViewFlowLayout.
3. in ViewDidLoad (or whenever you perform initialization)

- (void)viewDidLoad
{
[super viewDidLoad];

GSRightToLeftCollectionViewFlowLayout *layout = [[GSRightToLeftCollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(98, 138); // set the item size.
layout.minimumLineSpacing = 1; // other layout properties.
layout.minimumInteritemSpacing = 1; // other layout properties.
layout.headerReferenceSize = CGSizeMake(50, 18);
layout.sectionInset = UIEdgeInsetsMake(1.0f, 0.0f, 1.0f, 0.0f);
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
// in case you use headers / footers. this is also where you would register a Cell if you don't use storyboards.
[self.collectionView registerClass:[GSReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"HeaderView"];
self.collectionView.collectionViewLayout = layout;
}

UICollectionView doesn't fill data from right to left in RTL

You need to create your own UICollectionViewLayout, as I said in my comment, we will start backwards, first you need to add this lines on your ViewController viewDidLoad method

let semanticLayout = SemanticLayout()
semanticLayout.delegate = self
self.collectionView.collectionViewLayout = semanticLayout

this is the delegate that you need to implement

extension ViewController: SemanticLayoutDelegate{
func semanticDisposition() -> SemanticDisposition {
return SemanticDisposition.rigthToLeft
}
}

using your ViewController name instead of ViewController ofc

And here you have the SemanticLayout class, check that is fully customizable you can define if your UICollectionView will be RTL or LTR with delegate method semanticDisposition

import UIKit

protocol SemanticLayoutDelegate: UICollectionViewDelegateFlowLayout {
func semanticDisposition() -> SemanticDisposition
}

enum SemanticDisposition {
case leftToRigth
case rigthToLeft
}

class SemanticLayout: UICollectionViewLayout {

weak var delegate: SemanticLayoutDelegate!

fileprivate var cellPadding: CGFloat = 10

fileprivate var cache = [UICollectionViewLayoutAttributes]()

fileprivate var contentHeight: CGFloat = 0
private var rowsWidth : [CGFloat] = [0]
private var avaiableSpaces : [(Int,CGFloat)] = []
private var currentRow : Int = 0
private var rowHeigths : CGFloat = -1.0

fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}

private func availableWidthForRow(index:Int) -> CGFloat {
let ocuppedWidth = self.rowsWidth[index]
return self.contentWidth - ocuppedWidth
}

private func canAddCellAtRow(rowIndex:Int,size:CGSize) ->Bool
{
if(availableWidthForRow(index: rowIndex) >= size.width) {
return true
}

return false
}

override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}

override func prepare() {
// Check if cache is empty
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}

for item in 0 ..< collectionView.numberOfItems(inSection: 0) {

let indexPath = IndexPath(item: item, section: 0)

let viewSize: CGSize = delegate.collectionView!(collectionView, layout: self, sizeForItemAt: indexPath)
if(self.rowHeigths < 0) {
self.rowHeigths = viewSize.height
}

let width = viewSize.width
let height = viewSize.height

var xOffset = self.rowsWidth[self.currentRow]
if(self.canAddCellAtRow(rowIndex: self.currentRow, size: viewSize)) {

if(self.delegate.semanticDisposition() == .rigthToLeft) {
xOffset = (contentWidth - self.rowsWidth[self.currentRow]) - width
}

} else {
self.currentRow += 1
self.rowsWidth.append(0.0)
xOffset = self.rowsWidth[self.currentRow]
if(self.delegate.semanticDisposition() == .rigthToLeft) {
xOffset = (contentWidth - self.rowsWidth[self.currentRow]) - width
}

}

let yOffset = CGFloat(self.currentRow) * self.rowHeigths

let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)

let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)

contentHeight = max(contentHeight, frame.maxY)

self.rowsWidth[self.currentRow] += width
}
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()

// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}

Result
RTL

RTL
LTR
LTR

Aligning right to left on UICollectionView

You can get similar result by performing a transform on the collection view and reverse the flip on its content:

First when creating the UICollectionView I performed a horizontal flip on it:

[collectionView_ setTransform:CGAffineTransformMakeScale(-1, 1)];

Then subclass UICollectionViewCell and in here do the same horizontal flip on its contentView:

[self.contentView setTransform:CGAffineTransformMakeScale(-1, 1)];

Configure UICollectionViewFlowLayout to lay out rows from bottom to top

You could basically implement it with a simple logic, however this seems to be some how odd. If the collectionview contentsize is same as that of the collectionview bounds or if all the cells are visible then you could implement this with simple flowLayout as this,

@implementation SimpleFlowLayout

- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewLayoutAttributes *attribute = [super layoutAttributesForItemAtIndexPath:indexPath];
[self modifyLayoutAttribute:attribute];
return attribute;
}

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
for(UICollectionViewLayoutAttributes *attribute in attributes){
[self modifyLayoutAttribute:attribute];
}
return attributes;
}

- (void)modifyLayoutAttribute:(UICollectionViewLayoutAttributes*)attribute{
CGSize contentSize = self.collectionViewContentSize;
CGRect frame = attribute.frame;
frame.origin.x = contentSize.width - attribute.frame.origin.x - attribute.frame.size.width;
frame.origin.y = contentSize.height - attribute.frame.origin.y - attribute.frame.size.height;
attribute.frame = frame;

}

@end

And so the figure looks like this,

Sample Image

But, if you use more rows, more than the that can be seen on the screen at the same time, then there seems to be some problem with reusing. Since the UICollectionView datasource method, collectionView:cellForItemAtIndexPath: works linearly and asks for the indexPath as the user scrolls, the cell are asked in the usual increasing indexPath pattern such as 1 --- 100 though we would want it to reverse this pattern. While scrolling we would need the collectionView to ask us for the items in decreasing order since our 100 item resides at top and 1 item at bottom. So, I dont have any particular idea about how this could be accomplished.

Left Align Cells in UICollectionView

The other solutions in this thread do not work properly, when the line is composed by only 1 item or are over complicated.

Based on the example given by Ryan, I changed the code to detect a new line by inspecting the Y position of the new element. Very simple and quick in performance.

Swift:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)

var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.y >= maxY {
leftMargin = sectionInset.left
}

layoutAttribute.frame.origin.x = leftMargin

leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}

return attributes
}
}

If you want to have supplementary views keep their size, add the following at the top of the closure in the forEach call:

guard layoutAttribute.representedElementCategory == .cell else {
return
}

Objective-C:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];

CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
CGFloat maxY = -1.0f;

//this loop assumes attributes are in IndexPath order
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if (attribute.frame.origin.y >= maxY) {
leftMargin = self.sectionInset.left;
}

attribute.frame = CGRectMake(leftMargin, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);

leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing;
maxY = MAX(CGRectGetMaxY(attribute.frame), maxY);
}

return attributes;
}

UICollectionView spacing margins

You can use the collectionView:layout:insetForSectionAtIndex: method for your UICollectionView or set the sectionInset property of the UICollectionViewFlowLayout object attached to your UICollectionView:

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section{
return UIEdgeInsetsMake(top, left, bottom, right);
}

or

UICollectionViewFlowLayout *aFlowLayout = [[UICollectionViewFlowLayout alloc] init];
[aFlowLayout setSectionInset:UIEdgeInsetsMake(top, left, bottom, right)];

Updated for Swift 5

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 25, left: 15, bottom: 0, right: 5)
}


Related Topics



Leave a reply



Submit