Programing

자동 레이아웃이있는 UICollectionView 자체 크기 조정 셀

lottogame 2020. 5. 16. 10:03
반응형

자동 레이아웃이있는 UICollectionView 자체 크기 조정 셀


UICollectionViewCells자동 레이아웃을 사용하여 자체 크기 조정을 시도하고 있지만 셀 크기를 내용에 맞게 조정할 수는 없습니다. 셀의 contentView 안에있는 내용에서 셀 크기가 어떻게 업데이트되는지 이해하는 데 어려움을 겪고 있습니다.

내가 시도한 설정은 다음과 같습니다.

  • 사용자 UICollectionViewCellA를 UITextView그있는 contentView한다.
  • 에 대한 스크롤 UITextView이 비활성화되었습니다.
  • contentView의 가로 제약 조건은 "H : | [_textView (320)]"입니다. 즉, UITextView명시 적 너비가 320 인 셀의 왼쪽에 고정됩니다.
  • contentView의 수직 제약은 "V : | -0-[_ textView]"입니다. 즉 UITextView, 셀 상단에 고정됩니다.
  • UITextView상수로 설정 높이 제약이 UITextView보고서는 텍스트에 맞게됩니다.

셀 배경이 빨간색으로 UITextView설정되고 배경이 파란색으로 설정된 모습은 다음과 같습니다 .셀 배경 빨간색, UITextView 배경 파란색

나는 GitHub 에서 놀고있는 프로젝트를 여기 에 넣었다 .


스위프트 5 업데이트

preferredLayoutAttributesFittingAttributes이름 변경 preferredLayoutAttributesFitting및 자동 크기 조정 사용


스위프트 4 업데이트

systemLayoutSizeFittingSize 로 개명 systemLayoutSizeFitting


iOS 9 용으로 업데이트

iOS 9에서 GitHub 솔루션이 중단 된 것을보고 마침내 문제를 완전히 조사 할 시간을 얻었습니다. 자체 크기 조정 셀에 대한 서로 다른 구성의 몇 가지 예를 포함하도록 저장소를 업데이트했습니다. 내 결론은 자기 사이징 셀은 이론적으로는 훌륭하지만 실제로는 지저분하다는 것입니다. 자체 크기 조정 셀을 진행할 때주의 할 사항.

TL; DR

GitHub 프로젝트를 확인하십시오


자체 크기 조정 셀은 플로우 레이아웃에서만 지원되므로 사용중인 것이 맞는지 확인하십시오.

자체 크기 조정 셀이 작동하려면 설정해야 할 두 가지가 있습니다.

1. 설정 estimatedItemSizeUICollectionViewFlowLayout

estimatedItemSize속성 을 설정하면 흐름 레이아웃이 실제로 동적이 됩니다.

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

2. 셀 서브 클래스 크기 조정 지원 추가

이것은 2 가지 맛으로 나옵니다. 의 자동 레이아웃 또는 맞춤 재정의 preferredLayoutAttributesFittingAttributes.

자동 레이아웃으로 셀 생성 및 구성

셀의 제약 조건을 구성하는 것에 대한 훌륭한 SO 게시물 이 있기 때문에 이에 대해 자세히 설명하지 않습니다 . 것을 그냥 조심 엑스 코드 (6) 파산 당신이 아이폰 OS 7 지원하는 경우 아이폰 OS 7 그래서 함께 잔뜩, 당신은 autoresizingMask 셀의 경계 때와 같이 설정되어있는 contentView의 경계 셀의있는 contentView에와 있다는 설정되어 있는지 확인 같은 물건을 수행해야합니다 셀이로드됩니다 (예 :) awakeFromNib.

알아야 할 사항은 셀이 테이블 뷰 셀보다 더 심각하게 제한되어야한다는 것입니다. 예를 들어 너비를 동적으로 만들려면 셀에 높이 제한이 필요합니다. 마찬가지로, 높이를 동적으로하려면 셀에 대한 너비 제한이 필요합니다.

preferredLayoutAttributesFittingAttributes커스텀 셀에서 구현

이 기능이 호출되면 뷰가 이미 컨텐츠로 구성되었습니다 (예 : cellForItem호출). 제약 조건이 적절하게 설정되었다고 가정하면 다음과 같이 구현할 수 있습니다.

