By default, when you are using the flow layout in a collection view, cells are centered vertically. Is there a way to change this alignment ?
following code worked for me
@interface TopAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout
- (void)alignToTopForSameLineElements:(NSArray *)sameLineElements;
@end
@implementation TopAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
{
NSArray *attrs = [super layoutAttributesForElementsInRect:rect];
CGFloat baseline = -2;
NSMutableArray *sameLineElements = [NSMutableArray array];
for (UICollectionViewLayoutAttributes *element in attrs) {
if (element.representedElementCategory == UICollectionElementCategoryCell) {
CGRect frame = element.frame;
CGFloat centerY = CGRectGetMidY(frame);
if (ABS(centerY - baseline) > 1) {
baseline = centerY;
[self alignToTopForSameLineElements:sameLineElements];
[sameLineElements removeAllObjects];
}
[sameLineElements addObject:element];
}
}
[self alignToTopForSameLineElements:sameLineElements];//align one more time for the last line
return attrs;
}
- (void)alignToTopForSameLineElements:(NSArray *)sameLineElements
{
if (sameLineElements.count == 0) {
return;
}
NSArray *sorted = [sameLineElements sortedArrayUsingComparator:^NSComparisonResult(UICollectionViewLayoutAttributes *obj1, UICollectionViewLayoutAttributes *obj2) {
CGFloat height1 = obj1.frame.size.height;
CGFloat height2 = obj2.frame.size.height;
CGFloat delta = height1 - height2;
return delta == 0. ? NSOrderedSame : ABS(delta)/delta;
}];
UICollectionViewLayoutAttributes *tallest = [sorted lastObject];
[sameLineElements enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *obj, NSUInteger idx, BOOL *stop) {
obj.frame = CGRectOffset(obj.frame, 0, tallest.frame.origin.y - obj.frame.origin.y);
}];
}
@end
alignToTopForSameLineElement
wouldn't be better to check if sameLineEments
is less or equal to 1? Since there is no need to align the element when it's only one for row. –
Promiscuous return delta != 0. ? NSOrderedSame : ABS(delta)/delta;
. Order matters. –
Backhouse Swift 4 with functional oriented approach:
class TopAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)?
.map { $0.copy() } as? [UICollectionViewLayoutAttributes]
attributes?
.reduce([CGFloat: (CGFloat, [UICollectionViewLayoutAttributes])]()) {
guard $1.representedElementCategory == .cell else { return $0 }
return $0.merging([ceil($1.center.y): ($1.frame.origin.y, [$1])]) {
($0.0 < $1.0 ? $0.0 : $1.0, $0.1 + $1.1)
}
}
.values.forEach { minY, line in
line.forEach {
$0.frame = $0.frame.offsetBy(
dx: 0,
dy: minY - $0.frame.origin.y
)
}
}
return attributes
}
}
following code worked for me
@interface TopAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout
- (void)alignToTopForSameLineElements:(NSArray *)sameLineElements;
@end
@implementation TopAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
{
NSArray *attrs = [super layoutAttributesForElementsInRect:rect];
CGFloat baseline = -2;
NSMutableArray *sameLineElements = [NSMutableArray array];
for (UICollectionViewLayoutAttributes *element in attrs) {
if (element.representedElementCategory == UICollectionElementCategoryCell) {
CGRect frame = element.frame;
CGFloat centerY = CGRectGetMidY(frame);
if (ABS(centerY - baseline) > 1) {
baseline = centerY;
[self alignToTopForSameLineElements:sameLineElements];
[sameLineElements removeAllObjects];
}
[sameLineElements addObject:element];
}
}
[self alignToTopForSameLineElements:sameLineElements];//align one more time for the last line
return attrs;
}
- (void)alignToTopForSameLineElements:(NSArray *)sameLineElements
{
if (sameLineElements.count == 0) {
return;
}
NSArray *sorted = [sameLineElements sortedArrayUsingComparator:^NSComparisonResult(UICollectionViewLayoutAttributes *obj1, UICollectionViewLayoutAttributes *obj2) {
CGFloat height1 = obj1.frame.size.height;
CGFloat height2 = obj2.frame.size.height;
CGFloat delta = height1 - height2;
return delta == 0. ? NSOrderedSame : ABS(delta)/delta;
}];
UICollectionViewLayoutAttributes *tallest = [sorted lastObject];
[sameLineElements enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *obj, NSUInteger idx, BOOL *stop) {
obj.frame = CGRectOffset(obj.frame, 0, tallest.frame.origin.y - obj.frame.origin.y);
}];
}
@end
alignToTopForSameLineElement
wouldn't be better to check if sameLineEments
is less or equal to 1? Since there is no need to align the element when it's only one for row. –
Promiscuous return delta != 0. ? NSOrderedSame : ABS(delta)/delta;
. Order matters. –
Backhouse @DongXu: Your solution worked for me too. Here is the SWIFT version if it:
class TopAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout
{
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
if let attrs = super.layoutAttributesForElementsInRect(rect)
{
var baseline: CGFloat = -2
var sameLineElements = [UICollectionViewLayoutAttributes]()
for element in attrs
{
if element.representedElementCategory == .Cell
{
let frame = element.frame
let centerY = CGRectGetMidY(frame)
if abs(centerY - baseline) > 1
{
baseline = centerY
TopAlignedCollectionViewFlowLayout.alignToTopForSameLineElements(sameLineElements)
sameLineElements.removeAll()
}
sameLineElements.append(element)
}
}
TopAlignedCollectionViewFlowLayout.alignToTopForSameLineElements(sameLineElements) // align one more time for the last line
return attrs
}
return nil
}
private class func alignToTopForSameLineElements(sameLineElements: [UICollectionViewLayoutAttributes])
{
if sameLineElements.count < 1
{
return
}
let sorted = sameLineElements.sort { (obj1: UICollectionViewLayoutAttributes, obj2: UICollectionViewLayoutAttributes) -> Bool in
let height1 = obj1.frame.size.height
let height2 = obj2.frame.size.height
let delta = height1 - height2
return delta <= 0
}
if let tallest = sorted.last
{
for obj in sameLineElements
{
obj.frame = CGRectOffset(obj.frame, 0, tallest.frame.origin.y - obj.frame.origin.y)
}
}
}
}
if
clause inside of the for loop like this: for element in attrs where element.representedElementCategory == .cell { . . . } –
Algebra Swift 3 Version in case someone just wants to Copy & Paste:
class TopAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if let attrs = super.layoutAttributesForElements(in: rect) {
var baseline: CGFloat = -2
var sameLineElements = [UICollectionViewLayoutAttributes]()
for element in attrs {
if element.representedElementCategory == .cell {
let frame = element.frame
let centerY = frame.midY
if abs(centerY - baseline) > 1 {
baseline = centerY
alignToTopForSameLineElements(sameLineElements: sameLineElements)
sameLineElements.removeAll()
}
sameLineElements.append(element)
}
}
alignToTopForSameLineElements(sameLineElements: sameLineElements) // align one more time for the last line
return attrs
}
return nil
}
private func alignToTopForSameLineElements(sameLineElements: [UICollectionViewLayoutAttributes]) {
if sameLineElements.count < 1 { return }
let sorted = sameLineElements.sorted { (obj1: UICollectionViewLayoutAttributes, obj2: UICollectionViewLayoutAttributes) -> Bool in
let height1 = obj1.frame.size.height
let height2 = obj2.frame.size.height
let delta = height1 - height2
return delta <= 0
}
if let tallest = sorted.last {
for obj in sameLineElements {
obj.frame = obj.frame.offsetBy(dx: 0, dy: tallest.frame.origin.y - obj.frame.origin.y)
}
}
}
}
The awesome top voted answer from Fabio Felici needs updating for iOS 15 and 16. On iOS 16 it will crash almost every time you scroll.
Here's a more thorough solution that implements layoutAttributesForItem
to ensure that attributes are the same for all execution paths (to avoid a looping crash).
open class TopAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else { return nil }
guard layoutAttributes.representedElementCategory == .cell else { return layoutAttributes }
func layoutAttributesForRow() -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return [layoutAttributes] }
let contentWidth = collectionView.frame.size.width - sectionInset.left - sectionInset.right
var rowFrame = layoutAttributes.frame
rowFrame.origin.x = sectionInset.left
rowFrame.size.width = contentWidth
return super.layoutAttributesForElements(in: rowFrame)
}
let minYs = minimumYs(from: layoutAttributesForRow())
guard let minY = minYs[layoutAttributes.indexPath] else { return layoutAttributes }
layoutAttributes.frame = layoutAttributes.frame.offsetBy(dx: 0, dy: minY - layoutAttributes.frame.origin.y)
return layoutAttributes
}
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)?
.map { $0.copy() } as? [UICollectionViewLayoutAttributes]
let minimumYs = minimumYs(from: attributes)
attributes?.forEach {
guard $0.representedElementCategory == .cell else { return }
guard let minimumY = minimumYs[$0.indexPath] else { return }
$0.frame = $0.frame.offsetBy(dx: 0, dy: minimumY - $0.frame.origin.y)
}
return attributes
}
/// Returns the minimum Y values based for each index path.
private func minimumYs(from layoutAttributes: [UICollectionViewLayoutAttributes]?) -> [IndexPath: CGFloat] {
layoutAttributes?
.reduce([CGFloat: (CGFloat, [UICollectionViewLayoutAttributes])]()) {
guard $1.representedElementCategory == .cell else { return $0 }
return $0.merging([ceil($1.center.y): ($1.frame.origin.y, [$1])]) {
($0.0 < $1.0 ? $0.0 : $1.0, $0.1 + $1.1)
}
}
.values.reduce(into: [IndexPath: CGFloat]()) { result, line in
line.1.forEach { result[$0.indexPath] = line.0 }
} ?? [:]
}
}
I have used something similar to the before answers. In my case I want to align cells by colum with different heights.
import UIKit
class AlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if let attributes = super.layoutAttributesForElements(in: rect) {
let sectionElements: [Int : [UICollectionViewLayoutAttributes]] = attributes
.filter {
return $0.representedElementCategory == .cell //take cells only
}.groupBy {
return $0.indexPath.section //group attributes by section
}
sectionElements.forEach { (section, elements) in
//get suplementary view (header) to align each section
let suplementaryView = attributes.first {
return $0.representedElementCategory == .supplementaryView && $0.indexPath.section == section
}
//call align method
alignToTopSameSectionElements(elements, with: suplementaryView)
}
return attributes
}
return super.layoutAttributesForElements(in: rect)
}
private func alignToTopSameSectionElements(_ elements: [UICollectionViewLayoutAttributes], with suplementaryView: UICollectionViewLayoutAttributes?) {
//group attributes by colum
let columElements: [Int : [UICollectionViewLayoutAttributes]] = elements.groupBy {
return Int($0.frame.midX)
}
columElements.enumerated().forEach { (columIndex, object) in
let columElement = object.value.sorted {
return $0.indexPath < $1.indexPath
}
columElement.enumerated().forEach { (index, element) in
var frame = element.frame
if columIndex == 0 {
frame.origin.x = minimumLineSpacing
}
switch index {
case 0:
if let suplementaryView = suplementaryView {
frame.origin.y = suplementaryView.frame.maxY
}
default:
let beforeElement = columElement[index-1]
frame.origin.y = beforeElement.frame.maxY + minimumInteritemSpacing
}
element.frame = frame
}
}
}
}
public extension Array {
func groupBy <U> (groupingFunction group: (Element) -> U) -> [U: Array] {
var result = [U: Array]()
for item in self {
let groupKey = group(item)
if result.has(groupKey) {
result[groupKey]! += [item]
} else {
result[groupKey] = [item]
}
}
return result
}
}
This is the result of this layout:
This may or may not work for your particular situation, but I had some luck subclassing UICollectionViewFlowLayout
in the following way:
@interface CustomFlowLayout : UICollectionViewFlowLayout
@end
@implementation CustomFlowLayout
- (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];
currentItemAttributes.frame = CGRectOffset(currentItemAttributes.frame, 0, 0.5 * CGRectGetHeight(currentItemAttributes.frame));
return currentItemAttributes;
}
@end
@DongXu: Your solution worked for me too. Here is the Xamarin.iOS version if it:
public class TopAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout
{
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
if (base.LayoutAttributesForElementsInRect(rect) is UICollectionViewLayoutAttributes[] attrs)
{
// Find all the cells and group them together by the rows they appear on
var cellsGroupedByRow = attrs
.Where(attr => attr.RepresentedElementCategory == UICollectionElementCategory.Cell)
// The default flow layout aligns cells in the middle of the row.
// Thus, cells with the same Y center point are in the same row.
// Convert to int, otherwise float values can be slighty different for cells on the same row and cause bugs.
.GroupBy(attr => Convert.ToInt32(attr.Frame.GetMidY()));
foreach (var cellRowGroup in cellsGroupedByRow)
{
TopAlignCellsOnSameLine(cellRowGroup.ToArray());
}
return attrs;
}
return null;
}
private static void TopAlignCellsOnSameLine(UICollectionViewLayoutAttributes[] cells)
{
// If only 1 cell in the row its already top aligned.
if (cells.Length <= 1) return;
// The tallest cell has the correct Y value for all the other cells in the row
var tallestCell = cells.OrderByDescending(cell => cell.Frame.Height).First();
var topOfRow = tallestCell.Frame.Y;
foreach (var cell in cells)
{
if (cell.Frame.Y == topOfRow) continue;
var frame = cell.Frame;
frame.Y = topOfRow;
cell.Frame = frame;
}
}
}
I used https://cocoapods.org/pods/AlignedCollectionViewFlowLayout
Install the pod or simply add the file AlignedCollectionViewFlowLayout.swift to your project
In storyboard, select the Collection View's "Collection Layout" and assign the class AlignedCollectionViewFlowLayout
- In your View Controller's ViewDidLoad() function add:
let alignedFlowLayout = collectionView?.collectionViewLayout as? AlignedCollectionViewFlowLayout
alignedFlowLayout?.horizontalAlignment = .left
alignedFlowLayout?.verticalAlignment = .top
The UICollectionViewFlowLayout
class is derived from the UICollectionViewLayout
base class. And if you look at the documentation for that, you'll see there are a number of methods you can override, the most likely candidate being layoutAttributesForItemAtIndexPath:
.
If you override that method, you could let it call its super implementation, and then adjust the properties of the returned UICollectionViewLayoutAttributes
object. Specifically, you'll likely need to adjust the frame
property to reposition the item so it's no longer centered.
I used this code (https://github.com/yoeriboven/TopAlignedCollectionViewLayout) after DongXu's solution didn't quite work. The only modification was that it's originally designed to be used with a grid so I needed to instantiate the layout with an arbitrarily high column count...
let collectionViewFlowLayout = YBTopAlignedCollectionViewFlowLayout(numColumns: 1000)
@DongXu's answer is correct. However, I suggest to make those calculations in UICollectionViewFlowLayout
's prepare()
method. It will prevent multiple calculations on the same cell's attributes. Moreover, prepare()
is better place to manage attributes cache.
© 2022 - 2024 — McMap. All rights reserved.