자동 레이아웃을 사용하여 UICollectionView에서 셀의 1 차원 지정


113

iOS 8에서는 UICollectionViewFlowLayout자체 콘텐츠 크기에 따라 자동으로 셀 크기 조정을 지원합니다. 내용에 따라 너비와 높이 모두에서 셀 크기가 조정됩니다.

모든 셀의 너비 (또는 높이)에 대해 고정 값을 지정하고 다른 치수의 크기를 조정할 수 있습니까?

간단한 예를 들어 셀의 측면에 배치하는 제약 조건이있는 셀의 여러 줄 레이블을 고려하십시오. 여러 줄 레이블은 텍스트에 맞게 다양한 방식으로 크기를 조정할 수 있습니다. 셀은 컬렉션 뷰의 너비를 채우고 그에 따라 높이를 조정해야합니다. 대신 셀 크기가 임의로 조정되며 셀 크기가 컬렉션 뷰의 스크롤 할 수없는 차원보다 클 때 충돌이 발생합니다.


iOS 8에서는 systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority:컬렉션 뷰의 각 셀에 대해 레이아웃이 셀에서이 메서드를 호출하여 예상 크기를 전달합니다. 나에게 의미있는 것은 셀 에서이 방법을 재정의하고 주어진 크기를 전달하고 수평 제약 조건을 필수로 설정하고 수직 제약 조건에 낮은 우선 순위를 설정하는 것입니다. 이렇게하면 가로 크기가 레이아웃에 설정된 값으로 고정되고 세로 크기가 유연해질 수 있습니다.

이 같은:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return attributes;
}

그러나이 방법으로 반환 된 크기는 완전히 이상합니다. 이 방법에 대한 문서는 저에게 매우 불분명하며 UILayoutFittingCompressedSize UILayoutFittingExpandedSize크기가 0이고 매우 큰 상수 를 사용하는 것에 대해 언급합니다 .

size이 메서드 의 매개 변수는 실제로 두 개의 상수를 전달하는 방법입니까? 주어진 크기에 대해 적절한 높이를 얻을 것으로 예상되는 동작을 달성 할 방법이 없습니까?


대체 솔루션

1) 셀의 특정 너비를 지정하는 제약 조건을 추가하면 올바른 레이아웃을 얻을 수 있습니다. 이 제약 조건은 안전한 참조가없는 셀 컬렉션 뷰의 크기로 설정되어야하므로 이는 좋지 않은 솔루션입니다. 해당 제약 조건에 대한 값은 셀이 구성 될 때 전달 될 수 있지만 완전히 반 직관적으로 보입니다. 셀이나 콘텐츠 뷰에 ​​직접 제약 조건을 추가하면 많은 문제가 발생하기 때문에 이것은 또한 어색합니다.

2) 테이블보기를 사용합니다. 표보기는 셀의 너비가 고정되어 있으므로 상자에서 꺼내는 방식으로 작동하지만 여러 열에 고정 된 너비 셀이있는 iPad 레이아웃과 같은 다른 상황에는 적용되지 않습니다.

답변:


135

요청하는 것은 UICollectionView를 사용하여 UITableView와 같은 레이아웃을 생성하는 방법 인 것처럼 들립니다. 이것이 실제로 원하는 경우,이를 수행하는 올바른 방법은 사용자 정의 UICollectionViewLayout 하위 클래스 ( SBTableLayout유사 할 수 있음 )를 사용하는 것입니다.

반면에 기본 UICollectionViewFlowLayout을 사용하여이를 수행 할 수있는 깨끗한 방법이 있는지 정말로 묻는다면 방법이 없다고 생각합니다. iOS8의 자체 크기 조정 셀을 사용하더라도 간단하지 않습니다. 근본적인 문제는 흐름 레이아웃의 기계가 한 차원을 수정하고 다른 차원이 응답하도록 할 방법을 제공하지 않는다는 것입니다. (또한 가능하더라도 여러 줄 레이블의 크기를 조정하는 데 두 개의 레이아웃 패스가 필요하다는 점에서 추가적인 복잡성이있을 수 있습니다. 이것은 자체 크기 조정 셀이 systemLayoutSizeFittingSize에 대한 한 번의 호출을 통해 모든 크기 조정을 계산하려는 방식과 맞지 않을 수 있습니다.)

