[ios] UICollectionView에서 왼쪽 맞춤 셀

한 줄에 너비가 다른 여러 셀이있는 프로젝트에서 UICollectionView를 사용하고 있습니다. 에 따르면 :
https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

동일한 패딩을 사용하여 선 전체에 셀을 퍼뜨립니다. 이것은 예상대로 발생하지만 왼쪽 정렬하고 패딩 너비를 하드 코딩하고 싶습니다.

UICollectionViewFlowLayout을 하위 클래스로 만들어야한다고 생각하지만 온라인에서 튜토리얼 등을 읽은 후에는 이것이 어떻게 작동하는지 이해하지 못하는 것 같습니다.



답변

이 스레드의 다른 솔루션은 행이 하나의 항목으로 만 구성되거나 지나치게 복잡 할 때 제대로 작동하지 않습니다.

Ryan이 제시 한 예제를 기반으로 새 요소의 Y 위치를 검사하여 새 줄을 감지하도록 코드를 변경했습니다. 성능이 매우 간단하고 빠릅니다.

빠른:

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
    }
}

보조 뷰가 크기를 유지하도록하려면 forEach호출 의 클로저 맨 위에 다음을 추가합니다 .

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

목표 -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;
}


답변

이 질문에 대한 답변에는 많은 훌륭한 아이디어가 포함되어 있습니다. 그러나 대부분은 몇 가지 단점이 있습니다.

  • 셀의 y 값을 확인하지 않는 솔루션 은 한 줄 레이아웃에서만 작동합니다 . 여러 줄이있는 컬렉션 뷰 레이아웃에서는 실패합니다.
  • 솔루션 할 수는 체크 Y 와 같은 값 엔젤 가르시아 Olloqui의 대답은 모든 세포가 같은 높이가있는 경우에만 작업을 . 높이가 가변적 인 셀에서는 실패합니다.
  • 대부분의 솔루션은 layoutAttributesForElements(in rect: CGRect)함수 만 재정의합니다 . 그들은 재정의하지 않습니다 layoutAttributesForItem(at indexPath: IndexPath). 콜렉션 뷰가 주기적으로 후자의 함수를 호출하여 특정 인덱스 경로에 대한 레이아웃 속성을 검색하기 때문에 이는 문제입니다. 해당 함수에서 적절한 속성을 반환하지 않으면 모든 종류의 시각적 버그가 발생할 수 있습니다. 예를 들어 셀의 삽입 및 삭제 애니메이션 중 또는 컬렉션 뷰 레이아웃의 estimatedItemSize. 애플 문서의 상태 :

    모든 사용자 지정 레이아웃 개체는 layoutAttributesForItemAtIndexPath:메서드 를 구현해야 합니다.

  • 또한 많은 솔루션 rectlayoutAttributesForElements(in rect: CGRect)함수에 전달 되는 매개 변수 에 대해 가정 합니다. 예를 들어, 많은 사람들은는 rect항상 새 줄의 시작에서 시작 한다는 가정을 기반으로합니다 .

즉,

이 페이지에 제안 된 대부분의 솔루션은 일부 특정 응용 프로그램에서 작동하지만 모든 상황에서 예상대로 작동하지 않습니다.


AlignedCollectionViewFlowLayout

이러한 문제를 해결하기 위해 유사한 질문에 대한 답변에서 MattChris WagnerUICollectionViewFlowLayout제안한 유사한 아이디어를 따르는 하위 ​​클래스를 만들었습니다 . 셀을 정렬 할 수 있습니다.

⬅︎ 왼쪽 :

왼쪽 정렬 레이아웃

또는 ➡︎ 오른쪽 :

오른쪽 정렬 레이아웃

또한 각 행의 셀 을 세로로 정렬하는 옵션을 제공합니다 (높이가 다른 경우).

여기에서 간단히 다운로드 할 수 있습니다.

https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

사용법은 간단하며 README 파일에 설명되어 있습니다. 기본적으로의 인스턴스를 만들고 AlignedCollectionViewFlowLayout원하는 정렬을 지정한 다음 컬렉션 뷰의 collectionViewLayout속성에 할당합니다 .

 let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left,
                                                         verticalAlignment: .top)

 yourCollectionView.collectionViewLayout = alignedFlowLayout

( Cocoapods 에서도 사용할 수 있습니다 .)


작동 방식 (왼쪽 맞춤 셀) :

여기서 개념은 오로지 layoutAttributesForItem(at indexPath: IndexPath)기능 에만 의존 하는 입니다. 에서는 layoutAttributesForElements(in rect: CGRect)단순히 모든 셀의 rect인덱스 경로를 가져온 다음 모든 인덱스 경로에 대해 첫 번째 함수를 호출하여 올바른 프레임을 검색합니다.

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

    // We may not change the original layout attributes 
    // or UICollectionViewFlowLayout might complain.
    let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect))

    layoutAttributesObjects?.forEach({ (layoutAttributes) in
        if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
            let indexPath = layoutAttributes.indexPath
            // Retrieve the correct frame from layoutAttributesForItem(at: indexPath):
            if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
                layoutAttributes.frame = newFrame
            }
        }
    })

    return layoutAttributesObjects
}

