rebello95
rebello95

Reputation: 8576

Aligning right to left on UICollectionView

this is a pretty straightforward question, but I haven't been able to find a definitive answer to it on SO (if I missed it, please correct me).

Basically, my question is: Is it possible to align UICollectionView row contents from right to left instead of from left to right?

In my research I've seen answers suggesting subclassing UICollectionViewFlowLayout, but I haven't been able to find an example where one was created for right-alignment.

My goal is to have 2 collection views set up like this:

Example

Any help is greatly appreciated!

Upvotes: 74

Views: 57620

Answers (17)

John Stephen
John Stephen

Reputation: 7734

Here is my class for a right-aligned flow layout:

class RightAlignedFlowLayout: UICollectionViewLayout {
    
    private var cache = [UICollectionViewLayoutAttributes]()
    private var contentHeight: CGFloat = 0
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else { return 0 }
        return collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right
    }
    
    // Configure item size and spacing
    var itemSize = CGSize(width: 50, height: 50)
    var minimumLineSpacing: CGFloat = 10
    var minimumInteritemSpacing: CGFloat = 10
    var sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
    
    override func prepare() {
        guard let collectionView = collectionView else { return }
        cache.removeAll()
        
        var yOffset = sectionInset.top
        
        for section in 0..<collectionView.numberOfSections {
            var rowAttributes: [UICollectionViewLayoutAttributes] = []
            var rowHeight: CGFloat = 0
            
            var xOffset = sectionInset.left
            
            for item in 0..<collectionView.numberOfItems(inSection: section) {
                let indexPath = IndexPath(item: item, section: section)
                
                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                let itemWidth = itemSize.width
                
                if xOffset + itemWidth + sectionInset.right > contentWidth {
                    // Right-align items from the current row
                    let alignmentOffset = contentWidth - sectionInset.right - xOffset
                    rowAttributes.forEach { $0.frame.origin.x += alignmentOffset}
                    
                    // Add the row items
                    cache.append(contentsOf: rowAttributes)
                    
                    // Move to the next row
                    xOffset = sectionInset.left
                    yOffset += rowHeight + minimumLineSpacing
                    rowHeight = 0
                    rowAttributes = []
                }
                
                // Set frame for the current item
                let frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height)
                attributes.frame = frame
                rowAttributes.append(attributes)
                
                // Update xOffset for the next item
                xOffset += itemSize.width + minimumInteritemSpacing
                
                // Update row height
                rowHeight = max(rowHeight, itemSize.height)
            }
            
            // Process remaining items from the last row
            if !rowAttributes.isEmpty {
                
                // Right-align items from the current row
                let alignmentOffset = contentWidth - sectionInset.right - xOffset
                rowAttributes.forEach { $0.frame.origin.x += alignmentOffset}
                
                // Move to the next row
                yOffset += rowHeight + minimumLineSpacing
                
                // Add remaining items
                cache.append(contentsOf: rowAttributes)
            }
        }
        
        // Update content height to accommodate all items
        contentHeight = yOffset + sectionInset.bottom
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return cache.filter { $0.frame.intersects(rect) }
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache.first { $0.indexPath == indexPath }
    }
}

Put the class name (RightAlignedFlowLayout) into the Custom layout field for the UICollectionView in Interface Builder, or create a RightAlignedFlowLayout() in code and attach it to the collectionView.

I wish I could have properties, such as itemSize, for this layout show up as inspectable in IB, but it works similarly to the built-in flow layout otherwise.

Upvotes: 0

Hafiz Anser
Hafiz Anser

Reputation: 581

Needs to do 2 things:

1-

extension UICollectionViewFlowLayout {

    open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
        return true
    }

}

2-

self.categoryCollectionView.semanticContentAttribute = UISemanticContentAttribute.forceRightToLeft

Upvotes: -1

Krishna Kirana
Krishna Kirana

Reputation: 478

Adding below extension worked for me

extension UICollectionViewFlowLayout {
    open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
        return true  //RETURN true if collection view needs to enable RTL
    }
}

Upvotes: 8

Md. Shofiulla
Md. Shofiulla

Reputation: 2305

None of the above answers working for me. So finally I fixed my issue with this. I am using RTL for 2 languages(Arabic, French) Here is my solution

Use this line for transferring collection view left to right