그러나 흐름 레이아웃을 사용하여 자체 크기를 결정하고 컬렉션보기의 너비에 자연스럽게 반응하는 셀을 사용하여 테이블보기와 같은 레이아웃을 만들고 싶다면 물론 가능합니다. 여전히 지저분한 방법이 있습니다. "셀 크기 조정", 즉 컨트롤러가 셀 크기를 계산할 때만 유지하는 표시되지 않는 UICollectionViewCell로 수행했습니다.

이 접근 방식에는 두 부분이 있습니다. 첫 번째 부분은 컬렉션 뷰 대리자가 컬렉션 뷰의 너비를 취하고 크기 조정 셀을 사용하여 셀의 높이를 계산하여 올바른 셀 크기를 계산하는 것입니다.

UICollectionViewDelegateFlowLayout에서 다음과 같은 메소드를 구현합니다.

func collectionView(collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
  // NOTE: here is where we say we want cells to use the width of the collection view
   let requiredWidth = collectionView.bounds.size.width

   // NOTE: here is where we ask our sizing cell to compute what height it needs
  let targetSize = CGSize(width: requiredWidth, height: 0)
  /// NOTE: populate the sizing cell's contents so it can compute accurately
  self.sizingCell.label.text = items[indexPath.row]
  let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
  return adequateSize
}

이렇게하면 컬렉션 뷰가 둘러싸는 컬렉션 뷰를 기반으로 셀의 너비를 설정하지만 크기 조정 셀에 높이를 계산하도록 요청합니다.

두 번째 부분은 높이를 계산하기 위해 자체 AL 제약 조건을 사용하도록 크기 조정 셀을 가져 오는 것입니다. 이것은 다중 라인 UILabel이 효과적으로 2 단계 레이아웃 프로세스를 요구하는 방식으로 인해 예상보다 어려울 수 있습니다. 작업은 preferredLayoutSizeFittingSize다음과 같이 메서드에서 수행됩니다 .

 /*
 Computes the size the cell will need to be to fit within targetSize.

 targetSize should be used to pass in a width.

 the returned size will have the same width, and the height which is
 calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
 can fit within that width.

 */
 func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {

   // save original frame and preferredMaxLayoutWidth
   let originalFrame = self.frame
   let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth

   // assert: targetSize.width has the required width of the cell

   // step1: set the cell.frame to use that width
   var frame = self.frame
   frame.size = targetSize
   self.frame = frame

   // step2: layout the cell
   self.setNeedsLayout()
   self.layoutIfNeeded()
   self.label.preferredMaxLayoutWidth = self.label.bounds.size.width

   // assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width

   // step3: compute how tall the cell needs to be

   // this causes the cell to compute the height it needs, which it does by asking the 
   // label what height it needs to wrap within its current bounds (which we just set).
   let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

   // assert: computedSize has the needed height for the cell

   // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
   let newSize = CGSize(width:targetSize.width,height:computedSize.height)

   // restore old frame and preferredMaxLayoutWidth
   self.frame = originalFrame
   self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth

   return newSize
 }

(이 코드는 "고급 컬렉션보기"에있는 WWDC2014 세션의 샘플 코드에있는 Apple 샘플 코드에서 수정되었습니다.)

주목해야 할 몇 가지 사항이 있습니다. 레이블의 너비를 계산하고 설정하기 위해 layoutIfNeeded ()를 사용하여 전체 셀의 레이아웃을 강제 적용합니다. 하지만 그것만으로는 충분하지 않습니다. preferredMaxLayoutWidth레이블이 자동 레이아웃에서 해당 너비를 사용 하도록 설정해야한다고 생각합니다 . 그리고 systemLayoutSizeFittingSize나서야 레이블을 고려하면서 셀이 높이를 계산하도록 할 수 있습니다.

이 접근 방식이 마음에 드나요? 아니!! 너무 복잡하고 레이아웃을 두 번 수행합니다. 그러나 성능이 문제가되지 않는 한, 코드에서 레이아웃을 두 번 정의하는 것보다 런타임에 레이아웃을 두 번 수행하는 것이 유일한 대안 인 것 같습니다.

