Programming

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

procodes 2020. 8. 3. 21:30
반응형

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


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 UILayoutFittingExpandedSize0 크기와 꽤 큰 상수 사용하는 상수 사용하는 것에 대해 언급합니다 .

size이 방법 매개 변수가 실제로 두 상수를 전달하는 방법입니까? 주어진 크기에 적합한 높이를 얻을 것으로 예상되는 동작을 수행 할 방법이 없습니까?


대체 솔루션

1) 셀의 특정 너비를 지정하는 제약 조건을 추가하면 올바른 레이아웃을 얻을 수 있습니다. 제약 조건을 셀 참조 뷰의 크기로 설정해야하기 때문에이 방법은 좋지 않습니다. 해당 제약 조건의 값은 셀이 구성 될 때 전달 될 수 있지만 완전히 반 직관적 인 것처럼 보입니다. 셀에 직접 제약 조건을 추가하거나 내용보기로 인해 많은 문제가 발생하기 때문에 이는 어색합니다.

2) 테이블 뷰를 사용하십시오. 셀의 너비가 고정되어 있기 때문에 표보기는 이러한 방식으로 즉시 작동하지만 여러 열에 고정 너비의 셀이있는 iPad 레이아웃과 같은 다른 상황에는 적용되지 않습니다.


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
 }

(이 코드는 "Advanced Collection View"에서 WWDC2014 세션의 샘플 코드에있는 Apple 샘플 코드에서 수정되었습니다.)

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

Do I like this approach? No!! It feels way too complex, and it does layout twice. But as long as performance doesn't become an issue, I'd rather perform layout twice at runtime than have to define it twice in code, which seems to be the only other alternative.

My hope is that eventually self-sizing cells will work differently and this will all get a lot simpler.

Example project showing it at work.

But why not just use self-sizing cells?

In theory, iOS8's new facilities for "self-sizing cells" should make this unnecessary. If you've defined a cell with Auto Layout (AL), then the collection view should be smart enough to let it size itself and lay itself out correctly. In practice, I haven't seen any examples that have gotten this to work with multi-line labels. I think this is partly because the self-sizing cell mechanism is still buggy.

But I'd bet it's mostly because of the usual trickiness of Auto Layout and labels, which is that UILabels require a basically two-step layout process. It's not clear to me how you can perform both steps with self-sizing cells.

And like I said, this is really a job for a different layout. It is part of flow layout's essence that it positions things that have a size, rather than fixes a width and lets them choose their height.

And what about preferredLayoutAttributesFittingAttributes: ?

The preferredLayoutAttributesFittingAttributes: method is a red herring, I think. That is only there to be used with the new self-sizing cell mechanism. So this isn't the answer as long as that mechanism is unreliable.

And what's up with systemlayoutSizeFittingSize:?

You're right the docs are confusing.

The docs on systemLayoutSizeFittingSize: and systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: both suggest that you should only pass UILayoutFittingCompressedSize and UILayoutFittingExpandedSize as the targetSize. However, the method signature itself, the header comments, and the behavior of the functions indicate that they are responding to the exact value of the targetSize parameter.

In fact, if you set the UICollectionViewFlowLayoutDelegate.estimatedItemSize, in order to enable the new self-sizing cell mechanism, that value seems to get passed in as the targetSize. And UILabel.systemLayoutSizeFittingSize seems to return the exact same values as UILabel.sizeThatFits. This is suspicious, given that the argument to systemLayoutSizeFittingSize is supposed to be a rough target and the argument to sizeThatFits: is supposed to be a maximum circumscribing size.

More Resources

While it is sad to think that such a routine requirement should require "research resources", I think it does. Good examples and discussions are:


There's a cleaner way to do this than some of the other answers here, and it works well. It should be performant (collection views load fast, no unnecessary auto layout passes etc), and doesn't have any 'magic numbers' like a fixed collection view width. Changing the collection view size, e.g. on rotation, and then invalidating the layout should work great too.

1. Create the following flow layout subclass

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. Register your collection view for automatic sizing

// 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. Use the predefined width + custom height in your cell subclass

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

A simple way to do it in iOS 9 in a few lines of codes - the horizontal way exemple (fixing its height to its Collection View height) :

Init your Collection View Flow Layout with an estimatedItemSize to enable self-sizing cell :

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

Implement the Collection View Layout Delegate (in your View Controller most of the time), collectionView:layout:sizeForItemAtIndexPath: . The goal here is to set the fixed height (or width) to the Collection View dimension. The 10 value can be anything, but you should set it to a value that doesn't break constraints :

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

Override your custom cell preferredLayoutAttributesFittingAttributes: method, this part actually calculate your dynamic cell width based on your Auto Layout constraints and the height you have just set :

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

Try fixing your width in the preferred layout attributes:

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

Naturally you also want to ensure that you setup your layout correctly to:

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

Heres something I put on Github that uses constant width cells and supports dynamic type so the height of the cells updates as the system font size changes.


YES it can be done using auto layout programmatically and by setting constraints in storyboard or xib. You need to add constraint for width size to remain constant and set height greater than or equal to.
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatically/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
Hope this will be helpful and solve your issue.

참고URL : https://stackoverflow.com/questions/26143591/specifying-one-dimension-of-cells-in-uicollectionview-using-auto-layout

반응형