let LangCheck = PPLocalization.sharedInstance.getLanguage(forAPI: true)
if(LangCheck == "ar"){
   CollectionView.transform = CGAffineTransform(scaleX: -1, y: 1);
}

Use this line to make fit your label inside your collection view cell

let LangCheck = PPLocalization.sharedInstance.getLanguage(forAPI: true)
  if(LangCheck == "ar"){
     cell.lbCategoryName.transform = CGAffineTransform(scaleX: -1.0, y: 1.0)
  }

Upvotes: 0

Sabrina
Sabrina

Reputation: 2799

For iOS 9+

  1. Reverse your collectionView in viewDidLoad() with this :

    myCollectionView.transform = CGAffineTransform(scaleX: -1, y: 1)

  2. Reverse-Back your cell (because all things are mirrored) in cellForItemAt with this :

    cell.transform = CGAffineTransform(scaleX: -1, y: 1)

Now content is on right side and scroll starts from right.

Upvotes: 4

hasan
hasan

Reputation: 24205

UICollectionView already support rtl direction if UICollectionViewFlowLayout is not dynamic.

Changing Estimate size to None as the image shows automatically changed the direction of the CollectionView.

enter image description here

One more issue is to scroll to the end of the collection view after reloading data.

extension UICollectionView {
    
    func scrollToEndIfArabic() {
        if Language.shared.isArabic() {
            DispatchQueue.main.async {
                self.contentOffset
                    = CGPoint(x: self.contentSize.width
                        - self.frame.width
                        + self.contentInset.right, y: 0)
            }
        }
    }
}

Upvotes: 2

urvashi bhagat
urvashi bhagat

Reputation: 1163

extension UICollectionViewFlowLayout {

    open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
        return true
    }

}

Upvotes: 11

SamB
SamB

Reputation: 3245

From what I can tell, all of these answers want to fill the collection view from right to left. For example, if you labeled cells 1, 2, 3, then they would appear in the order 3, 2, 1 in the collection view. In my case I wanted the cells to appear in the order 1, 2, 3 (like text right alignment when the line is not full). In order to do this I created a simple UICollectionViewFlow layout.

import UIKit

class RightAlignFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
    {
        guard let attrsArr = super.layoutAttributesForElements(in: rect) else {
            return nil
        }
        guard let collectionView = self.collectionView else { return attrsArr }
        if self.collectionViewContentSize.width > collectionView.bounds.width {
            return attrsArr
        }

        let remainingSpace = collectionView.bounds.width - self.collectionViewContentSize.width

        for attr in attrsArr {
            attr.frame.origin.x += remainingSpace
        }

        return attrsArr
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let attrs = super.layoutAttributesForItem(at: indexPath) else { return nil }
        guard let collectionView = self.collectionView else { return attrs }
        if self.collectionViewContentSize.width > collectionView.bounds.width {
            return attrs
        }

        let remainingSpace = collectionView.bounds.width - self.collectionViewContentSize.width
        attrs.frame.origin.x += remainingSpace
        return attrs
    }

}

Upvotes: 1

Ashi
Ashi

Reputation: 864

Non of the above answers worked for me. The main reason was that most of them are not complete. However, I found this solution from this link by AlexSerdobintsev.

In AppDelegate.cs. First, you have to import the following function

[DllImport(ObjCRuntime.Constants.ObjectiveCLibrary, EntryPoint = "objc_msgSend")]
internal extern static IntPtr IntPtr_objc_msgSend(IntPtr receiver, IntPtr selector, UISemanticContentAttribute arg1);

Then call the function inside FinishedLaunching

        var selector = new ObjCRuntime.Selector("setSemanticContentAttribute:");
        IntPtr_objc_msgSend(UIView.Appearance.Handle, selector.Handle, UISemanticContentAttribute.ForceRightToLeft);

voila! we are done. Here is the file after applying the changes:

    [Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    [DllImport(ObjCRuntime.Constants.ObjectiveCLibrary, EntryPoint = "objc_msgSend")]
    internal extern static IntPtr IntPtr_objc_msgSend(IntPtr receiver, IntPtr selector, UISemanticContentAttribute arg1);


    //
    // This method is invoked when the application has loaded and is ready to run. In this 
    // method you should instantiate the window, load the UI into it and then make the window
    // visible.
    //
    // You have 17 seconds to return from this method, or iOS will terminate your application.
    //
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        global::Xamarin.Forms.Forms.SetFlags("Shell_Experimental", "Visual_Experimental", "CollectionView_Experimental", "FastRenderers_Experimental");
        global::Xamarin.Forms.Forms.Init();
        LoadApplication(new App());

        ...

        var selector = new ObjCRuntime.Selector("setSemanticContentAttribute:");
        IntPtr_objc_msgSend(UIView.Appearance.Handle, selector.Handle, UISemanticContentAttribute.ForceRightToLeft);

        return base.FinishedLaunching(app, options);
    }
}

