UICollectionView에서 왼쪽 맞춤 셀


96

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

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

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


답변:


169

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

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

4
감사! 배열의 속성을 복사하여 수정 된 경고를 let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
받았습니다.

2
내가했던 것처럼 중앙 정렬 버전을 찾는 것을 우연히 발견했을 수있는 사람을 위해 Angel의 답변 수정 버전을 여기에 게시했습니다. stackoverflow.com/a/38254368/2845237
Alex Koshy

이 솔루션을 시도했지만 컬렉션 뷰의 범위를 초과하는 컬렉션 뷰의 오른쪽에있는 일부 셀이 있습니다. 다른 사람이 이것을 만났습니까?
Happiehappie

@Happiehappie 예, 저도 그 행동을 가지고 있습니다. 어떤 수정?
John Baum 2011

이 솔루션을 시도했지만 인터페이스 빌더 유틸리티 패널에서 레이아웃을 collectionView와 연결할 수 없습니다. 코드로 끝냈습니다. 이유는 무엇입니까?
acrespo

63

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

  • 셀의 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에 쓴 가장 긴 답변이라고 생각합니다. 이게 도움이 되길 바란다. 😉


이 시나리오로 신청합니다. stackoverflow.com/questions/56349052/… 하지만 작동하지 않습니다. 어떻게
이걸

이것으로도 (정말 멋진 프로젝트) 내 앱의 수직 스크롤 컬렉션 뷰에서 깜박이는 경험이 있습니다. 20 개의 셀이 있고 첫 번째 스크롤에서 첫 번째 셀이 비어 있고 모든 셀이 오른쪽으로 한 위치 이동합니다. 위아래로 한 번 전체 스크롤하면 제대로 정렬됩니다.
Parth Tamane

멋진 프로젝트! Carthage에서 활성화되는 것을 보면 좋을 것입니다.
pjuzeliunas

30

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

}

예상 결과:

여기에 이미지 설명 입력


행별로 셀 속성을 그룹화 할 때 10임의의 숫자를 사용합니까?
amariduran

25

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에게 감사드립니다!


야. 조금 혼란 스러워요. 시작과 끝에서 모든 문자열에 공백을 추가하면 추가 한 공백없이 항목 셀에 내 단어가 표시됩니다. 어떻게해야하는지 아십니까?
Mohamed Lee

컬렉션 뷰에 섹션 헤더가 있으면 죽이는 것 같습니다.
TylerJames

22

질문이 한동안 올라 왔지만 답이없고 좋은 질문입니다. 대답은 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를 실행중인 계산 된 여백으로 설정하고 배열에 새 요소를 추가합니다.


링크 된 질문에 대한 Chris Wagner의 솔루션 ( stackoverflow.com/a/15554667/482557 ) 보다 이것이 더 나은 방법이라고 생각 합니다. 구현에는 반복적 인 UICollectionViewLayout 호출이 없습니다.
Evan R

1
UICollectionViewFlowLayout이 항목을 중앙에 배치하기 때문에 행에 단일 항목이있는 경우 작동하는 것처럼 보이지 않습니다. 문제 해결을 위해 센터를 확인할 수 있습니다. 같은 것attribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos 2015-07-20

나는 이것을 구현했지만 어떤 이유로 첫 번째 섹션에서 중단됩니다. 첫 번째 셀은 첫 번째 행의 왼쪽에 나타나고 두 번째 셀 (동일한 행에 맞지 않음)은 다음 행으로 이동하지만 왼쪽은 아닙니다. 정렬. 대신 x 위치는 첫 번째 셀이 끝나는 곳에서 시작됩니다.
Nicolas Miari 2015 년

@NicolasMiari 루프는 새 행을 결정 leftMargin하고 if (attributes.frame.origin.x == self.sectionInset.left) {확인으로 재설정합니다 . 이것은 기본 레이아웃이 새 행의 셀을 sectionInset.left. 그렇지 않은 경우 설명하는 동작이 발생합니다. 루프 내부에 중단 점을 삽입하고 비교 직전에 두 번째 행의 셀에 대해 양쪽을 확인합니다. 행운을 빕니다.
Mike Sand

3
감사합니다 ... UICollectionViewLeftAlignedLayout : github.com/mokagio/UICollectionViewLeftAlignedLayout을 사용하게 되었습니다 . 몇 가지 추가 메서드를 재정의합니다.
Nicolas Miari 2015 년

9

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

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

이 레이아웃의 당신의 초기화에 여는 대괄호 누락
RyanOfCourse

두 가지 답변 github.com/eroth/ERJustifiedFlowLayout 및 UICollectionViewLeftAlignedLayout을 테스트했습니다 . EstimatedItemSize (자체 크기 조정)를 사용할 때 두 번째 만 올바른 결과를 생성했습니다.
EckhardN


5

다음은 Swift의 원래 답변입니다. 여전히 대부분 잘 작동합니다.

class LeftAlignedFlowLayout: UICollectionViewFlowLayout {

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

        var leftMargin = sectionInset.left

        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
        }

        return attributes
    }
}

예외 : 셀 자동 크기 조정

