Reputation: 8576
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:
Any help is greatly appreciated!
Upvotes: 74
Views: 57620
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
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
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
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
Reputation: 2799
For iOS 9+
Reverse your collectionView in viewDidLoad() with this :
myCollectionView.transform = CGAffineTransform(scaleX: -1, y: 1)
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
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.
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
Reputation: 1163
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true
}
}
Upvotes: 11
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
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
Reputation: 855
you can use this since iOS 11:
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true
}
}
Upvotes: 30
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
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
When you build to Simulator, text will be right-aligned, and your Collection View will be ordered from Right to Left.
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.
Upvotes: 21
Reputation: 1292
Without doing any Xtransform to the collection view, simply forced RTL:
YourCollectionView.semanticContentAttribute = UISemanticContentAttribute.forceRightToLeft
Upvotes: 111
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
Reputation: 4510
In addition to Tawfik's answer:
You can also set UICollectionView's Semantic
property via Interface Builder:
More about this property: in this question
Upvotes: 35
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
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:
I ended up making a couple modifications to the library, but overall it works great.
Upvotes: 7