UICollectionView – dynamic细胞高度?
我需要显示一堆具有不同高度的collectionViewCells。 意见太复杂,我不想手动计算预期的高度。 我想执行自动布局来计算单元格高度
调用cellForItemAtIndexPath
之外的cellForItemAtIndexPath
会破坏collectionView并导致崩溃
另一个问题是单元格不在单独的xib中,所以我不能手动实例化一个临时的单元格并将其用于高度计算。
任何解决scheme?
public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { var cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as UICollectionViewCell configureCell(cell, item: items[indexPath.row]) cell.contentView.setNeedsLayout() cell.contentView.layoutIfNeeded() return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) }
编辑:
一旦调用dequeueReusableCellWithReuseIdentifier,就会发生崩溃。 如果我不调用这种方法,而是返回一个大小,一切都很好,单元格显示没有计算的大小
stream布局中不支持负值或零大小
2015-01-26 18:24:34.231 [13383:9752256] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]' *** First throw call stack: ( 0 CoreFoundation 0x00000001095aef35 __exceptionPreprocess + 165 1 libobjc.A.dylib 0x0000000109243bb7 objc_exception_throw + 45 2 CoreFoundation 0x0000000109499f33 -[__NSArrayM objectAtIndex:] + 227 3 UIKit 0x0000000107419d9c -[UICollectionViewFlowLayout _getSizingInfos] + 842 4 UIKit 0x000000010741aca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526 5 UIKit 0x000000010741651f -[UICollectionViewFlowLayout prepareLayout] + 257 6 UIKit 0x000000010742da10 -[UICollectionViewData _prepareToLoadData] + 67 7 UIKit 0x00000001074301c6 -[UICollectionViewData layoutAttributesForItemAtIndexPath:] + 44 8 UIKit 0x00000001073fddb1 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 248 9 0x00000001042b824c _TFC1228BasePaginatingViewController14collectionViewfS0_FTCSo16UICollectionView6layoutCSo22UICollectionViewLayout22sizeForItemAtIndexPathCSo11NSIndexPath_VSC6CGSize + 700 10 0x00000001042b83d4 _TToFC1228BasePaginatingViewController14collectionViewfS0_FTCSo16UICollectionView6layoutCSo22UICollectionViewLayout22sizeForItemAtIndexPathCSo11NSIndexPath_VSC6CGSize + 100 11 UIKit 0x0000000107419e2e -[UICollectionViewFlowLayout _getSizingInfos] + 988 12 UIKit 0x000000010741aca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526 13 UIKit 0x000000010741651f -[UICollectionViewFlowLayout prepareLayout] + 257 14 UIKit 0x000000010742da10 -[UICollectionViewData _prepareToLoadData] + 67 15 UIKit 0x000000010742e0e9 -[UICollectionViewData validateLayoutInRect:] + 54 16 UIKit 0x00000001073f67b8 -[UICollectionView layoutSubviews] + 170 17 UIKit 0x0000000106e3c973 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 521 18 QuartzCore 0x0000000106b0fde8 -[CALayer layoutSublayers] + 150 19 QuartzCore 0x0000000106b04a0e _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 380 20 QuartzCore 0x0000000106b0487e _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24 21 QuartzCore 0x0000000106a7263e _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 242 22 QuartzCore 0x0000000106a7374a _ZN2CA11Transaction6commitEv + 390 23 QuartzCore 0x0000000106a73db5 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 89 24 CoreFoundation 0x00000001094e3dc7 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23 25 CoreFoundation 0x00000001094e3d20 __CFRunLoopDoObservers + 368 26 CoreFoundation 0x00000001094d9b53 __CFRunLoopRun + 1123 27 CoreFoundation 0x00000001094d9486 CFRunLoopRunSpecific + 470 28 GraphicsServices 0x000000010be869f0 GSEventRunModal + 161 29 UIKit 0x0000000106dc3420 UIApplicationMain + 1282 30 0x000000010435c709 main + 169 31 libdyld.dylib 0x000000010a0f2145 start + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException
这是一个Ray Wenderlich教程 ,向您展示如何使用AutoLayoutdynamic调整UITableViewCell
的大小。 我想这将是相同的UICollectionViewCell
。
但是,基本上,你最终需要去队列和configuration一个原型单元并抓取它的高度。 读完这篇文章之后,我决定不实施这个方法,只写一些清晰明确的大小代码。
以下是我认为整篇文章的“秘诀”:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return [self heightForBasicCellAtIndexPath:indexPath]; } - (CGFloat)heightForBasicCellAtIndexPath:(NSIndexPath *)indexPath { static RWBasicCell *sizingCell = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWBasicCellIdentifier]; }); [self configureBasicCell:sizingCell atIndexPath:indexPath]; return [self calculateHeightForConfiguredSizingCell:sizingCell]; } - (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell { [sizingCell setNeedsLayout]; [sizingCell layoutIfNeeded]; CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; return size.height + 1.0f; // Add 1.0f for the cell separator height }
编辑:我做了一些研究到你的崩溃,并决定没有办法得到这个没有自定义的XIB完成。 虽然这有点令人沮丧,但是您应该能够从Storyboard剪切并粘贴到自定义的空XIB。
一旦你完成了,像下面这样的代码将会让你继续:
// ViewController.m #import "ViewController.h" #import "CollectionViewCell.h" @interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout> { } @property (weak, nonatomic) IBOutlet CollectionViewCell *cell; @property (weak, nonatomic) IBOutlet UICollectionView *collectionView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor lightGrayColor]; [self.collectionView registerNib:[UINib nibWithNibName:@"CollectionViewCell" bundle:nil] forCellWithReuseIdentifier:@"cell"]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"viewDidAppear..."); } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return 1; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return 50; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return 10.0f; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 10.0f; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return [self sizingForRowAtIndexPath:indexPath]; } - (CGSize)sizingForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *title = @"This is a long title that will cause some wrapping to occur. This is a long title that will cause some wrapping to occur."; static NSString *subtitle = @"This is a long subtitle that will cause some wrapping to occur. This is a long subtitle that will cause some wrapping to occur."; static NSString *buttonTitle = @"This is a really long button title that will cause some wrapping to occur."; static CollectionViewCell *sizingCell = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sizingCell = [[NSBundle mainBundle] loadNibNamed:@"CollectionViewCell" owner:self options:nil][0]; }); [sizingCell configureWithTitle:title subtitle:[NSString stringWithFormat:@"%@: Number %d.", subtitle, (int)indexPath.row] buttonTitle:buttonTitle]; [sizingCell setNeedsLayout]; [sizingCell layoutIfNeeded]; CGSize cellSize = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; NSLog(@"cellSize: %@", NSStringFromCGSize(cellSize)); return cellSize; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { static NSString *title = @"This is a long title that will cause some wrapping to occur. This is a long title that will cause some wrapping to occur."; static NSString *subtitle = @"This is a long subtitle that will cause some wrapping to occur. This is a long subtitle that will cause some wrapping to occur."; static NSString *buttonTitle = @"This is a really long button title that will cause some wrapping to occur."; CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; [cell configureWithTitle:title subtitle:[NSString stringWithFormat:@"%@: Number %d.", subtitle, (int)indexPath.row] buttonTitle:buttonTitle]; return cell; } @end
上面的代码(以及一个非常基本的UICollectionViewCell
子类和关联的XIB)给了我这个:
我刚刚在UICollectionView中遇到了这个问题,并且以类似于上面的答案的方式解决了这个问题,但是采用了纯粹的UICollectionView方式。
-
创build一个自定义的UICollectionViewCell,其中包含您将使用dynamic填充的任何内容。 我为它创build了自己的.xib,因为它似乎是最简单的方法。
-
在.xib中添加允许从上到下计算单元格的约束。 如果你没有考虑到所有的高度,重新resize将不起作用。 假设你有一个顶部的视图,然后是它下面的标签,下面有一个标签。 您需要将约束条件连接到该视图顶部的单元格,然后将视图的底部连接到第一个标签的顶部,将第一个标签的底部连接到第二个标签的顶部,将第二个标签的底部连接到底部到细胞的底部。
-
将.xib加载到viewcontroller中,并使用
viewDidLoad
上的collectionView进行注册let nib = UINib(nibName: CustomCellName, bundle: nil) self.collectionView!.registerNib(nib, forCellWithReuseIdentifier: "customCellID")`
-
将该xib的第二个副本加载到类中,并将其作为属性存储,以便可以使用它来确定该单元格的大小
let sizingNibNew = NSBundle.mainBundle().loadNibNamed(CustomCellName, owner: CustomCellName.self, options: nil) as NSArray self.sizingNibNew = (sizingNibNew.objectAtIndex(0) as? CustomViewCell)!
-
在您的视图控制器中实现
UICollectionViewFlowLayoutDelegate
。 重要的方法称为sizeForItemAtIndexPath
。 在该方法中,您将需要从与indexPath关联的单元格的数据源中提取数据。 然后configurationsizingCell并调用preferredLayoutSizeFittingSize
。 该方法返回一个CGSize,它由宽度减去内容插入和self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
返回的高度self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
。override func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { guard let data = datasourceArray?[indexPath.item] else { return CGSizeZero } let sectionInset = self.collectionView?.collectionViewLayout.sectionInset let widthToSubtract = sectionInset!.left + sectionInset!.right let requiredWidth = collectionView.bounds.size.width let targetSize = CGSize(width: requiredWidth, height: 0) sizingNibNew.configureCell(data as! CustomCellData, delegate: self) let adequateSize = self.sizingNibNew.preferredLayoutSizeFittingSize(targetSize) return CGSize(width: (self.collectionView?.bounds.width)! - widthToSubtract, height: adequateSize.height) }
-
在自定义单元本身的类,你将需要重写
awakeFromNib
并告诉contentView
,其大小需要灵活override func awakeFromNib() { super.awakeFromNib() self.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleHeight] }
-
在自定义单元格覆盖
layoutSubviews
override func layoutSubviews() { self.layoutIfNeeded() }
-
在自定义单元类的
preferredLayoutSizeFittingSize
类中。 这是你需要做的任何欺骗项目正在布局。 如果它的标签,你将需要告诉它什么它的preferredMaxWidth应该是。preferredLayoutSizeFittingSize(targetSize: CGSize)-> CGSize { let originalFrame = self.frame let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth var frame = self.frame frame.size = targetSize self.frame = frame self.setNeedsLayout() self.layoutIfNeeded() self.label.preferredMaxLayoutWidth = self.questionLabel.bounds.size.width // calling this tells the cell to figure out a size for it based on the current items set let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) let newSize = CGSize(width:targetSize.width, height:computedSize.height) self.frame = originalFrame self.questionLabel.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth return newSize }
所有这些步骤应该给你正确的大小。 如果你得到0或其他时髦的数字比你没有正确设置你的约束。
我们可以保持集合视图单元格的dynamic高度没有XIB(只使用故事板)。
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { NSAttributedString* labelString = [[NSAttributedString alloc] initWithString:@"Your long string goes here" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:17.0]}]; CGRect cellRect = [labelString boundingRectWithSize:CGSizeMake(cellWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin context:nil]; return CGSizeMake(cellWidth, cellRect.size.height); }
确保IB中的numberOfLines应该是0。
我遵循这个SO中提到的步骤,一切都很好,除非我的集合视图具有较less的数据(文本),使其足够宽。 检查systemLyaoutSizeFittingSize
的文档,我有这个解决scheme,所以我的单元格占据了我所要求的宽度:
- (CGSize)calculateSizeForSizingCell:(UICollectionViewCell *)sizingCell width:(CGFloat)width { CGRect frame = sizingCell.frame; frame.size.width = width; sizingCell.frame = frame; [sizingCell setNeedsLayout]; [sizingCell layoutIfNeeded]; CGSize size = [sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel]; return size; }
希望这将有助于某人。
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);
苹果文档:
等同于发送-systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:对于两个优先级,使用UILayoutPriorityFittingSizeLevel。
根据苹果公司的文档,默认值是“非常低的”
当你发送 – [UIView systemLayoutSizeFittingSize:],计算最接近目标大小(参数)的尺寸。 UILayoutPriorityFittingSizeLevel是视图要在该计算中符合目标大小的优先级别。 这是相当低的。 确切地说,在这个优先权上制约是不合适的。 你想要更高或更低。
所以我的默认行为的改变是强制使用UILayoutPriorityRequired
的宽度(水平拟合)。
按照bolnad的答案直到第4步。
然后通过replace所有其他步骤来简化:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { // Configure your cell sizingNibNew.configureCell(data as! CustomCellData, delegate: self) // We use the full width minus insets let width = collectionView.frame.size.width - collectionView.sectionInset.left - collectionView.sectionInset.right // Constrain our cell to this width let height = sizingNibNew.systemLayoutSizeFitting(CGSize(width: width, height: .infinity), withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityFittingSizeLevel).height return CGSize(width: width, height: height) }
TL; DR:扫描到图像,然后在这里检查工作项目。
更新我的答案,find一个更简单的解决scheme..
在我的情况下,我想修复宽度,并有可变高度单元格。 我想要一个可以重复使用的解决scheme来处理轮换,而且不需要太多的干预。
我到达的是在收集单元(在这种情况下,为我的基类)重写(只) systemLayoutFitting(...)
,并首先击败UICollectionView的努力设置在contentView
上的错误维度通过添加已知的约束尺寸,在这种情况下,宽度。
class EstimatedWidthCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) contentView.translatesAutoresizingMaskIntoConstraints = false } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) contentView.translatesAutoresizingMaskIntoConstraints = false } override func systemLayoutSizeFitting( _ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { width.constant = targetSize.width
然后返回单元格的最终大小 – 用于(这感觉像一个错误)的单元格本身的维度,但不contentView
– 否则约束到一个冲突的大小(因此上面的约束)。 要计算正确的单元格大小,我使用较低的优先级来放置要浮点的维度,然后返回所需的高度以适应要解决的宽度内的内容:
let size = contentView.systemLayoutSizeFitting( CGSize(width: targetSize.width, height: 1), withHorizontalFittingPriority: .required, verticalFittingPriority: verticalFittingPriority) print("\(#function) \(#line) \(targetSize) -> \(size)") return size } lazy var width: NSLayoutConstraint = { return contentView.widthAnchor .constraint(equalToConstant: bounds.size.width) .isActive(true) }() }
但是这个宽度从哪里来? 它通过集合视图的stream布局上的estimatedItemSize
进行configuration:
lazy var collectionView: UICollectionView = { let view = UICollectionView(frame: CGRect(), collectionViewLayout: layout) view.backgroundColor = .cyan view.translatesAutoresizingMaskIntoConstraints = false return view }() lazy var layout: UICollectionViewFlowLayout = { let layout = UICollectionViewFlowLayout() let width = view.bounds.size.width // should adjust for inset layout.estimatedItemSize = CGSize(width: width, height: 10) layout.scrollDirection = .vertical return layout }()
最后,为了处理旋转,我实现了trailCollectionDidChange
来使布局无效:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { layout.estimatedItemSize = CGSize(width: view.bounds.size.width, height: 10) layout.invalidateLayout() super.traitCollectionDidChange(previousTraitCollection) }
最终结果如下所示:
我在这里发表了一个工作示例。