//forces the system to do one layout pass
var isHeightCalculated: Bool = false

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    //Exhibit A - We need to cache our calculation to prevent a crash.
    if !isHeightCalculated {
        setNeedsLayout()
        layoutIfNeeded()
        let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
        var newFrame = layoutAttributes.frame
        newFrame.size.width = CGFloat(ceilf(Float(size.width)))
        layoutAttributes.frame = newFrame
        isHeightCalculated = true
    }
    return layoutAttributes
}

참고 iOS 9에서는주의하지 않으면 구현에서 충돌이 발생할 수있는 동작이 약간 변경되었습니다 (자세한 내용은 여기 참조 ). 구현할 때 preferredLayoutAttributesFittingAttributes레이아웃 속성의 프레임 만 한 번만 변경해야합니다. 이 작업을 수행하지 않으면 레이아웃이 구현을 무기한 호출하고 결국 충돌합니다. 한 가지 해결책은 셀에서 계산 된 크기를 캐시하고 셀을 재사용하거나 isHeightCalculated속성으로 수행 한 내용을 변경할 때마다 무효화하는 것입니다 .

레이아웃 경험

이 시점에서 collectionView에 '기능적인'동적 셀이 있어야합니다. 아직 테스트 중에 충분한 기본 솔루션을 찾지 못 했으므로 언제든지 의견을 말하십시오. 여전히 UITableView역동적 인 사이징 IMHO와의 싸움에서이기는 것 같습니다 .

경고

프로토 타입 셀을 사용하여 추정 된 항목 크기 를 계산하는 경우 XIB가 크기 클래스를 사용 하면 중단됩니다 . 그 이유는 XIB에서 셀을로드 할 때 크기 클래스가로 구성되기 때문입니다 Undefined. iOS 7에서는 크기 클래스가 장치 (iPad = Regular-Any, iPhone = Compact-Any)를 기반으로로드되기 때문에 iOS 8 이상에서만 손상됩니다. XIB를로드하지 않고 추정 된 항목 크기를 설정하거나 XIB에서 셀을로드하고 collectionView에 추가하고 (traitCollection을 설정 함) 레이아웃을 수행 한 다음 슈퍼 뷰에서 제거 할 수 있습니다. 또는 셀이 traitCollection게터를 재정의 하고 적절한 특성을 반환 하도록 할 수도 있습니다 . 그것은 당신에게 달려 있습니다.

내가 놓친 것이 있으면 알려주세요. 도움이 되길 바랍니다. 행운을 빕니다.



Daniel Galasko의 답변에 대한 몇 가지 주요 변경 사항으로 모든 문제가 해결되었습니다. 불행히도, 나는 직접적으로 (아직) 의견을 말할만큼 충분한 평판을 얻지 못했습니다.

1 단계에서 자동 레이아웃을 사용하는 경우 단일 상위 UIView를 셀에 추가하기 만하면됩니다. 셀 내부의 모든 것은 부모의 하위 뷰 여야합니다. 그것은 내 모든 문제에 답했다. Xcode는 UITableViewCells에 이것을 자동으로 추가하지만 UICollectionViewCells에는 그렇지 않습니다. 문서 에 따르면 :

셀 모양을 구성하려면 dataView 속성의보기에 데이터 항목의 내용을 하위보기로 표시하는 데 필요한보기를 추가하십시오. 셀 자체에 하위 뷰를 직접 추가하지 마십시오.

그런 다음 3 단계를 완전히 건너 뜁니다. 필요하지 않습니다.


iOS10에는 UICollectionViewFlowLayout.automaticSize(이전 UICollectionViewFlowLayoutAutomaticSize) 이라는 새로운 상수가 있으므로 대신 :

self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)

이것을 사용할 수 있습니다 :

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

컬렉션 뷰의 셀에 일정한 wid가있는 경우 특히 성능이 향상됩니다.

흐름 레이아웃 액세스 :

override func viewDidLoad() {
   super.viewDidLoad()

   if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
      flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
   }
}

