Вказання одного розміру комірок у UICollectionView за допомогою автоматичного макета


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які просто представляють нульовий розмір і досить великий.

Чи 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 вимагає двоступеневого планування. Робота виконується методом 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
 }

(Цей код адаптований із зразкового коду Apple із зразкового коду сесії WWDC2014 у розділі "Розширений перегляд колекції".)

Пара помічає. Він використовує layoutIfNeeded () для примусового розташування всієї комірки, щоб обчислити та встановити ширину мітки. Але цього недостатньо. Я вважаю, що вам також потрібно встановити preferredMaxLayoutWidthтак, щоб мітка використовувала цю ширину за допомогою автоматичного макета. І тільки тоді ви можете використовувати systemLayoutSizeFittingSizeдля того, щоб змусити клітинку обчислити її висоту, враховуючи мітку.

Чи подобається мені такий підхід? Немає!! Це здається занадто складним, і він робить макет двічі. Але поки продуктивність не стає проблемою, я краще виконувати макет двічі під час виконання, ніж доводиться визначати її двічі в коді, що, здається, є єдиною іншою альтернативою.

Я сподіваюсь, що врешті-решт самоклеючі клітини працюватимуть інакше, і все це стане набагато простішим.

Приклад проекту, що показує його на роботі.

Але чому б не просто використовувати саморозмірні клітини?

Теоретично нові засоби iOS8 для "саморозмірних комірок" повинні зробити це непотрібним. Якщо ви визначили клітинку за допомогою автоматичного макета (AL), то вигляд колекції повинен бути досить розумним, щоб дозволити розміру і викласти правильно. На практиці я не бачив жодних прикладів, які б змусили це працювати з багаторядковими мітками. Я думаю, що це частково тому, що механізм самовимірювання клітин все ще є помилковим.

Але я б сказав, що це здебільшого через звичайну хитрість автоматичного розкладу та міток, а саме те, що для UILabels потрібен в основному процес двоетапної компонування. Мені незрозуміло, як можна виконати обидва кроки за допомогою самостійних розмірів комірок.

І як я вже говорив, це справді робота для іншого планування. Це суть потоку компонування потоку в тому, що він розміщує речі, які мають розмір, а не фіксує ширину і дозволяє їм вибирати висоту.

А як щодо переважнихLayoutAttributesFittingAttributes:?

preferredLayoutAttributesFittingAttributes:Метод є відволікаючим маневром, я думаю. Це лише там, де застосовується новий механізм саморозмірного осередку. Отже, це не відповідь, якщо цей механізм є ненадійним.

А що з systemlayoutSizeFittingSize :?

Ти маєш рацію, що документи плутають.

Документи про те systemLayoutSizeFittingSize:і systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:те, і інше пропонують вам пройти тільки UILayoutFittingCompressedSizeі UILayoutFittingExpandedSizeяк targetSize. Однак сам підпис методу, заголовок коментарів та поведінка функцій вказують на те, що вони відповідають точному значенню targetSizeпараметра.

Насправді, якщо ви встановите UICollectionViewFlowLayoutDelegate.estimatedItemSizeдля того, щоб увімкнути новий механізм саморозмірної комірки, це значення, здається, передається як targetSize. І, UILabel.systemLayoutSizeFittingSizeздається, повертає точно такі ж значення, як і UILabel.sizeThatFits. Це підозріло, враховуючи, що аргумент до systemLayoutSizeFittingSizeповинен бути грубою ціллю, а аргумент до sizeThatFits:повинен бути максимально обмежуючим розміром.

Більше ресурсів

Хоча сумно думати, що така рутинна вимога повинна вимагати «ресурсів дослідження», я думаю, що це є. Хорошими прикладами та дискусіями є:


Чому ви повертаєте кадр до старого кадру?
vrwim

Я це роблю, тому ця функція може використовуватися лише для визначення бажаного розміру макета подання, не впливаючи на стан макета подання, до якого ви його викликаєте. Практично це не має значення, якщо ви тримаєте цей погляд лише з метою робити макетні розрахунки. Також те, що я роблю тут, є недостатньою. Одночасне відновлення кадру насправді не повертає його до його попереднього стану, оскільки виконання макету може змінити макети підпогляду.
algal

1
Це такий безлад для чогось, чим люди займаються з ios6. Apple завжди виходить певним чином, але це ніколи не зовсім правильно.
GregP

