한 줄에 너비가 다른 여러 셀이있는 프로젝트에서 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:
메서드 를 구현해야 합니다. -
또한 많은 솔루션
rect
이layoutAttributesForElements(in rect: CGRect)
함수에 전달 되는 매개 변수 에 대해 가정 합니다. 예를 들어, 많은 사람들은는rect
항상 새 줄의 시작에서 시작 한다는 가정을 기반으로합니다 .
즉,
이 페이지에 제안 된 대부분의 솔루션은 일부 특정 응용 프로그램에서 작동하지만 모든 상황에서 예상대로 작동하지 않습니다.
AlignedCollectionViewFlowLayout
이러한 문제를 해결하기 위해 유사한 질문에 대한 답변에서 Matt 와 Chris Wagner 가 UICollectionViewFlowLayout
제안한 유사한 아이디어를 따르는 하위 클래스를 만들었습니다 . 셀을 정렬 할 수 있습니다.
⬅︎ 왼쪽 :
또는 ➡︎ 오른쪽 :
또한 각 행의 셀 을 세로로 정렬하는 옵션을 제공합니다 (높이가 다른 경우).
여기에서 간단히 다운로드 할 수 있습니다.
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. 왼쪽 맞춤 자동 크기 조정 UICollectionViewCell
s
아래 구현은의 자동 크기 조정 셀을 왼쪽 정렬하기 위해 UICollectionViewLayout
‘s layoutAttributesForElements(in:)
, UICollectionViewFlowLayout
‘s estimatedItemSize
및 UILabel
‘ 를 사용하는 방법을 보여줍니다 .preferredMaxLayoutWidth
UICollectionView
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
아래 구현은의 미리 정의 된 크기로 셀을 왼쪽 정렬하기 위해 UICollectionViewLayout
의 layoutAttributesForElements(in:)
및 UICollectionViewFlowLayout
의 사용 방법을 보여줍니다 .itemSize
UICollectionView
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
왼쪽, 오른쪽 또는 전체 (기본적으로 기본) 수평 정렬을 수행 하는 하위 클래스 라이브러리를 만들었습니다. 또한 각 셀 사이의 절대 거리를 설정할 수도 있습니다. 수평 중심 정렬과 수직 정렬도 추가 할 계획입니다.