내 희망은 결국 자체 크기 조정 세포가 다르게 작동하고이 모든 것이 훨씬 더 간단해질 것이라는 것입니다.

직장에서 그것을 보여주는 예제 프로젝트 .

그러나 왜 자체 크기 조정 셀을 사용하지 않습니까?

이론적으로 iOS8의 새로운 "셀 자체 크기 조정"기능은이를 불필요하게 만들어야합니다. 자동 레이아웃 (AL)을 사용하여 셀을 정의한 경우 컬렉션보기는 자체 크기를 조정하고 올바르게 배치 할 수있을만큼 스마트해야합니다. 실제로 여러 줄 레이블과 함께 작동하도록 만든 예제는 본 적이 없습니다. 나는 이것이 부분적으로 자체 크기 조정 세포 메커니즘이 여전히 버그가 있기 때문 이라고 생각 합니다 .

하지만 대부분은 자동 레이아웃 및 라벨의 일반적인 까다로운 문제 때문일 것입니다. UILabels에는 기본적으로 2 단계 레이아웃 프로세스가 필요하다는 것입니다. 자체 크기 조정 셀로 두 단계를 모두 수행하는 방법이 명확하지 않습니다.

그리고 제가 말했듯이 이것은 실제로 다른 레이아웃을위한 작업입니다. 너비를 고정하고 높이를 선택할 수 있도록하는 것이 아니라 크기가있는 항목을 배치하는 것이 흐름 레이아웃의 핵심입니다.

그리고 preferredLayoutAttributesFittingAttributes :?

preferredLayoutAttributesFittingAttributes:방법은 훈제 청어, 나는 생각한다. 그것은 새로운 자체 크기 조정 셀 메커니즘과 함께 사용됩니다. 따라서 메커니즘이 신뢰할 수없는 한 이것은 답이 아닙니다.

그리고 systemlayoutSizeFittingSize :는 무엇입니까?

문서가 혼란 스럽습니다.

의 문서 systemLayoutSizeFittingSize:systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:모두 만 통과해야한다고 제안 UILayoutFittingCompressedSize하고 UILayoutFittingExpandedSize는 AS targetSize. 그러나 메서드 서명 자체, 헤더 주석 및 함수 동작은 targetSize매개 변수 의 정확한 값에 응답하고 있음을 나타냅니다 .

실제로 UICollectionViewFlowLayoutDelegate.estimatedItemSize새로운 자체 크기 조정 셀 메커니즘을 사용하기 위해을 설정하면 해당 값이 targetSize로 전달되는 것처럼 보입니다. 그리고와 UILabel.systemLayoutSizeFittingSize정확히 동일한 값을 반환하는 것 같습니다 UILabel.sizeThatFits. 인수가 systemLayoutSizeFittingSize대략적인 대상이고 인수가 sizeThatFits:최대 외접 크기 여야 한다는 점을 감안할 때 이것은 의심 스럽습니다 .

더 많은 리소스

그러한 일상적인 요구 사항에 "연구 자원"이 필요하다고 생각하는 것은 슬프지만 나는 그렇게 생각합니다. 좋은 예와 토론은 다음과 같습니다.


프레임을 이전 프레임으로 되 돌리는 이유는 무엇입니까?
vrwim 2015

이 기능은 호출하는 뷰의 레이아웃 상태에 영향을주지 않고 뷰의 선호하는 레이아웃 크기를 결정하는 데만 사용할 수 있습니다. 실제로이 뷰를 목적으로 만 유지하는 경우에는 관련이 없습니다. 레이아웃 계산을합니다. 또한 여기서 제가하는 일은 불충분합니다. 프레임 만 복원해도 레이아웃을 수행하면 하위보기의 레이아웃이 수정되었을 수 있으므로 이전 상태로 실제로 복원되지 않습니다.
algal

1
이것은 사람들이 ios6 이후로해온 일에 너무 엉망입니다. 애플은 항상 어떤 방식으로 나왔지만 결코 옳지 않습니다.
GregP

1
viewDidLoad에서 크기 조정 셀을 수동으로 인스턴스화하고 사용자 정의 속성을 볼 수 있습니다. 그것은 단 하나의 셀이므로 저렴합니다. 크기 조정에만 사용하고 모든 상호 작용을 관리하고 있으므로 컬렉션 뷰의 셀 재사용 메커니즘에 참여할 필요가 없으므로 컬렉션 뷰 자체에서 가져올 필요가 없습니다.
algal