슬프게도 큰 예외가 하나 있습니다. 당신이 사용하는 경우 UICollectionViewFlowLayoutestimatedItemSize. 내부적으로 UICollectionViewFlowLayout는 약간의 변화가 있습니다. 나는 그것을 완전히 추적하지 않았지만 layoutAttributesForElementsInRect셀 크기를 조정하는 동안 다른 메서드를 호출하는 것은 분명합니다 . 시행 착오를 통해 layoutAttributesForItemAtIndexPath자동 크기 조정 중에 각 셀을 개별적으로 더 자주 호출하는 것 같습니다 . 이 업데이트 LeftAlignedFlowLayoutestimatedItemSize. 정적 크기의 셀에서도 작동하지만 추가 레이아웃 호출로 인해 셀 자동 크기 조정이 필요하지 않을 때마다 원래 답변을 사용하게됩니다.

class LeftAlignedFlowLayout: UICollectionViewFlowLayout {

    private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let layoutAttribute = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes

        // First in a row.
        if layoutAttribute?.frame.origin.x == sectionInset.left {
            return layoutAttribute
        }

        // We need to align it to the previous item.
        let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
        guard let previousLayoutAttribute = self.layoutAttributesForItemAtIndexPath(previousIndexPath) else {
            return layoutAttribute
        }

        layoutAttribute?.frame.origin.x = previousLayoutAttribute.frame.maxX + self.minimumInteritemSpacing

        return layoutAttribute
    }
}

이것은 첫 번째 섹션에서 작동하지만 테이블이 스크롤 될 때까지 두 번째 섹션에서 레이블 크기가 조정되지 않습니다. 내 컬렉션 뷰는 테이블 셀에 있습니다.
EI Captain v2.0

5

신속하게. Michaels 답변에 따르면

override func layoutAttributesForElementsInRect(rect: CGRect) ->     [UICollectionViewLayoutAttributes]? {
    guard let oldAttributes = super.layoutAttributesForElementsInRect(rect) else {
        return super.layoutAttributesForElementsInRect(rect)
    }
    let spacing = CGFloat(50) // REPLACE WITH WHAT SPACING YOU NEED
    var newAttributes = [UICollectionViewLayoutAttributes]()
    var leftMargin = self.sectionInset.left
    for attributes in oldAttributes {
        if (attributes.frame.origin.x == self.sectionInset.left) {
            leftMargin = self.sectionInset.left
        } else {
            var newLeftAlignedFrame = attributes.frame
            newLeftAlignedFrame.origin.x = leftMargin
            attributes.frame = newLeftAlignedFrame
        }

        leftMargin += attributes.frame.width + spacing
        newAttributes.append(attributes)
    }
    return newAttributes
}

4

모든 답변을 바탕으로 조금 변하고 잘 작동합니다.

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
                   || layoutAttribute.frame.origin.x == sectionInset.left {
            leftMargin = sectionInset.left
        }

        if layoutAttribute.frame.origin.x == sectionInset.left {
            leftMargin = sectionInset.left
        }
        else {
            layoutAttribute.frame.origin.x = leftMargin
        }

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

    return attributes
}

4

당신 중 누군가가 문제에 직면하는 경우- 컬렉션 뷰의 오른쪽에있는 일부 셀이 컬렉션 뷰의 경계를 초과합니다 . 그런 다음 이것을 사용하십시오-

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {

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

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

            layoutAttribute.frame.origin.x = leftMargin

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

CGFloat 값 을 비교하는 대신 INT 를 사용하십시오 .


3

여기의 답변을 기반으로하지만 컬렉션 뷰에 머리글이나 바닥 글이있을 때 충돌 및 정렬 문제가 수정되었습니다. 왼쪽 만 셀 정렬 :

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

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

        var leftMargin = sectionInset.left
        var prevMaxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in

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

            if layoutAttribute.frame.origin.y >= prevMaxY {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            prevMaxY = layoutAttribute.frame.maxY
        }

        return attributes
    }
}

2

Michael Sand의 답변에 감사드립니다 . 여러 행의 솔루션 (각 행의 동일한 정렬 Top y)으로 수정했습니다. 즉, 각 항목에 대한 간격도 왼쪽 정렬입니다.

static CGFloat const ITEM_SPACING = 10.0f;

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    CGRect contentRect = {CGPointZero, self.collectionViewContentSize};

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

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
    NSMutableDictionary *leftMarginDictionary = [[NSMutableDictionary alloc] init];

    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        UICollectionViewLayoutAttributes *attr = attributes.copy;

        CGFloat lastLeftMargin = [[leftMarginDictionary valueForKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]] floatValue];
        if (lastLeftMargin == 0) lastLeftMargin = leftMargin;

        CGRect newLeftAlignedFrame = attr.frame;
        newLeftAlignedFrame.origin.x = lastLeftMargin;
        attr.frame = newLeftAlignedFrame;

        lastLeftMargin += attr.frame.size.width + ITEM_SPACING;
        [leftMarginDictionary setObject:@(lastLeftMargin) forKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]];
        [newAttributesForElementsInRect addObject:attr];
    }

    return newAttributesForElementsInRect;
}

1

