2015-01-27 3 views
35

Мне нужно отобразить кучу collectionViewCells, которые имеют разную высоту. представления слишком сложны, и я не хочу вручную вычислять ожидаемую высоту. Я хочу, чтобы обеспечить соблюдение автоматической компоновки для расчета высоты ячейкиUICollectionView - динамическая высота ячейки?

Вызов dequeueReusableCellWithReuseIdentifier за пределами cellForItemAtIndexPath разрывов CollectionView и заставляет его врезаться

Другой проблемой является клетка не в отдельном XIb, так что я не могу вручную создать экземпляр временный и использовать его для расчета высоты.

Любые решения для этого?

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) 
} 

EDIT:

Крушение происходит, как только dequeueReusableCellWithReuseIdentifier называется. Если я не называю этот метод и вместо того, чтобы возвращать размер все прекрасно работают и клетки показывают без вычисленного размера

отрицательных или нулевых размеры не поддерживаются в макете потока

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 

ответ

18

Вот Ray Wenderlich tutorial который показывает вам, как использовать AutoLayout для динамического размера UITableViewCell. Я бы подумал, что это будет то же самое для UICollectionViewCell.

В принципе, вы в конечном итоге вычеркиваете и настраиваете прототип ячейки и захватываете ее высоту. Прочитав эту статью, я решил NOT реализовать этот метод и просто написать ясный явный код размера.

Вот что я считаю «секретный соус» для всей статьи:

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


EDIT: Я сделал некоторые исследования в вашей аварии, и решил, что нет никакого способа, чтобы получить это без пользовательского XIB. Хотя это немного разочаровывает, вы должны иметь возможность вырезать и вставлять с вашей раскадровки в обычную, пустую 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) дает мне это:

enter image description here

+2

Это не работает. на collectionView он падает. – aryaxt

+0

... аварии, где, с каким видом аварии? – Tommy

+0

Спасибо за ответ, что от этого не получается – aryaxt

20

Я просто столкнулся с этой проблемой на UICollectionView и тем, как я ее решил, как и в предыдущем ответе, но в чистом виде UICollectionView.

  1. Создайте пользовательский UICollectionViewCell, который содержит все, что вы будете заполнять, чтобы сделать его динамичным. Я создал для себя свой собственный .xib, поскольку это похоже на самый простой подход.

  2. Добавить ограничения в этом .xib, которые позволяют вычислять ячейку сверху вниз.Повторная калибровка не будет работать, если вы не указали всю высоту. Скажем, у вас есть вид сверху, затем надпись под ним и еще одна надпись. Вам нужно будет подключить ограничения к вершине ячейки вверху этого представления, затем к нижней части окна вверху первой метки, нижней части первой метки к верхней части второй метки и нижней части второй метки к нижней части ячейки.

  3. Загрузите .xib в ViewController и зарегистрировать его с CollectionView на viewDidLoad

    let nib = UINib(nibName: CustomCellName, bundle: nil) 
    self.collectionView!.registerNib(nib, forCellWithReuseIdentifier: "customCellID")` 
    
  4. нагрузки второй экземпляр этого XIb в класс и хранить его как свойство, так что вы можете использовать его для определить размер того, что эта клетка должна быть

    let sizingNibNew = NSBundle.mainBundle().loadNibNamed(CustomCellName, owner: CustomCellName.self, options: nil) as NSArray 
    self.sizingNibNew = (sizingNibNew.objectAtIndex(0) as? CustomViewCell)! 
    
  5. Реализовать UICollectionViewFlowLayoutDelegate в контроллере представления. Метод, который имеет значение, называется sizeForItemAtIndexPath. Внутри этого метода вам нужно будет вытащить данные из источника данных, который связан с этой ячейкой из indexPath. Затем настройте sizingCell и вызовите preferredLayoutSizeFittingSize. Метод возвращает CGSize, который будет состоять из ширины минус вложений содержимого и высоты, возвращаемой с 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) 
    } 
    
  6. В классе самой пользовательской ячейки вам необходимо переопределить awakeFromNib и сказать contentView, что его размер должен быть гибким

    override func awakeFromNib() { 
        super.awakeFromNib() 
        self.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleHeight] 
    } 
    
  7. В пользовательской ячейке переопределении layoutSubviews

    override func layoutSubviews() { 
        self.layoutIfNeeded() 
        } 
    
  8. В классе специального ячеистого ящика preferredLayoutSizeFittingSize. Здесь вам понадобятся какие-либо обманки на предметах, которые выложены. Если его ярлык вам нужно будет указать, какой должна быть его предпочтительнаяMaxWidth.

    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 или другие напуганные цифры, чем вы неправильно настроили свои ограничения.

+0

Это отличный код кода. Действительно помог мне. У меня была ячейка со стеклянными панелями и две расширяющиеся метки внутри - ячейка теперь расширяется, чтобы соответствовать содержимому! –

+0

@JamesMundy благодарит за комментарий, я надеялся, что это поможет ему разобраться! – bolnad

+0

@bolnad: Бинго! Это сработало. Но я хочу создать авторасширяющуюся ячейку, на основе которой расширяются/заключаются контракты на выбор. Любые идеи? –

7

Мы можем поддерживать динамическую высоту для ячейки сбора данных без 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); 
} 

Убедитесь, что numberOfLines в ИБ должен быть равен 0.

+1

Жаль, что я мог бы проголосовать за это дважды! – Chris

+0

Такое простое решение!Я сделал статическую функцию в своей ячейке просмотра коллекции, чтобы выполнить этот расчет и вернуть высоту. –

1

Я последовал шаги, упомянутые в этом так и все в порядке, за исключением, когда моя коллекция View имеет меньше данных (текст), чтобы сделать его достаточно широко. Проверка документации в systemLyaoutSizeFittingSize, у меня есть это решение, так что моя клетка взять ширину, как я просил:

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

Apple, док:

Эквивалент представляемого -systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority: с UILayoutPriorityFittingSizeLevel для обоих приоритетов.

Хотя значение по умолчанию «довольно низкий», согласно документу от Apple:

При отправке - [UIView systemLayoutSizeFittingSize:], размер фитинга наиболее близко к целевому размеру (аргумент) является вычислен. UILayoutPriorityFittingSizeLevel - это уровень приоритета, с которым представление хочет соответствовать целевому размеру в этом вычислении. Это довольно низко. Как правило, нецелесообразно ограничивать этот приоритет. Вы хотите быть выше или ниже.

Таким образом, мое изменение поведения по умолчанию заключается в обеспечении ширины (горизонтальной установки) с помощью UILayoutPriorityRequired.

0

Follow bolnad answer до Шаг 4.

Затем сделать это проще, заменив все остальные шаги:

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) 
} 
1

TL; DR: Сканирование вниз изображения, а затем проверить working project here.

Обновление моего ответа для более простого решения, которое я нашел ..

В моем случае я хотел исправить ширину и иметь v . Я хотел, чтобы решение для многоразового использования, которое обрабатывало ротацию и не требовало большого вмешательства.

То, что я прибыл в, было переопределение (только) 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) 
    }() 
} 

Но откуда эта ширина?Он настраивается через estimatedItemSize на макете потока Представление коллекции в:

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) 
} 

Окончательный результат выглядит следующим образом:

enter image description here

И я опубликовал a working sample here.

Смежные вопросы