1
도대체 어떻게 자체 크기 조절 세포가 여전히 깨질 수 있습니까?!
Reuben Scratton 2016

50

여기에 다른 답변보다 더 깨끗한 방법이 있으며 잘 작동합니다. 성능이 뛰어나야하며 (컬렉션 뷰가 빠르게로드되고, 불필요한 자동 레이아웃 패스가 없음 등) 고정 된 컬렉션 뷰 너비와 같은 '매직 넘버'가 없어야합니다. 예를 들어 회전에서 컬렉션보기 크기를 변경 한 다음 레이아웃을 무효화하는 것도 잘 작동합니다.

1. 다음 흐름 레이아웃 하위 클래스를 만듭니다.

class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout {

    // Don't forget to use this class in your storyboard (or code, .xib etc)

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
        guard let collectionView = collectionView else { return attributes }
        attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right
        return attributes
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let allAttributes = super.layoutAttributesForElementsInRect(rect)
        return allAttributes?.flatMap { attributes in
            switch attributes.representedElementCategory {
            case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath)
            default: return attributes
            }
        }
    }
}

2. 자동 크기 조정을 위해 컬렉션보기 등록

// The provided size should be a plausible estimate of the actual
// size. You can set your item size in your storyboard
// to a good estimate and use the code below. Otherwise,
// you can provide it manually too, e.g. CGSize(width: 100, height: 100)

flowLayout.estimatedItemSize = flowLayout.itemSize

3. 셀 하위 클래스에서 미리 정의 된 너비 + 사용자 지정 높이 사용

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    return layoutAttributes
}

8
이것은 내가 자체 크기 조정 주제에서 찾은 가장 큰 대답입니다. 고마워요 조던!
haik.ampardjian 2010 년

1
iOS (9.3)에서 작동하도록 할 수 없습니다. 10에서는 잘 작동합니다. 응용 프로그램이 _updateVisibleCells에서 충돌합니다 (무한 재귀?). openradar.me/25303115
cristiano2lopes

@ cristiano2lopes iOS 9.3에서 잘 작동합니다. 합리적인 추정치를 제공하고 있는지 확인하고 셀 제약 조건이 올바른 크기로 해결되도록 설정되었는지 확인하십시오.
Jordan Smith

이것은 훌륭하게 작동합니다. Xcode 8을 사용하고 있으며 iOS 9.3.3 (물리적 장치)에서 테스트했으며 충돌하지 않습니다! 잘 작동합니다. 견적이 좋은지 확인하십시오!
hahmed nov.

1
@JordanSmith 이것은 iOS 10에서 작동하지 않습니다. 샘플 데모가 있습니까? 내용을 기반으로 컬렉션 뷰 셀 높이를 수행하기 위해 많은 것을 시도했지만 아무것도하지 않았습니다. 나는 아이폰 OS 10에서 자동 레이아웃을 사용하여 신속 3.를 사용하고
Ekta Padaliya을

6

몇 줄의 코드로 iOS 9에서 작업을 수행하는 간단한 방법-수평 방법의 예 (높이를 Collection View 높이로 고정) :

estimatedItemSize자체 크기 조정 셀을 사용 하려면 컬렉션보기 흐름 레이아웃을 초기화합니다 .

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);

Collection View Layout Delegate (대부분의 경우 View Controller에서)를 구현합니다 collectionView:layout:sizeForItemAtIndexPath:. 여기서 목표는 고정 높이 (또는 너비)를 컬렉션 뷰 차원으로 설정하는 것입니다. 10 값은 무엇이든 될 수 있지만 제약 조건을 위반하지 않는 값으로 설정해야합니다.

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(10, CGRectGetHeight(collectionView.bounds));
}

사용자 지정 셀 preferredLayoutAttributesFittingAttributes:방법을 재정의합니다 .이 부분은 실제로 자동 레이아웃 제약 조건과 방금 설정 한 높이를 기반으로 동적 셀 너비를 계산합니다.

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy];
    float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width;
    CGRect frame = attributes.frame;
    frame.size.width = desiredWidth;
    attributes.frame = frame;
    return attributes;
}