Swift 5에서 작동하는 최고의 코드를 찾기위한 여정은 다음과 같습니다.이 스레드와 다른 스레드의 몇 가지 답변에 참여하여 직면 한 경고와 문제를 해결했습니다. 컬렉션 뷰를 스크롤 할 때 경고와 비정상적인 동작이 발생했습니다. 콘솔은 다음을 인쇄합니다.

이는 흐름 레이아웃 "xyz"가 UICollectionViewFlowLayout에서 반환 한 속성을 복사하지 않고 수정하기 때문에 발생할 수 있습니다.

내가 직면 한 또 다른 문제는 화면 오른쪽에서 일부 긴 셀이 잘리는 것입니다. 또한 대리자 함수에서 섹션 삽입과 minimumInteritemSpacing을 설정하여 값이 사용자 정의 클래스에 반영되지 않았습니다. 이에 대한 수정 사항은 컬렉션 뷰에 적용하기 전에 해당 속성을 레이아웃의 인스턴스로 설정하는 것이 었습니다.

컬렉션보기에 레이아웃을 사용하는 방법은 다음과 같습니다.

let layout = LeftAlignedCollectionViewFlowLayout()
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 7.5
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
super.init(frame: frame, collectionViewLayout: layout)

다음은 흐름 레이아웃 클래스입니다.

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }

        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if Int(layoutAttribute.frame.origin.y) >= Int(maxY) || layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }

            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

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

        return attributes
    }
}

당신은 나의 하루를 구했습니다
David 'mArm'Ansermot

0

UICollectionView의 문제점은 사용 가능한 영역에 셀을 자동으로 맞추려고한다는 것입니다. 먼저 행과 열의 수를 정의한 다음 해당 행과 열의 셀 크기를 정의하여이 작업을 수행했습니다.

1) 내 UICollectionView의 섹션 (행)을 정의하려면 :

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) 섹션의 항목 수를 정의합니다. 섹션마다 다른 수의 항목을 정의 할 수 있습니다. 'section'매개 변수를 사용하여 섹션 번호를 얻을 수 있습니다.

(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

3) 각 섹션과 행의 셀 크기를 개별적으로 정의합니다. 당신은 섹션 번호와 'indexPath'매개 변수 예를 사용하여 행 번호를 얻을 수있는 [indexPath section]섹션 번호와 [indexPath row]행 번호를.

(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

4) 다음을 사용하여 행과 섹션에 셀을 표시 할 수 있습니다.

(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

참고 : UICollectionView에서

Section == Row
IndexPath.Row == Column

0

Mike Sand의 대답은 좋지만이 코드에서 몇 가지 문제가 발생했습니다 (긴 셀이 잘린 것처럼). 그리고 새로운 코드 :

#define ITEM_SPACE 7.0f

@implementation LeftAlignedCollectionViewFlowLayout
- (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];

    UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];

    if (indexPath.item == 0) { // first item of section
        CGRect frame = currentItemAttributes.frame;
        frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
        currentItemAttributes.frame = frame;

        return currentItemAttributes;
    }

    NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
    CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width + ITEM_SPACE;

    CGRect currentFrame = currentItemAttributes.frame;
    CGRect strecthedCurrentFrame = CGRectMake(0,
                                              currentFrame.origin.y,
                                              self.collectionView.frame.size.width,
                                              currentFrame.size.height);

    if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
        // the approach here is to take the current frame, left align it to the edge of the view
        // then stretch it the width of the collection view, if it intersects with the previous frame then that means it
        // is on the same line, otherwise it is on it's own new line
        CGRect frame = currentItemAttributes.frame;
        frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
        currentItemAttributes.frame = frame;
        return currentItemAttributes;
    }

    CGRect frame = currentItemAttributes.frame;
    frame.origin.x = previousFrameRightPoint;
    currentItemAttributes.frame = frame;
    return currentItemAttributes;
}

0

minimumInteritemSpacing델리게이트의 존중에 대한 Angel García Olloqui의 답변을 collectionView(_:layout:minimumInteritemSpacingForSectionAt:)구현하는 경우 수정했습니다.

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

        let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout
        let spacing = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: 0) ?? minimumInteritemSpacing

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

    return attributes
}

0

위의 코드는 저에게 효과적입니다. 각각의 Swift 3.0 코드를 공유하고 싶습니다.

class SFFlowLayout: UICollectionViewFlowLayout {

    let itemSpacing: CGFloat = 3.0

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

        let attriuteElementsInRect = super.layoutAttributesForElements(in: rect)
        var newAttributeForElement: Array<UICollectionViewLayoutAttributes> = []
        var leftMargin = self.sectionInset.left
        for tempAttribute in attriuteElementsInRect! {
            let attribute = tempAttribute 
            if attribute.frame.origin.x == self.sectionInset.left {
                leftMargin = self.sectionInset.left
            }
            else {
                var newLeftAlignedFrame = attribute.frame
                newLeftAlignedFrame.origin.x = leftMargin
                attribute.frame = newLeftAlignedFrame
            }
            leftMargin += attribute.frame.size.width + itemSpacing
            newAttributeForElement.append(attribute)
        }
        return newAttributeForElement
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.