iOS 10 이상에서는 매우 간단한 2 단계 프로세스입니다.

  1. 모든 셀 내용이 단일 UIView (또는 UIStackView와 같은 UIView의 하위 항목) 내에 배치되어 자동 레이아웃을 많이 단순화해야합니다. UITableViewCells의 크기를 동적으로 조정하는 것처럼 전체 뷰 계층에는 가장 바깥 쪽 컨테이너에서 가장 안쪽 뷰까지 제약 조건이 구성되어 있어야합니다. UICollectionViewCell과 즉각적인 childview 사이의 제약 조건이 포함됩니다.

  2. UICollectionView의 flowlayout에 자동 크기 조정을 지시하십시오.

    yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    

  1. viewDidLoad ()에 flowLayout 추가

    override func viewDidLoad() {
        super.viewDidLoad() 
        if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = CGSize(width: 1, height:1)
        }
    }
    
  2. 또한 UIView를 셀의 mainContainer로 설정하고 셀에 필요한 모든보기를 추가하십시오.

  3. 자세한 내용은이 멋진 튜토리얼을 참조하십시오 .iOS 9 및 10에서 자동 레이아웃을 사용하여 자동 크기 조정 셀이있는 UICollectionView


일정 시간 동안이 문제를 해결 한 후 스크롤을 비활성화하지 않으면 UITextViews에서 크기 조정이 작동하지 않는 것으로 나타났습니다.

let textView = UITextView()
textView.scrollEnabled = false

컬렉션 뷰의 동적 셀 높이를 수행했습니다. 여기 git hub repo가 있습니다.

그리고 preferredLayoutAttributesFittingAttributes가 두 번 이상 호출 된 이유를 찾아보십시오. 실제로는 3 회 이상 호출됩니다.

콘솔 로그 사진 : 여기에 이미지 설명을 입력하십시오

첫 번째 preferredLayoutAttributesFittingAttributes :

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c290e0> index path: (<NSIndexPath:    0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 57.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);

layoutAttributes.frame.size.height는 현재 상태 57.5 입니다.

두 번째 preferredLayoutAttributesFittingAttributes :

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c16370> index path: (<NSIndexPath: 0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);

예상대로 셀 프레임 높이가 534.5변경되었습니다 . 그러나 컬렉션 뷰의 높이는 여전히 0입니다.

3 번째 preferredLayoutAttributesFittingAttributes :

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa403d516a0> index path: (<NSIndexPath: 0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 477);

컬렉션 뷰 높이가 0에서 477로 변경된 것을 볼 수 있습니다 .

동작은 스크롤 처리와 비슷합니다.

1. Before self-sizing cell

2. Validated self-sizing cell again after other cells recalculated.

3. Did changed self-sizing cell

처음에는이 방법이 한 번만 호출된다고 생각했습니다. 그래서 나는 다음과 같이 코딩했다 :

CGRect frame = layoutAttributes.frame;
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy];
newAttributes.frame = frame;
return newAttributes;

이 줄 :

frame.size.height = frame.size.height + self.collectionView.contentSize.height;

시스템 호출 무한 루프 및 앱 충돌이 발생합니다.

크기가 변경되면 모든 셀의 위치 (예 : 프레임)가 더 이상 변경되지 않을 때까지 모든 셀의 preferredLayoutAttributesFittingAttributes를 반복해서 검증합니다.


UICollectionViewDelegateFlowLayout 메소드를 구현하는 경우 :

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

When you call collectionview performBatchUpdates:completion:, the size height will use sizeForItemAtIndexPath instead of preferredLayoutAttributesFittingAttributes.

The rendering process of performBatchUpdates:completion will go through the method preferredLayoutAttributesFittingAttributes but it ignores your changes.


To whomever it may help,

I had that nasty crash if estimatedItemSize was set. Even if I returned 0 in numberOfItemsInSection. Therefore, the cells themselves and their auto-layout were not the cause of the crash... The collectionView just crashed, even when empty, just because estimatedItemSize was set for self-sizing.

In my case I reorganized my project, from a controller containing a collectionView to a collectionViewController, and it worked.

Go figure.


The example method above does not compile. Here is a corrected version (but untested as to whether or not it works.)

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes 
{
    let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes

    var newFrame = attr.frame
    self.frame = newFrame

    self.setNeedsLayout()
    self.layoutIfNeeded()

    let desiredHeight: CGFloat = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    newFrame.size.height = desiredHeight
    attr.frame = newFrame
    return attr
}