(이 copy()함수는 단순히 배열에있는 모든 레이아웃 속성의 전체 복사본을 생성합니다. 구현을 위해 소스 코드 를 살펴볼 수 있습니다 .)

이제 우리가해야 할 일은 layoutAttributesForItem(at indexPath: IndexPath)함수를 올바르게 구현하는 것뿐입니다 . 수퍼 클래스는 UICollectionViewFlowLayout이미 각 줄에 올바른 수의 셀을 배치하므로 해당 행 내에서 왼쪽으로 이동하기 만하면됩니다. 어려움은 각 셀을 왼쪽으로 이동하는 데 필요한 공간의 양을 계산하는 데 있습니다.

셀 사이에 고정 된 간격을두고 싶기 때문에 핵심 아이디어는 이전 셀 (현재 배치 된 셀의 왼쪽 셀)이 이미 적절하게 배치되었다고 가정하는 것입니다. 그런 다음 maxX이전 셀의 프레임 값에 셀 간격을 추가하기 만하면됩니다 .origin.x 됩니다. 이것이 현재 셀의 프레임 값입니다.

이제 우리는 줄의 시작 부분에 도달했을 때만 알면되므로 이전 줄의 셀 옆에 셀을 정렬하지 않습니다. (이는 잘못된 레이아웃을 초래할뿐만 아니라 매우 지연 될 수 있습니다.) 따라서 재귀 앵커가 필요합니다. 재귀 앵커를 찾는 데 사용하는 접근 방식은 다음과 같습니다.

인덱스 i 의 셀이 인덱스 i-1 의 셀과 같은 줄에 있는지 확인하려면 …

 +---------+----------------------------------------------------------------+---------+
 |         |                                                                |         |
 |         |     +------------+                                             |         |
 |         |     |            |                                             |         |
 | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
 |  inset  |     |intersection|        |                     |   line rect  |  inset  |
 |         |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -|         |
 | (left)  |     |            |             current item                    | (right) |
 |         |     +------------+                                             |         |
 |         |     previous item                                              |         |
 +---------+----------------------------------------------------------------+---------+

… 현재 셀 주위에 사각형을 “그리고”전체 컬렉션 뷰의 너비에 걸쳐 늘립니다. 은 UICollectionViewFlowLayout같은 라인에있는 모든 셀을 수직으로 모든 세포를 중심으로 해야 이 사각형과 교차.

따라서 인덱스 i-1을 가진 셀이 인덱스 i 인 셀에서 생성 된이 선 사각형과 교차 하는지 간단히 확인합니다 .

  • 교차하는 경우 인덱스 i 가있는 셀은 줄에서 가장 왼쪽 셀이 아닙니다.
    → 이전 셀의 프레임 (인덱스 i−1 사용 )을 가져와 현재 셀을 옆으로 이동합니다.

  • 교차하지 않는 경우 인덱스 i 가있는 셀이 줄에서 가장 왼쪽에있는 셀입니다.
    → 셀을 컬렉션보기의 왼쪽 가장자리로 이동합니다 (세로 위치는 변경하지 않음).

layoutAttributesForItem(at indexPath: IndexPath)가장 중요한 부분은 아이디어 를 이해하는 것이고 소스 코드 에서 내 구현을 항상 확인할 수 있기 때문에 여기 에 함수 의 실제 구현을 게시하지 않겠습니다 . ( .right정렬 및 다양한 수직 정렬 옵션 도 허용하기 때문에 여기에 설명 된 것보다 조금 더 복잡 합니다. 그러나 동일한 아이디어를 따릅니다.)


와우, 이것이 내가 Stackoverflow에 쓴 가장 긴 답변이라고 생각합니다. 이게 도움이 되길 바란다. ?


답변

Swift 4.1 및 iOS 11을 사용하면 필요에 따라 문제를 해결하기 위해 다음 두 가지 완전한 구현 중 하나를 선택할 수 있습니다 .


#1. 왼쪽 맞춤 자동 크기 조정 UICollectionViewCells