iOS9에서이 간단한 방법이 셀에 여러 줄이 포함되어있을 때 올바르게 작동하는지 알고 UILabel있습니까? 줄 바꿈이 셀의 고유 높이에 영향을 미칩니다. 이것은 내가 설명하는 모든 백 플립을 요구하는 일반적인 경우입니다. 내 대답 이후 iOS가 문제를 해결했는지 궁금합니다.
algal

2
@algal 슬프게도 대답은 '아니오'입니다.
jamesk

4

선호하는 레이아웃 속성에서 너비를 수정 해보십시오.

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy];
    CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGRect newFrame = attr.frame;
    newFrame.size.height = size.height;
    attr.frame = newFrame;
    return attr;
}

당연히 레이아웃을 다음과 같이 올바르게 설정해야합니다.

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)];

여기에 일정한 너비의 셀을 사용하고 동적 유형을 지원 하는 Github에 넣은 것이 있으므로 시스템 글꼴 크기가 변경되면 셀의 높이가 업데이트됩니다.


이 스 니펫에서는 systemLayoutSizeFittingSize :를 호출합니다. preferredMaxLayoutWidth를 어딘가에 설정하지 않고도이 작업을 수행 할 수 있었습니까? 내 경험상 preferredMaxLayoutWidth를 설정하지 않는 경우, 예를 들어 다른 곳에 정의 된 자동 레이아웃 제약 조건의 논리를 재현하는 계산으로 sizeThatFits :를 재정 의하여 추가 수동 레이아웃을 수행해야합니다.
algal

@algal preferredMaxLayoutWidth는 UILabel의 속성입니까? 그게 어떤 관련이 있는지 확실하지 않습니까?
Daniel Galasko 2014 년

이런! 좋은 지적이 아닙니다! 나는 이것을 UITextView로 처리 한 적이 없기 때문에 API가 거기에서 다르다는 것을 깨닫지 못했습니다. UITextView.systemLayoutSizeFittingSize는 intrinsicContentSize가 없기 때문에 프레임을 존중하는 것처럼 보입니다. 그러나 UILabel.systemLayoutSizeFittingSize는 intrinsicContentSize가 있으므로 프레임을 무시하므로 intrinsicContentSize를 가져와 AL에 래핑 된 높이를 노출하려면 preferredMaxLayoutWidth가 필요합니다. 여전히 UITextView에 두 개의 패스 또는 수동 레이아웃이 필요한 것처럼 보이지만이 모든 것을 완전히 이해하지는 못한다고 생각합니다.
algal

@algal 동의 할 수 있습니다. UILabel을 사용할 때 털이 많은 걸림돌을 경험했습니다. 선호하는 너비를 설정하면 문제가 해결되지만이 너비는 수퍼 뷰와 함께 확장되어야하므로보다 동적 인 접근 방식을 사용하는 것이 좋습니다. 내 프로젝트의 대부분이 선호하는 속성을 사용하지 않는, 나는 일반적으로 항상 프로그래머와 같은 빠른 수정 깊은 문제로 그것을 사용
다니엘 Galasko

0

예, 프로그래밍 방식으로 자동 레이아웃을 사용하고 스토리 보드 또는 xib에서 제약 조건을 설정하여 수행 할 수 있습니다. 너비 크기를 일정하게 유지하고 높이를 다음보다 크거나 같게 설정하려면 제약 조건을 추가해야합니다.
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatically/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
희망이 도움이되고 문제를 해결합니다.


3
이것은 절대 너비 값에 대해 작동해야하지만 셀이 항상 컬렉션 뷰의 전체 너비가되기를 원하는 경우에는 작동하지 않습니다.
Michael Waterfall

@MichaelWaterfall AutoLayout을 사용하여 전체 너비로 작동하도록 관리했습니다. 셀 내부의 콘텐츠보기에 너비 제한을 추가했습니다. 그런 다음 cellForItemAtIndexPath제약 조건 을 업데이트하십시오 cell.constraintItemWidth.constant = UIScreen.main.bounds.width.. 내 앱은 portait 전용입니다. reloadData방향 변경 후 구속 조건을 업데이트하고 싶을 것입니다 .
Flitskikker 2016
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.