Update more information:

  • If you use flowLayout.estimatedItemSize, suggest use iOS8.3 later version. Before iOS8.3, it will crash [super layoutAttributesForElementsInRect:rect];. The error message is

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'

  • Second, in iOS8.x version, flowLayout.estimatedItemSize will cause different section inset setting did not work. i.e. function: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:.


For anyone who tried everything without luck, this is the only thing that got it working for me. For the multiline labels inside cell, try adding this magic line:

label.preferredMaxLayoutWidth = 200

More info: here

Cheers!


In addition to above answers,

Just make sure you set estimatedItemSize property of UICollectionViewFlowLayout to some size and do not implement sizeForItem:atIndexPath delegate method.

That's it.


I tried using estimatedItemSize but there were a bunch of bugs when inserting and deleting cells if the estimatedItemSize was not exactly equal to the cell's height. i stopped setting estimatedItemSize and implemented dynamic cell's by using a prototype cell. here's how that's done:

create this protocol:

protocol SizeableCollectionViewCell {
    func fittedSize(forConstrainedSize size: CGSize)->CGSize
}

implement this protocol in your custom UICollectionViewCell:

class YourCustomCollectionViewCell: UICollectionViewCell, SizeableCollectionViewCell {

    @IBOutlet private var mTitle: UILabel!
    @IBOutlet private var mDescription: UILabel!
    @IBOutlet private var mContentView: UIView!
    @IBOutlet private var mTitleTopConstraint: NSLayoutConstraint!
    @IBOutlet private var mDesciptionBottomConstraint: NSLayoutConstraint!

    func fittedSize(forConstrainedSize size: CGSize)->CGSize {

        let fittedSize: CGSize!

        //if height is greatest value, then it's dynamic, so it must be calculated
        if size.height == CGFLoat.greatestFiniteMagnitude {

            var height: CGFloat = 0

            /*now here's where you want to add all the heights up of your views.
              apple provides a method called sizeThatFits(size:), but it's not 
              implemented by default; except for some concrete subclasses such 
              as UILabel, UIButton, etc. search to see if the classes you use implement 
              it. here's how it would be used:
            */
            height += mTitle.sizeThatFits(size).height
            height += mDescription.sizeThatFits(size).height
            height += mCustomView.sizeThatFits(size).height    //you'll have to implement this in your custom view

            //anything that takes up height in the cell has to be included, including top/bottom margin constraints
            height += mTitleTopConstraint.constant
            height += mDescriptionBottomConstraint.constant

            fittedSize = CGSize(width: size.width, height: height)
        }
        //else width is greatest value, if not, you did something wrong
        else {
            //do the same thing that's done for height but with width, remember to include leading/trailing margins in calculations
        }

        return fittedSize
    }
}

now make your controller conform to UICollectionViewDelegateFlowLayout, and in it, have this field:

class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout {
    private var mCustomCellPrototype = UINib(nibName: <name of the nib file for your custom collectionviewcell>, bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell
}

it will be used as a prototype cell to bind data to and then determine how that data affected the dimension that you want to be dynamic

finally, the UICollectionViewDelegateFlowLayout's collectionView(:layout:sizeForItemAt:) has to be implemented:

class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

    private var mDataSource: [CustomModel]

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)->CGSize {

        //bind the prototype cell with the data that corresponds to this index path
        mCustomCellPrototype.bind(model: mDataSource[indexPath.row])    //this is the same method you would use to reconfigure the cells that you dequeue in collectionView(:cellForItemAt:). i'm calling it bind

        //define the dimension you want constrained
        let width = UIScreen.main.bounds.size.width - 20    //the width you want your cells to be
        let height = CGFloat.greatestFiniteMagnitude    //height has the greatest finite magnitude, so in this code, that means it will be dynamic
        let constrainedSize = CGSize(width: width, height: height)

        //determine the size the cell will be given this data and return it
        return mCustomCellPrototype.fittedSize(forConstrainedSize: constrainedSize)
    }
}

and that's it. Returning the cell's size in collectionView(:layout:sizeForItemAt:) in this way preventing me from having to use estimatedItemSize, and inserting and deleting cells works perfectly.

참고 URL : https://stackoverflow.com/questions/25895311/uicollectionview-self-sizing-cells-with-auto-layout

반응형