아래 구현은의 자동 크기 조정 셀을 왼쪽 정렬하기 위해 UICollectionViewLayout‘s layoutAttributesForElements(in:), UICollectionViewFlowLayout‘s estimatedItemSizeUILabel‘ 를 사용하는 방법을 보여줍니다 .preferredMaxLayoutWidthUICollectionView

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]

    let columnLayout = FlowLayout(
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = columnLayout
        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return array.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
        cell.label.text = array[indexPath.row]
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .orange
        label.preferredMaxLayoutWidth = 120
        label.numberOfLines = 0

        contentView.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
        contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
        contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
        contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

예상 결과:

여기에 이미지 설명 입력


# 2. UICollectionViewCell고정 된 크기로 왼쪽 정렬 s

아래 구현은의 미리 정의 된 크기로 셀을 왼쪽 정렬하기 위해 UICollectionViewLayoutlayoutAttributesForElements(in:)UICollectionViewFlowLayout의 사용 방법을 보여줍니다 .itemSizeUICollectionView

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let columnLayout = FlowLayout(
        itemSize: CGSize(width: 140, height: 140),
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = columnLayout
        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 7
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        self.itemSize = itemSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .cyan
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

예상 결과:

여기에 이미지 설명 입력


답변

2019 년의 간단한 솔루션

이것은 수년 동안 상황이 많이 변한 우울한 질문 중 하나입니다. 이제 쉽습니다.

기본적으로 다음과 같이합니다.

    // as you move across one row ...
    a.frame.origin.x = x
    x += a.frame.width + minimumInteritemSpacing
    // and, obviously start fresh again each row

이제 필요한 것은 상용구 코드입니다.

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

    guard let att = super.layoutAttributesForElements(in: rect) else { return [] }
    var x: CGFloat = sectionInset.left
    var y: CGFloat = -1.0

    for a in att {
        if a.representedElementCategory != .cell { continue }

        if a.frame.origin.y >= y { x = sectionInset.left }

        a.frame.origin.x = x
        x += a.frame.width + minimumInteritemSpacing

        y = a.frame.maxY
    }
    return att
}

복사하여 붙여 넣기 만하면됩니다. UICollectionViewFlowLayout 됩니다.

복사 및 붙여 넣기를위한 전체 작업 솔루션 :

이것이 전부입니다.

class TagsLayout: UICollectionViewFlowLayout {

    required override init() {super.init(); common()}
    required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder); common()}

    private func common() {
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        minimumLineSpacing = 10
        minimumInteritemSpacing = 10
    }

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

        guard let att = super.layoutAttributesForElements(in:rect) else {return []}
        var x: CGFloat = sectionInset.left
        var y: CGFloat = -1.0

        for a in att {
            if a.representedElementCategory != .cell { continue }

            if a.frame.origin.y >= y { x = sectionInset.left }
            a.frame.origin.x = x
            x += a.frame.width + minimumInteritemSpacing
            y = a.frame.maxY
        }
        return att
    }
}

여기에 이미지 설명 입력

그리고 마지막으로…

처음으로 이것을 명확히 한 위의 @AlexShubin에게 감사드립니다!


답변

질문이 한동안 올라 왔지만 답이없고 좋은 질문입니다. 대답은 UICollectionViewFlowLayout 하위 클래스에서 하나의 메서드를 재정의하는 것입니다.

@implementation MYFlowLayoutSubclass

//Note, the layout's minimumInteritemSpacing (default 10.0) should not be less than this. 
#define ITEM_SPACING 10.0f

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];

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

    //this loop assumes attributes are in IndexPath order
    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        if (attributes.frame.origin.x == self.sectionInset.left) {
            leftMargin = self.sectionInset.left; //will add outside loop
        } else {
            CGRect newLeftAlignedFrame = attributes.frame;
            newLeftAlignedFrame.origin.x = leftMargin;
            attributes.frame = newLeftAlignedFrame;
        }

        leftMargin += attributes.frame.size.width + ITEM_SPACING;
        [newAttributesForElementsInRect addObject:attributes];
    }

    return newAttributesForElementsInRect;
}

@end

Apple에서 권장하는대로 super에서 레이아웃 속성을 가져 와서 반복합니다. 행의 첫 번째 경우 (origin.x가 왼쪽 여백에있는 것으로 정의 됨), 그대로두고 x를 0으로 재설정합니다. 그런 다음 첫 번째 셀과 모든 셀에 대해 해당 셀의 너비와 약간의 여백을 추가합니다. 이것은 루프의 다음 항목으로 전달됩니다. 첫 번째 항목이 아닌 경우 origin.x를 실행중인 계산 된 여백으로 설정하고 배열에 새 요소를 추가합니다.


답변

나는 같은 문제가 있었다, Cocoapod UICollectionViewLeftAlignedLayout 을 시도해보십시오. 프로젝트에 포함하고 다음과 같이 초기화하십시오.

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];


답변

Michael Sand의 답변을 바탕으로 UICollectionViewFlowLayout왼쪽, 오른쪽 또는 전체 (기본적으로 기본) 수평 정렬을 수행 하는 하위 클래스 라이브러리를 만들었습니다. 또한 각 셀 사이의 절대 거리를 설정할 수도 있습니다. 수평 중심 정렬과 수직 정렬도 추가 할 계획입니다.

https://github.com/eroth/ERJustifiedFlowLayout