1
Ви можете вручну інстанціювати осередок розміру в viewDidLoad і утримувати в ньому перегляд спеціального властивості. Це лише одна клітина, так що це дешево. Оскільки ви використовуєте його лише для розміру розмірів і керуєте всією взаємодією з ним, йому не потрібно брати участь у механізмі повторного використання комірок подання колекції, тому вам не потрібно отримувати його з самого подання колекції.
водорості

1
Як на землі ще можуть бути розбиті саморозмірні клітини ?!
Рубін Скреттон

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

1
Неможливо, щоб це працювало на iOS (9.3). На 10 це чудово працює. Додаток виходить з ладу на _updateVisibleCells (нескінченна рекурсія?). openradar.me/25303115
cristiano2lopes

@ cristiano2lopes це добре працює для мене на iOS 9.3. Переконайтеся, що ви надаєте обґрунтовану оцінку, і переконайтеся, що ваші обмеження в клітинках встановлені для вирішення правильного розміру.
Джордан Сміт

Це працює дивовижно. Я використовую Xcode 8 і перевірений в iOS 9.3.3 (фізичний пристрій), і він не виходить з ладу! Чудово працює. Просто переконайтесь, що ваша оцінка хороша!
гамед

1
@JordanSmith Це не працює в iOS 10. Чи є у вас демонстраційний зразок. Я спробував багато справ зробити висоту осередку для перегляду колекції на основі її вмісту, але нічого. Я використовую автоматичний макет в iOS 10 і використовую швидкий 3.
Ekta Padaliya

6

Простий спосіб зробити це в iOS 9 за допомогою декількох рядків кодів - горизонтальний зразок (фіксуючи його висоту на висоті перегляду колекції):

Запустіть макет потоку перегляду колекції за допомогою estimatedItemSizeввімкнення, щоб увімкнути клітинку з розміром:

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

Реалізуйте делегат макета перегляду колекції (більшу частину свого контролера представлення) 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 :. Вам вдалося змусити це працювати, не встановлюючи десь і бажануMaxLayoutWidth? На мій досвід, якщо ви не встановлюєте бажануMaxLayoutWidth, вам потрібно зробити додатковий макет вручну, наприклад, переосмисливши sizeThatFits: з обчисленнями, які відтворюють логіку обмежень для автоматичного планування, визначених в інших місцях.
algal

@algal preferenceMaxLayoutWidth є властивістю UILabel? Не зовсім впевнений, наскільки це актуально?
Даніель Галаско

На жаль! Добрий момент, це не так! Я ніколи не обробляв це за допомогою UITextView, тому не розумів, що їх API відрізняються. Схоже, UITextView.systemLayoutSizeFittingSize поважає свій кадр, оскільки у нього немає intrinsicContentSize. Але UILabel.systemLayoutSizeFittingSize ігнорує кадр, оскільки він має intrinsicContentSize, тому бажанаMaxLayoutWidth необхідна для отримання intrinsicContentSize, щоб виставити обернену висоту AL. Все ще здається, що UITextView знадобиться два проходи або ручне розташування, але я не думаю, що я все це розумію повністю.
algal

@algal Я можу погодитися, я також зазнав деяких волохатих корчів при використанні UILabel. Налаштування бажаної ширини вирішує проблему, але було б непогано мати більш динамічний підхід, оскільки цю ширину потрібно масштабувати з її оглядом. Більшість моїх проектів не використовують бажану власність, я, як правило, використовую це як швидке виправлення, оскільки це завжди глибша проблема
Даніель Галаско,

0

ТАК це можна зробити за допомогою автоматичного макета програмно та встановивши обмеження в раскадровці або xib. Потрібно додати обмеження, щоб розмір ширини залишався постійним і встановлював висоту, більшу або рівну.
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatic/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
Сподіваюся, що це буде корисно і вирішити вашу проблему.


3
Це повинно працювати для абсолютного значення ширини, але якщо ви хочете, щоб комірка завжди була повною шириною подання колекції, вона не працюватиме.
Водоспад Майкл

@MichaelWaterfall Мені вдалося змусити його працювати на повну ширину за допомогою функції AutoLayout. Я додав обмеження на ширину для мого перегляду вмісту всередині комірки. Тоді в cellForItemAtIndexPathоновленні обмеження: cell.constraintItemWidth.constant = UIScreen.main.bounds.width. Мій додаток лише для портатів. Можливо, ви хочете reloadDataпісля зміни орієнтації оновити обмеження.
Flitskikker
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.