Upvotes: 0

Marwan Alqadi
Marwan Alqadi

Reputation: 855

you can use this since iOS 11:

extension UICollectionViewFlowLayout {

    open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
        return true
    }

}

Upvotes: 30

cohen72
cohen72

Reputation: 2988

By changing both flipsHorizontallyInOppositeLayoutDirection and developmentLayoutDirection, I was able to achieve a full screen RTL scroll where the first item was all the way to the right, and to reach the last cell, user will need to scroll to the left.

Like so:

class RTLCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override var flipsHorizontallyInOppositeLayoutDirection: Bool {
        return true
    }

    override var developmentLayoutDirection: UIUserInterfaceLayoutDirection {
        return UIUserInterfaceLayoutDirection.rightToLeft
    }
}

Upvotes: 28

Robert Chen
Robert Chen

Reputation: 5355

As of iOS 9, Collection Views support RTL according to this WWDC video. So it's no longer necessary to create an RTL flow layout (unless you're already using a custom layout).

Select: Edit Scheme... > Options > Run > Application Language > Right to Left Pseudolanguage

enter image description here

When you build to Simulator, text will be right-aligned, and your Collection View will be ordered from Right to Left.

enter image description here

There's a problem though. When contentOffset.x == 0, the Collection View scroll position is at the Left edge (wrong) instead of the Right edge (correct). See this stack article for details.

One workaround is to simply scroll the First item to the .Left (There's a gotcha -- .Left is actually on the Right, or Leading edge):

override func viewDidAppear(animated: Bool) {
    if collectionView?.numberOfItemsInSection(0) > 0  {
        let indexPath = NSIndexPath(forItem: 0, inSection: 0)
        collectionView?.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: false)
    }
}

In my test project, my Collection View was nested inside a Table View Cell, so I didn't have access to viewDidAppear(). So instead, I ended up hooking into drawRect():

class CategoryRow : UITableViewCell {
    @IBOutlet weak var collectionView: UICollectionView!

    override func drawRect(rect: CGRect) {
        super.drawRect(rect)
        scrollToBeginning()
    }

    override func prepareForReuse() {
        scrollToBeginning()
    }

    func scrollToBeginning() {
        guard collectionView.numberOfItems(inSection: 0) > 0 else { return }
        let indexPath = IndexPath(item: 0, section: 0)
        collectionView.scrollToItem(at: indexPath, at: .left, animated: false)
    }
}

To see this in action, check out the RTL branch on this git repo. And for context, see this blog post and its comments.

enter image description here

Upvotes: 21

Tawfik Bouabid
Tawfik Bouabid

Reputation: 1292

Without doing any Xtransform to the collection view, simply forced RTL:

YourCollectionView.semanticContentAttribute = UISemanticContentAttribute.forceRightToLeft

Upvotes: 111

Saqib Omer
Saqib Omer

Reputation: 5477

For anybody trying to achieve Right to Left layout of UICollectionView in Swift

//in viewDidLoad
    YourCollectionView.transform = CGAffineTransform(scaleX: -1.0, y: 1.0)

//in cellForItemAtIndexPath
    cell.transform = CGAffineTransform(scaleX: -1.0, y: 1.0)

Upvotes: 23

medvedNick
medvedNick

Reputation: 4510

In addition to Tawfik's answer:

You can also set UICollectionView's Semantic property via Interface Builder:

Storyboard

More about this property: in this question

Upvotes: 35

LK__
LK__

Reputation: 6723

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)];

Upvotes: 93

rebello95
rebello95

Reputation: 8576

For anyone who has the same question:

I ended up using the UICollectionViewRightAlignedLayout library that @chinttu-roxen-ramani recommended. You can either set it with code:

self.collectionView.collectionViewLayout = [[UICollectionViewRightAlignedLayout alloc] init];

or through interface builder:

Through interface builder

I ended up making a couple modifications to the library, but overall it works great.

Upvotes: 7

Related Questions